From 76abf2200cb297e1b89abaf44d8997f047848d95 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Tue, 21 May 2024 20:08:53 -0500 Subject: [PATCH 1/4] Uncouple safe_mode from OTA (#6759) --- CODEOWNERS | 2 +- esphome/components/esphome/ota/__init__.py | 30 ++--- .../components/esphome/ota/ota_esphome.cpp | 98 +------------- esphome/components/esphome/ota/ota_esphome.h | 27 +--- esphome/components/ota/__init__.py | 4 +- esphome/components/safe_mode/__init__.py | 53 +++++++- .../components/safe_mode/button/__init__.py | 13 +- .../safe_mode/button/safe_mode_button.cpp | 6 +- .../safe_mode/button/safe_mode_button.h | 6 +- esphome/components/safe_mode/safe_mode.cpp | 125 ++++++++++++++++++ esphome/components/safe_mode/safe_mode.h | 44 ++++++ .../components/safe_mode/switch/__init__.py | 13 +- .../safe_mode/switch/safe_mode_switch.cpp | 9 +- .../safe_mode/switch/safe_mode_switch.h | 6 +- esphome/cpp_helpers.py | 17 +-- tests/components/ota/common.yaml | 3 - tests/components/safe_mode/common.yaml | 6 +- tests/test1.yaml | 7 +- tests/test2.yaml | 4 +- tests/test3.yaml | 6 +- tests/test4.yaml | 3 +- 21 files changed, 282 insertions(+), 200 deletions(-) create mode 100644 esphome/components/safe_mode/safe_mode.cpp create mode 100644 esphome/components/safe_mode/safe_mode.h diff --git a/CODEOWNERS b/CODEOWNERS index dc6c8caa87..22e581275b 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -306,7 +306,7 @@ esphome/components/rp2040_pwm/* @jesserockz esphome/components/rpi_dpi_rgb/* @clydebarrow esphome/components/rtl87xx/* @kuba2k2 esphome/components/rtttl/* @glmnet -esphome/components/safe_mode/* @jsuanet @paulmonigatti +esphome/components/safe_mode/* @jsuanet @kbx81 @paulmonigatti esphome/components/scd4x/* @martgras @sjtrny esphome/components/script/* @esphome/core esphome/components/sdm_meter/* @jesserockz @polyfaces diff --git a/esphome/components/esphome/ota/__init__.py b/esphome/components/esphome/ota/__init__.py index abe9323b53..c5903974c2 100644 --- a/esphome/components/esphome/ota/__init__.py +++ b/esphome/components/esphome/ota/__init__.py @@ -1,19 +1,16 @@ -from esphome.cpp_generator import RawExpression import esphome.codegen as cg import esphome.config_validation as cv from esphome.components.ota import BASE_OTA_SCHEMA, ota_to_code, OTAComponent from esphome.const import ( CONF_ID, CONF_NUM_ATTEMPTS, - CONF_OTA, CONF_PASSWORD, CONF_PORT, CONF_REBOOT_TIMEOUT, CONF_SAFE_MODE, CONF_VERSION, - KEY_PAST_SAFE_MODE, ) -from esphome.core import CORE, coroutine_with_priority +from esphome.core import coroutine_with_priority CODEOWNERS = ["@esphome/core"] @@ -28,7 +25,6 @@ CONFIG_SCHEMA = ( cv.Schema( { cv.GenerateID(): cv.declare_id(ESPHomeOTAComponent), - cv.Optional(CONF_SAFE_MODE, default=True): cv.boolean, cv.Optional(CONF_VERSION, default=2): cv.one_of(1, 2, int=True), cv.SplitDefault( CONF_PORT, @@ -39,10 +35,15 @@ CONFIG_SCHEMA = ( rtl87xx=8892, ): cv.port, cv.Optional(CONF_PASSWORD): cv.string, - cv.Optional( - CONF_REBOOT_TIMEOUT, default="5min" - ): cv.positive_time_period_milliseconds, - cv.Optional(CONF_NUM_ATTEMPTS, default="10"): cv.positive_not_null_int, + cv.Optional(CONF_NUM_ATTEMPTS): cv.invalid( + f"'{CONF_SAFE_MODE}' (and its related configuration variables) has moved from 'ota' to its own component. See https://esphome.io/components/safe_mode" + ), + cv.Optional(CONF_REBOOT_TIMEOUT): cv.invalid( + f"'{CONF_SAFE_MODE}' (and its related configuration variables) has moved from 'ota' to its own component. See https://esphome.io/components/safe_mode" + ), + cv.Optional(CONF_SAFE_MODE): cv.invalid( + f"'{CONF_SAFE_MODE}' (and its related configuration variables) has moved from 'ota' to its own component. See https://esphome.io/components/safe_mode" + ), } ) .extend(BASE_OTA_SCHEMA) @@ -50,10 +51,8 @@ CONFIG_SCHEMA = ( ) -@coroutine_with_priority(50.0) +@coroutine_with_priority(52.0) async def to_code(config): - CORE.data[CONF_OTA] = {} - var = cg.new_Pvariable(config[CONF_ID]) await ota_to_code(var, config) cg.add(var.set_port(config[CONF_PORT])) @@ -63,10 +62,3 @@ async def to_code(config): cg.add_define("USE_OTA_VERSION", config[CONF_VERSION]) await cg.register_component(var, config) - - if config[CONF_SAFE_MODE]: - condition = var.should_enter_safe_mode( - config[CONF_NUM_ATTEMPTS], config[CONF_REBOOT_TIMEOUT] - ) - cg.add(RawExpression(f"if ({condition}) return")) - CORE.data[CONF_OTA][KEY_PAST_SAFE_MODE] = True diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index f2f1cfc6a8..9d5044aaeb 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -78,23 +78,9 @@ void ESPHomeOTAComponent::dump_config() { ESP_LOGCONFIG(TAG, " Password configured"); } #endif - if (this->has_safe_mode_ && this->safe_mode_rtc_value_ > 1 && - this->safe_mode_rtc_value_ != ESPHomeOTAComponent::ENTER_SAFE_MODE_MAGIC) { - ESP_LOGW(TAG, "Last reset occurred too quickly; safe mode will be invoked in %" PRIu32 " restarts", - this->safe_mode_num_attempts_ - this->safe_mode_rtc_value_); - } } -void ESPHomeOTAComponent::loop() { - this->handle_(); - - if (this->has_safe_mode_ && (millis() - this->safe_mode_start_time_) > this->safe_mode_enable_time_) { - this->has_safe_mode_ = false; - // successful boot, reset counter - ESP_LOGI(TAG, "Boot seems successful; resetting boot loop counter"); - this->clean_rtc(); - } -} +void ESPHomeOTAComponent::loop() { this->handle_(); } static const uint8_t FEATURE_SUPPORTS_COMPRESSION = 0x01; @@ -423,86 +409,4 @@ bool ESPHomeOTAComponent::writeall_(const uint8_t *buf, size_t len) { float ESPHomeOTAComponent::get_setup_priority() const { return setup_priority::AFTER_WIFI; } uint16_t ESPHomeOTAComponent::get_port() const { return this->port_; } void ESPHomeOTAComponent::set_port(uint16_t port) { this->port_ = port; } - -void ESPHomeOTAComponent::set_safe_mode_pending(const bool &pending) { - if (!this->has_safe_mode_) - return; - - uint32_t current_rtc = this->read_rtc_(); - - if (pending && current_rtc != ESPHomeOTAComponent::ENTER_SAFE_MODE_MAGIC) { - ESP_LOGI(TAG, "Device will enter safe mode on next boot"); - this->write_rtc_(ESPHomeOTAComponent::ENTER_SAFE_MODE_MAGIC); - } - - if (!pending && current_rtc == ESPHomeOTAComponent::ENTER_SAFE_MODE_MAGIC) { - ESP_LOGI(TAG, "Safe mode pending has been cleared"); - this->clean_rtc(); - } -} -bool ESPHomeOTAComponent::get_safe_mode_pending() { - return this->has_safe_mode_ && this->read_rtc_() == ESPHomeOTAComponent::ENTER_SAFE_MODE_MAGIC; -} - -bool ESPHomeOTAComponent::should_enter_safe_mode(uint8_t num_attempts, uint32_t enable_time) { - this->has_safe_mode_ = true; - this->safe_mode_start_time_ = millis(); - this->safe_mode_enable_time_ = enable_time; - this->safe_mode_num_attempts_ = num_attempts; - this->rtc_ = global_preferences->make_preference(233825507UL, false); - this->safe_mode_rtc_value_ = this->read_rtc_(); - - bool is_manual_safe_mode = this->safe_mode_rtc_value_ == ESPHomeOTAComponent::ENTER_SAFE_MODE_MAGIC; - - if (is_manual_safe_mode) { - ESP_LOGI(TAG, "Safe mode has been entered manually"); - } else { - ESP_LOGCONFIG(TAG, "There have been %" PRIu32 " suspected unsuccessful boot attempts", this->safe_mode_rtc_value_); - } - - if (this->safe_mode_rtc_value_ >= num_attempts || is_manual_safe_mode) { - this->clean_rtc(); - - if (!is_manual_safe_mode) { - ESP_LOGE(TAG, "Boot loop detected. Proceeding to safe mode"); - } - - this->status_set_error(); - this->set_timeout(enable_time, []() { - ESP_LOGE(TAG, "No OTA attempt made, restarting"); - App.reboot(); - }); - - // Delay here to allow power to stabilise before Wi-Fi/Ethernet is initialised. - delay(300); // NOLINT - App.setup(); - - ESP_LOGI(TAG, "Waiting for OTA attempt"); - - return true; - } else { - // increment counter - this->write_rtc_(this->safe_mode_rtc_value_ + 1); - return false; - } -} - -void ESPHomeOTAComponent::write_rtc_(uint32_t val) { - this->rtc_.save(&val); - global_preferences->sync(); -} - -uint32_t ESPHomeOTAComponent::read_rtc_() { - uint32_t val; - if (!this->rtc_.load(&val)) - return 0; - return val; -} - -void ESPHomeOTAComponent::clean_rtc() { this->write_rtc_(0); } - -void ESPHomeOTAComponent::on_safe_shutdown() { - if (this->has_safe_mode_ && this->read_rtc_() != ESPHomeOTAComponent::ENTER_SAFE_MODE_MAGIC) - this->clean_rtc(); -} } // namespace esphome diff --git a/esphome/components/esphome/ota/ota_esphome.h b/esphome/components/esphome/ota/ota_esphome.h index e8f36f05ca..42629b4346 100644 --- a/esphome/components/esphome/ota/ota_esphome.h +++ b/esphome/components/esphome/ota/ota_esphome.h @@ -15,17 +15,9 @@ class ESPHomeOTAComponent : public ota::OTAComponent { void set_auth_password(const std::string &password) { password_ = password; } #endif // USE_OTA_PASSWORD - /// Manually set the port OTA should listen on. + /// Manually set the port OTA should listen on void set_port(uint16_t port); - bool should_enter_safe_mode(uint8_t num_attempts, uint32_t enable_time); - - /// Set to true if the next startup will enter safe mode - void set_safe_mode_pending(const bool &pending); - bool get_safe_mode_pending(); - - // ========== INTERNAL METHODS ========== - // (In most use cases you won't need these) void setup() override; void dump_config() override; float get_setup_priority() const override; @@ -33,14 +25,7 @@ class ESPHomeOTAComponent : public ota::OTAComponent { uint16_t get_port() const; - void clean_rtc(); - - void on_safe_shutdown() override; - protected: - void write_rtc_(uint32_t val); - uint32_t read_rtc_(); - void handle_(); bool readall_(uint8_t *buf, size_t len); bool writeall_(const uint8_t *buf, size_t len); @@ -53,16 +38,6 @@ class ESPHomeOTAComponent : public ota::OTAComponent { std::unique_ptr server_; std::unique_ptr client_; - - bool has_safe_mode_{false}; ///< stores whether safe mode can be enabled - uint32_t safe_mode_start_time_; ///< stores when safe mode was enabled - uint32_t safe_mode_enable_time_{60000}; ///< The time safe mode should be on for - uint32_t safe_mode_rtc_value_; - uint8_t safe_mode_num_attempts_; - ESPPreferenceObject rtc_; - - static const uint32_t ENTER_SAFE_MODE_MAGIC = - 0x5afe5afe; ///< a magic number to indicate that safe mode should be entered on next boot }; } // namespace esphome diff --git a/esphome/components/ota/__init__.py b/esphome/components/ota/__init__.py index 3d2956931c..4e447bfb2d 100644 --- a/esphome/components/ota/__init__.py +++ b/esphome/components/ota/__init__.py @@ -6,7 +6,7 @@ from esphome.core import CORE, coroutine_with_priority from esphome.const import CONF_ESPHOME, CONF_OTA, CONF_PLATFORM, CONF_TRIGGER_ID CODEOWNERS = ["@esphome/core"] -AUTO_LOAD = ["md5"] +AUTO_LOAD = ["md5", "safe_mode"] IS_PLATFORM_COMPONENT = True @@ -76,7 +76,7 @@ BASE_OTA_SCHEMA = cv.Schema( ) -@coroutine_with_priority(51.0) +@coroutine_with_priority(54.0) async def to_code(config): cg.add_define("USE_OTA") diff --git a/esphome/components/safe_mode/__init__.py b/esphome/components/safe_mode/__init__.py index ab884bfee4..7f227d7dd1 100644 --- a/esphome/components/safe_mode/__init__.py +++ b/esphome/components/safe_mode/__init__.py @@ -1,5 +1,56 @@ +from esphome.cpp_generator import RawExpression import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import ( + CONF_DISABLED, + CONF_ID, + CONF_NUM_ATTEMPTS, + CONF_REBOOT_TIMEOUT, + CONF_SAFE_MODE, + KEY_PAST_SAFE_MODE, +) +from esphome.core import CORE, coroutine_with_priority -CODEOWNERS = ["@paulmonigatti", "@jsuanet"] + +CODEOWNERS = ["@paulmonigatti", "@jsuanet", "@kbx81"] safe_mode_ns = cg.esphome_ns.namespace("safe_mode") +SafeModeComponent = safe_mode_ns.class_("SafeModeComponent", cg.Component) + + +def _remove_id_if_disabled(value): + value = value.copy() + if value[CONF_DISABLED]: + value.pop(CONF_ID) + return value + + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(SafeModeComponent), + cv.Optional(CONF_DISABLED, default=False): cv.boolean, + cv.Optional(CONF_NUM_ATTEMPTS, default="10"): cv.positive_not_null_int, + cv.Optional( + CONF_REBOOT_TIMEOUT, default="5min" + ): cv.positive_time_period_milliseconds, + } + ).extend(cv.COMPONENT_SCHEMA), + _remove_id_if_disabled, +) + + +@coroutine_with_priority(50.0) +async def to_code(config): + if config[CONF_DISABLED]: + return + + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + + condition = var.should_enter_safe_mode( + config[CONF_NUM_ATTEMPTS], config[CONF_REBOOT_TIMEOUT] + ) + cg.add(RawExpression(f"if ({condition}) return")) + CORE.data[CONF_SAFE_MODE] = {} + CORE.data[CONF_SAFE_MODE][KEY_PAST_SAFE_MODE] = True diff --git a/esphome/components/safe_mode/button/__init__.py b/esphome/components/safe_mode/button/__init__.py index bd51d2e038..5662db8f7e 100644 --- a/esphome/components/safe_mode/button/__init__.py +++ b/esphome/components/safe_mode/button/__init__.py @@ -1,16 +1,15 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import button -from esphome.components.esphome.ota import ESPHomeOTAComponent from esphome.const import ( - CONF_ESPHOME, + CONF_SAFE_MODE, DEVICE_CLASS_RESTART, ENTITY_CATEGORY_CONFIG, ICON_RESTART_ALERT, ) -from .. import safe_mode_ns +from .. import safe_mode_ns, SafeModeComponent -DEPENDENCIES = ["ota.esphome"] +DEPENDENCIES = ["safe_mode"] SafeModeButton = safe_mode_ns.class_("SafeModeButton", button.Button, cg.Component) @@ -21,7 +20,7 @@ CONFIG_SCHEMA = ( entity_category=ENTITY_CATEGORY_CONFIG, icon=ICON_RESTART_ALERT, ) - .extend({cv.GenerateID(CONF_ESPHOME): cv.use_id(ESPHomeOTAComponent)}) + .extend({cv.GenerateID(CONF_SAFE_MODE): cv.use_id(SafeModeComponent)}) .extend(cv.COMPONENT_SCHEMA) ) @@ -30,5 +29,5 @@ async def to_code(config): var = await button.new_button(config) await cg.register_component(var, config) - ota = await cg.get_variable(config[CONF_ESPHOME]) - cg.add(var.set_ota(ota)) + safe_mode_component = await cg.get_variable(config[CONF_SAFE_MODE]) + cg.add(var.set_safe_mode(safe_mode_component)) diff --git a/esphome/components/safe_mode/button/safe_mode_button.cpp b/esphome/components/safe_mode/button/safe_mode_button.cpp index d513b79c12..261688807a 100644 --- a/esphome/components/safe_mode/button/safe_mode_button.cpp +++ b/esphome/components/safe_mode/button/safe_mode_button.cpp @@ -8,11 +8,13 @@ namespace safe_mode { static const char *const TAG = "safe_mode.button"; -void SafeModeButton::set_ota(esphome::ESPHomeOTAComponent *ota) { this->ota_ = ota; } +void SafeModeButton::set_safe_mode(SafeModeComponent *safe_mode_component) { + this->safe_mode_component_ = safe_mode_component; +} void SafeModeButton::press_action() { ESP_LOGI(TAG, "Restarting device in safe mode..."); - this->ota_->set_safe_mode_pending(true); + this->safe_mode_component_->set_safe_mode_pending(true); // Let MQTT settle a bit delay(100); // NOLINT diff --git a/esphome/components/safe_mode/button/safe_mode_button.h b/esphome/components/safe_mode/button/safe_mode_button.h index a306735b7f..fea0955abb 100644 --- a/esphome/components/safe_mode/button/safe_mode_button.h +++ b/esphome/components/safe_mode/button/safe_mode_button.h @@ -1,7 +1,7 @@ #pragma once #include "esphome/components/button/button.h" -#include "esphome/components/esphome/ota/ota_esphome.h" +#include "esphome/components/safe_mode/safe_mode.h" #include "esphome/core/component.h" namespace esphome { @@ -10,10 +10,10 @@ namespace safe_mode { class SafeModeButton : public button::Button, public Component { public: void dump_config() override; - void set_ota(esphome::ESPHomeOTAComponent *ota); + void set_safe_mode(SafeModeComponent *safe_mode_component); protected: - esphome::ESPHomeOTAComponent *ota_; + SafeModeComponent *safe_mode_component_; void press_action() override; }; diff --git a/esphome/components/safe_mode/safe_mode.cpp b/esphome/components/safe_mode/safe_mode.cpp new file mode 100644 index 0000000000..06772ae1e0 --- /dev/null +++ b/esphome/components/safe_mode/safe_mode.cpp @@ -0,0 +1,125 @@ +#include "safe_mode.h" + +#include "esphome/core/application.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" +#include "esphome/core/util.h" + +#include +#include +#include + +namespace esphome { +namespace safe_mode { + +static const char *const TAG = "safe_mode"; + +void SafeModeComponent::dump_config() { + ESP_LOGCONFIG(TAG, "Safe Mode:"); + ESP_LOGCONFIG(TAG, " Invoke after %u boot attempts", this->safe_mode_num_attempts_); + ESP_LOGCONFIG(TAG, " Remain in safe mode for %" PRIu32 " seconds", + this->safe_mode_enable_time_ / 1000); // because milliseconds + + if (this->safe_mode_rtc_value_ > 1 && this->safe_mode_rtc_value_ != SafeModeComponent::ENTER_SAFE_MODE_MAGIC) { + auto remaining_restarts = this->safe_mode_num_attempts_ - this->safe_mode_rtc_value_; + if (remaining_restarts) { + ESP_LOGW(TAG, "Last reset occurred too quickly; safe mode will be invoked in %" PRIu32 " restarts", + remaining_restarts); + } else { + ESP_LOGW(TAG, "SAFE MODE IS ACTIVE"); + } + } +} + +float SafeModeComponent::get_setup_priority() const { return setup_priority::AFTER_WIFI; } + +void SafeModeComponent::loop() { + if (!this->boot_successful_ && (millis() - this->safe_mode_start_time_) > this->safe_mode_enable_time_) { + // successful boot, reset counter + ESP_LOGI(TAG, "Boot seems successful; resetting boot loop counter"); + this->clean_rtc(); + this->boot_successful_ = true; + } +} + +void SafeModeComponent::set_safe_mode_pending(const bool &pending) { + uint32_t current_rtc = this->read_rtc_(); + + if (pending && current_rtc != SafeModeComponent::ENTER_SAFE_MODE_MAGIC) { + ESP_LOGI(TAG, "Device will enter safe mode on next boot"); + this->write_rtc_(SafeModeComponent::ENTER_SAFE_MODE_MAGIC); + } + + if (!pending && current_rtc == SafeModeComponent::ENTER_SAFE_MODE_MAGIC) { + ESP_LOGI(TAG, "Safe mode pending has been cleared"); + this->clean_rtc(); + } +} + +bool SafeModeComponent::get_safe_mode_pending() { + return this->read_rtc_() == SafeModeComponent::ENTER_SAFE_MODE_MAGIC; +} + +bool SafeModeComponent::should_enter_safe_mode(uint8_t num_attempts, uint32_t enable_time) { + this->safe_mode_start_time_ = millis(); + this->safe_mode_enable_time_ = enable_time; + this->safe_mode_num_attempts_ = num_attempts; + this->rtc_ = global_preferences->make_preference(233825507UL, false); + this->safe_mode_rtc_value_ = this->read_rtc_(); + + bool is_manual_safe_mode = this->safe_mode_rtc_value_ == SafeModeComponent::ENTER_SAFE_MODE_MAGIC; + + if (is_manual_safe_mode) { + ESP_LOGI(TAG, "Safe mode invoked manually"); + } else { + ESP_LOGCONFIG(TAG, "There have been %" PRIu32 " suspected unsuccessful boot attempts", this->safe_mode_rtc_value_); + } + + if (this->safe_mode_rtc_value_ >= num_attempts || is_manual_safe_mode) { + this->clean_rtc(); + + if (!is_manual_safe_mode) { + ESP_LOGE(TAG, "Boot loop detected. Proceeding to safe mode"); + } + + this->status_set_error(); + this->set_timeout(enable_time, []() { + ESP_LOGW(TAG, "Safe mode enable time has elapsed -- restarting"); + App.reboot(); + }); + + // Delay here to allow power to stabilize before Wi-Fi/Ethernet is initialised + delay(300); // NOLINT + App.setup(); + + ESP_LOGW(TAG, "SAFE MODE IS ACTIVE"); + + return true; + } else { + // increment counter + this->write_rtc_(this->safe_mode_rtc_value_ + 1); + return false; + } +} + +void SafeModeComponent::write_rtc_(uint32_t val) { + this->rtc_.save(&val); + global_preferences->sync(); +} + +uint32_t SafeModeComponent::read_rtc_() { + uint32_t val; + if (!this->rtc_.load(&val)) + return 0; + return val; +} + +void SafeModeComponent::clean_rtc() { this->write_rtc_(0); } + +void SafeModeComponent::on_safe_shutdown() { + if (this->read_rtc_() != SafeModeComponent::ENTER_SAFE_MODE_MAGIC) + this->clean_rtc(); +} + +} // namespace safe_mode +} // namespace esphome diff --git a/esphome/components/safe_mode/safe_mode.h b/esphome/components/safe_mode/safe_mode.h new file mode 100644 index 0000000000..6c7450a6ff --- /dev/null +++ b/esphome/components/safe_mode/safe_mode.h @@ -0,0 +1,44 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/defines.h" +#include "esphome/core/helpers.h" +#include "esphome/core/preferences.h" + +namespace esphome { +namespace safe_mode { + +/// SafeModeComponent provides a safe way to recover from repeated boot failures +class SafeModeComponent : public Component { + public: + bool should_enter_safe_mode(uint8_t num_attempts, uint32_t enable_time); + + /// Set to true if the next startup will enter safe mode + void set_safe_mode_pending(const bool &pending); + bool get_safe_mode_pending(); + + void dump_config() override; + float get_setup_priority() const override; + void loop() override; + + void clean_rtc(); + + void on_safe_shutdown() override; + + protected: + void write_rtc_(uint32_t val); + uint32_t read_rtc_(); + + bool boot_successful_{false}; ///< set to true after boot is considered successful + uint32_t safe_mode_start_time_; ///< stores when safe mode was enabled + uint32_t safe_mode_enable_time_{60000}; ///< The time safe mode should remain active for + uint32_t safe_mode_rtc_value_; + uint8_t safe_mode_num_attempts_; + ESPPreferenceObject rtc_; + + static const uint32_t ENTER_SAFE_MODE_MAGIC = + 0x5afe5afe; ///< a magic number to indicate that safe mode should be entered on next boot +}; + +} // namespace safe_mode +} // namespace esphome diff --git a/esphome/components/safe_mode/switch/__init__.py b/esphome/components/safe_mode/switch/__init__.py index 0f8e500482..7271358149 100644 --- a/esphome/components/safe_mode/switch/__init__.py +++ b/esphome/components/safe_mode/switch/__init__.py @@ -1,15 +1,14 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import switch -from esphome.components.esphome.ota import ESPHomeOTAComponent from esphome.const import ( - CONF_ESPHOME, + CONF_SAFE_MODE, ENTITY_CATEGORY_CONFIG, ICON_RESTART_ALERT, ) -from .. import safe_mode_ns +from .. import safe_mode_ns, SafeModeComponent -DEPENDENCIES = ["ota.esphome"] +DEPENDENCIES = ["safe_mode"] SafeModeSwitch = safe_mode_ns.class_("SafeModeSwitch", switch.Switch, cg.Component) @@ -20,7 +19,7 @@ CONFIG_SCHEMA = ( entity_category=ENTITY_CATEGORY_CONFIG, icon=ICON_RESTART_ALERT, ) - .extend({cv.GenerateID(CONF_ESPHOME): cv.use_id(ESPHomeOTAComponent)}) + .extend({cv.GenerateID(CONF_SAFE_MODE): cv.use_id(SafeModeComponent)}) .extend(cv.COMPONENT_SCHEMA) ) @@ -29,5 +28,5 @@ async def to_code(config): var = await switch.new_switch(config) await cg.register_component(var, config) - ota = await cg.get_variable(config[CONF_ESPHOME]) - cg.add(var.set_ota(ota)) + safe_mode_component = await cg.get_variable(config[CONF_SAFE_MODE]) + cg.add(var.set_safe_mode(safe_mode_component)) diff --git a/esphome/components/safe_mode/switch/safe_mode_switch.cpp b/esphome/components/safe_mode/switch/safe_mode_switch.cpp index 71408df140..13b35ed210 100644 --- a/esphome/components/safe_mode/switch/safe_mode_switch.cpp +++ b/esphome/components/safe_mode/switch/safe_mode_switch.cpp @@ -6,9 +6,11 @@ namespace esphome { namespace safe_mode { -static const char *const TAG = "safe_mode_switch"; +static const char *const TAG = "safe_mode.switch"; -void SafeModeSwitch::set_ota(esphome::ESPHomeOTAComponent *ota) { this->ota_ = ota; } +void SafeModeSwitch::set_safe_mode(SafeModeComponent *safe_mode_component) { + this->safe_mode_component_ = safe_mode_component; +} void SafeModeSwitch::write_state(bool state) { // Acknowledge @@ -16,13 +18,14 @@ void SafeModeSwitch::write_state(bool state) { if (state) { ESP_LOGI(TAG, "Restarting device in safe mode..."); - this->ota_->set_safe_mode_pending(true); + this->safe_mode_component_->set_safe_mode_pending(true); // Let MQTT settle a bit delay(100); // NOLINT App.safe_reboot(); } } + void SafeModeSwitch::dump_config() { LOG_SWITCH("", "Safe Mode Switch", this); } } // namespace safe_mode diff --git a/esphome/components/safe_mode/switch/safe_mode_switch.h b/esphome/components/safe_mode/switch/safe_mode_switch.h index 5bd15a44de..24e660c803 100644 --- a/esphome/components/safe_mode/switch/safe_mode_switch.h +++ b/esphome/components/safe_mode/switch/safe_mode_switch.h @@ -1,6 +1,6 @@ #pragma once -#include "esphome/components/esphome/ota/ota_esphome.h" +#include "esphome/components/safe_mode/safe_mode.h" #include "esphome/components/switch/switch.h" #include "esphome/core/component.h" @@ -10,10 +10,10 @@ namespace safe_mode { class SafeModeSwitch : public switch_::Switch, public Component { public: void dump_config() override; - void set_ota(esphome::ESPHomeOTAComponent *ota); + void set_safe_mode(SafeModeComponent *safe_mode_component); protected: - esphome::ESPHomeOTAComponent *ota_; + SafeModeComponent *safe_mode_component_; void write_state(bool state) override; }; diff --git a/esphome/cpp_helpers.py b/esphome/cpp_helpers.py index ce494e5d9d..825224bb9d 100644 --- a/esphome/cpp_helpers.py +++ b/esphome/cpp_helpers.py @@ -3,12 +3,9 @@ import logging from esphome.const import ( CONF_DISABLED_BY_DEFAULT, CONF_ENTITY_CATEGORY, - CONF_ESPHOME, CONF_ICON, CONF_INTERNAL, CONF_NAME, - CONF_OTA, - CONF_PLATFORM, CONF_SAFE_MODE, CONF_SETUP_PRIORITY, CONF_TYPE_ID, @@ -141,22 +138,12 @@ async def build_registry_list(registry, config): async def past_safe_mode(): - ota_conf = {} - for ota_item in CORE.config.get(CONF_OTA, []): - if ota_item[CONF_PLATFORM] == CONF_ESPHOME: - ota_conf = ota_item - break - - if not ota_conf: - return - - safe_mode_enabled = ota_conf[CONF_SAFE_MODE] - if not safe_mode_enabled: + if CONF_SAFE_MODE not in CORE.config: return def _safe_mode_generator(): while True: - if CORE.data.get(CONF_OTA, {}).get(KEY_PAST_SAFE_MODE, False): + if CORE.data.get(CONF_SAFE_MODE, {}).get(KEY_PAST_SAFE_MODE, False): return yield diff --git a/tests/components/ota/common.yaml b/tests/components/ota/common.yaml index 4910e2d891..1433dada1f 100644 --- a/tests/components/ota/common.yaml +++ b/tests/components/ota/common.yaml @@ -4,11 +4,8 @@ wifi: ota: - platform: esphome - safe_mode: true password: "superlongpasswordthatnoonewillknow" port: 3286 - reboot_timeout: 2min - num_attempts: 5 on_begin: then: - logger.log: "OTA start" diff --git a/tests/components/safe_mode/common.yaml b/tests/components/safe_mode/common.yaml index 1dfc516254..9c1d1ad3f9 100644 --- a/tests/components/safe_mode/common.yaml +++ b/tests/components/safe_mode/common.yaml @@ -2,9 +2,9 @@ wifi: ssid: MySSID password: password1 -ota: - - platform: esphome - safe_mode: true +safe_mode: + num_attempts: 3 + reboot_timeout: 2min button: - platform: safe_mode diff --git a/tests/test1.yaml b/tests/test1.yaml index dc46b55c44..2a20a1bb45 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -264,13 +264,14 @@ uart: parity: EVEN baud_rate: 9600 +safe_mode: + num_attempts: 3 + reboot_timeout: 2min + ota: - platform: esphome - safe_mode: true password: "superlongpasswordthatnoonewillknow" port: 3286 - reboot_timeout: 2min - num_attempts: 5 on_state_change: then: lambda: >- diff --git a/tests/test2.yaml b/tests/test2.yaml index 54ff4807a3..92977697c1 100644 --- a/tests/test2.yaml +++ b/tests/test2.yaml @@ -79,11 +79,11 @@ uart: sequence: - lambda: UARTDebug::log_hex(direction, bytes, ':'); +safe_mode: + ota: - platform: esphome - safe_mode: true port: 3286 - num_attempts: 15 logger: level: DEBUG diff --git a/tests/test3.yaml b/tests/test3.yaml index 7554d4bcb2..d10413b142 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -327,11 +327,13 @@ modbus: vbus: uart_id: uart_4 +safe_mode: + num_attempts: 5 + reboot_timeout: 10min + ota: - platform: esphome - safe_mode: true port: 3286 - reboot_timeout: 15min logger: hardware_uart: UART1 diff --git a/tests/test4.yaml b/tests/test4.yaml index 86beee81c6..c9e8a27317 100644 --- a/tests/test4.yaml +++ b/tests/test4.yaml @@ -102,9 +102,10 @@ uart: baud_rate: 1200 parity: EVEN +safe_mode: + ota: - platform: esphome - safe_mode: true port: 3286 logger: From 1ca7c2d7ddc44f8debe288a98fbca3e40796f5cf Mon Sep 17 00:00:00 2001 From: Jeroen van Oort Date: Wed, 22 May 2024 06:17:32 +0200 Subject: [PATCH 2/4] Add support for acting as Modbus server (#4874) Co-authored-by: Jeroen van Oort Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/modbus/__init__.py | 35 ++++++++++++ esphome/components/modbus/modbus.cpp | 27 ++++++--- esphome/components/modbus/modbus.h | 9 +++ .../components/modbus_controller/__init__.py | 52 +++++++++++++++-- .../modbus_controller/modbus_controller.cpp | 57 +++++++++++++++++-- .../modbus_controller/modbus_controller.h | 22 +++++++ .../modbus_controller/test.esp32.yaml | 22 ++++++- 7 files changed, 203 insertions(+), 21 deletions(-) diff --git a/esphome/components/modbus/__init__.py b/esphome/components/modbus/__init__.py index 6fea7033f2..ae0c818c28 100644 --- a/esphome/components/modbus/__init__.py +++ b/esphome/components/modbus/__init__.py @@ -1,5 +1,9 @@ +from __future__ import annotations +from typing import Literal + import esphome.codegen as cg import esphome.config_validation as cv +import esphome.final_validate as fv from esphome.cpp_helpers import gpio_pin_expression from esphome.components import uart from esphome.const import ( @@ -17,13 +21,21 @@ Modbus = modbus_ns.class_("Modbus", cg.Component, uart.UARTDevice) ModbusDevice = modbus_ns.class_("ModbusDevice") MULTI_CONF = True +CONF_ROLE = "role" CONF_MODBUS_ID = "modbus_id" CONF_SEND_WAIT_TIME = "send_wait_time" +ModbusRole = modbus_ns.enum("ModbusRole") +MODBUS_ROLES = { + "client": ModbusRole.CLIENT, + "server": ModbusRole.SERVER, +} + CONFIG_SCHEMA = ( cv.Schema( { cv.GenerateID(): cv.declare_id(Modbus), + cv.Optional(CONF_ROLE, default="client"): cv.enum(MODBUS_ROLES), cv.Optional(CONF_FLOW_CONTROL_PIN): pins.gpio_output_pin_schema, cv.Optional( CONF_SEND_WAIT_TIME, default="250ms" @@ -43,6 +55,7 @@ async def to_code(config): await uart.register_uart_device(var, config) + cg.add(var.set_role(config[CONF_ROLE])) if CONF_FLOW_CONTROL_PIN in config: pin = await gpio_pin_expression(config[CONF_FLOW_CONTROL_PIN]) cg.add(var.set_flow_control_pin(pin)) @@ -62,6 +75,28 @@ def modbus_device_schema(default_address): return cv.Schema(schema) +def final_validate_modbus_device( + name: str, *, role: Literal["server", "client"] | None = None +): + def validate_role(value): + assert role in MODBUS_ROLES + if value != role: + raise cv.Invalid(f"Component {name} requires role to be {role}") + return value + + def validate_hub(hub_config): + hub_schema = {} + if role is not None: + hub_schema[cv.Required(CONF_ROLE)] = validate_role + + return cv.Schema(hub_schema, extra=cv.ALLOW_EXTRA)(hub_config) + + return cv.Schema( + {cv.Required(CONF_MODBUS_ID): fv.id_declaration_match_schema(validate_hub)}, + extra=cv.ALLOW_EXTRA, + ) + + async def register_modbus_device(var, config): parent = await cg.get_variable(config[CONF_MODBUS_ID]) cg.add(var.set_parent(parent)) diff --git a/esphome/components/modbus/modbus.cpp b/esphome/components/modbus/modbus.cpp index 137fb0b26b..f8dd4c18b9 100644 --- a/esphome/components/modbus/modbus.cpp +++ b/esphome/components/modbus/modbus.cpp @@ -77,7 +77,13 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) { ESP_LOGD(TAG, "Modbus user-defined function %02X found", function_code); } else { - // the response for write command mirrors the requests and data startes at offset 2 instead of 3 for read commands + // data starts at 2 and length is 4 for read registers commands + if (this->role == ModbusRole::SERVER && (function_code == 0x3 || function_code == 0x4)) { + data_offset = 2; + data_len = 4; + } + + // the response for write command mirrors the requests and data starts at offset 2 instead of 3 for read commands if (function_code == 0x5 || function_code == 0x06 || function_code == 0xF || function_code == 0x10) { data_offset = 2; data_len = 4; @@ -123,6 +129,9 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) { // Ignore modbus exception not related to a pending command ESP_LOGD(TAG, "Ignoring Modbus error - not expecting a response"); } + } else if (this->role == ModbusRole::SERVER && (function_code == 0x3 || function_code == 0x4)) { + device->on_modbus_read_registers(function_code, uint16_t(data[1]) | (uint16_t(data[0]) << 8), + uint16_t(data[3]) | (uint16_t(data[2]) << 8)); } else { device->on_modbus_data(data); } @@ -164,16 +173,18 @@ void Modbus::send(uint8_t address, uint8_t function_code, uint16_t start_address std::vector data; data.push_back(address); data.push_back(function_code); - data.push_back(start_address >> 8); - data.push_back(start_address >> 0); - if (function_code != 0x5 && function_code != 0x6) { - data.push_back(number_of_entities >> 8); - data.push_back(number_of_entities >> 0); + if (this->role == ModbusRole::CLIENT) { + data.push_back(start_address >> 8); + data.push_back(start_address >> 0); + if (function_code != 0x5 && function_code != 0x6) { + data.push_back(number_of_entities >> 8); + data.push_back(number_of_entities >> 0); + } } if (payload != nullptr) { - if (function_code == 0xF || function_code == 0x10) { // Write multiple - data.push_back(payload_len); // Byte count is required for write + if (this->role == ModbusRole::SERVER || function_code == 0xF || function_code == 0x10) { // Write multiple + data.push_back(payload_len); // Byte count is required for write } else { payload_len = 2; // Write single register or coil } diff --git a/esphome/components/modbus/modbus.h b/esphome/components/modbus/modbus.h index dd8732c6e9..4a78ed4aab 100644 --- a/esphome/components/modbus/modbus.h +++ b/esphome/components/modbus/modbus.h @@ -8,6 +8,11 @@ namespace esphome { namespace modbus { +enum ModbusRole { + CLIENT, + SERVER, +}; + class ModbusDevice; class Modbus : public uart::UARTDevice, public Component { @@ -27,11 +32,14 @@ class Modbus : public uart::UARTDevice, public Component { void send(uint8_t address, uint8_t function_code, uint16_t start_address, uint16_t number_of_entities, uint8_t payload_len = 0, const uint8_t *payload = nullptr); void send_raw(const std::vector &payload); + void set_role(ModbusRole role) { this->role = role; } void set_flow_control_pin(GPIOPin *flow_control_pin) { this->flow_control_pin_ = flow_control_pin; } uint8_t waiting_for_response{0}; void set_send_wait_time(uint16_t time_in_ms) { send_wait_time_ = time_in_ms; } void set_disable_crc(bool disable_crc) { disable_crc_ = disable_crc; } + ModbusRole role; + protected: GPIOPin *flow_control_pin_{nullptr}; @@ -50,6 +58,7 @@ class ModbusDevice { void set_address(uint8_t address) { address_ = address; } virtual void on_modbus_data(const std::vector &data) = 0; virtual void on_modbus_error(uint8_t function_code, uint8_t exception_code) {} + virtual void on_modbus_read_registers(uint8_t function_code, uint16_t start_address, uint16_t number_of_registers){}; void send(uint8_t function, uint16_t start_address, uint16_t number_of_entities, uint8_t payload_len = 0, const uint8_t *payload = nullptr) { this->parent_->send(this->address_, function, start_address, number_of_entities, payload_len, payload); diff --git a/esphome/components/modbus_controller/__init__.py b/esphome/components/modbus_controller/__init__.py index 8703771c3a..b8ab48fcc6 100644 --- a/esphome/components/modbus_controller/__init__.py +++ b/esphome/components/modbus_controller/__init__.py @@ -23,6 +23,8 @@ CODEOWNERS = ["@martgras"] AUTO_LOAD = ["modbus"] +CONF_READ_LAMBDA = "read_lambda" +CONF_SERVER_REGISTERS = "server_registers" MULTI_CONF = True modbus_controller_ns = cg.esphome_ns.namespace("modbus_controller") @@ -31,6 +33,7 @@ ModbusController = modbus_controller_ns.class_( ) SensorItem = modbus_controller_ns.struct("SensorItem") +ServerRegister = modbus_controller_ns.struct("ServerRegister") ModbusFunctionCode_ns = modbus_controller_ns.namespace("ModbusFunctionCode") ModbusFunctionCode = ModbusFunctionCode_ns.enum("ModbusFunctionCode") @@ -94,10 +97,18 @@ TYPE_REGISTER_MAP = { "FP32_R": 2, } -MULTI_CONF = True - _LOGGER = logging.getLogger(__name__) +ModbusServerRegisterSchema = cv.Schema( + { + cv.GenerateID(): cv.declare_id(ServerRegister), + cv.Required(CONF_ADDRESS): cv.positive_int, + cv.Optional(CONF_VALUE_TYPE, default="U_WORD"): cv.enum(SENSOR_VALUE_TYPE), + cv.Required(CONF_READ_LAMBDA): cv.returning_lambda, + } +) + + CONFIG_SCHEMA = cv.All( cv.Schema( { @@ -106,6 +117,9 @@ CONFIG_SCHEMA = cv.All( CONF_COMMAND_THROTTLE, default="0ms" ): cv.positive_time_period_milliseconds, cv.Optional(CONF_OFFLINE_SKIP_UPDATES, default=0): cv.positive_int, + cv.Optional( + CONF_SERVER_REGISTERS, + ): cv.ensure_list(ModbusServerRegisterSchema), } ) .extend(cv.polling_component_schema("60s")) @@ -154,6 +168,17 @@ def validate_modbus_register(config): return config +def _final_validate(config): + if CONF_SERVER_REGISTERS in config: + return modbus.final_validate_modbus_device("modbus_controller", role="server")( + config + ) + return config + + +FINAL_VALIDATE_SCHEMA = _final_validate + + def modbus_calc_properties(config): byte_offset = 0 reg_count = 0 @@ -183,7 +208,7 @@ def modbus_calc_properties(config): async def add_modbus_base_properties( - var, config, sensor_type, lamdba_param_type=cg.float_, lamdba_return_type=float + var, config, sensor_type, lambda_param_type=cg.float_, lambda_return_type=float ): if CONF_CUSTOM_COMMAND in config: cg.add(var.set_custom_data(config[CONF_CUSTOM_COMMAND])) @@ -196,13 +221,13 @@ async def add_modbus_base_properties( config[CONF_LAMBDA], [ (sensor_type.operator("ptr"), "item"), - (lamdba_param_type, "x"), + (lambda_param_type, "x"), ( cg.std_vector.template(cg.uint8).operator("const").operator("ref"), "data", ), ], - return_type=cg.optional.template(lamdba_return_type), + return_type=cg.optional.template(lambda_return_type), ) cg.add(var.set_template(template_)) @@ -211,6 +236,23 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) cg.add(var.set_command_throttle(config[CONF_COMMAND_THROTTLE])) cg.add(var.set_offline_skip_updates(config[CONF_OFFLINE_SKIP_UPDATES])) + if CONF_SERVER_REGISTERS in config: + for server_register in config[CONF_SERVER_REGISTERS]: + cg.add( + var.add_server_register( + cg.new_Pvariable( + server_register[CONF_ID], + server_register[CONF_ADDRESS], + server_register[CONF_VALUE_TYPE], + TYPE_REGISTER_MAP[server_register[CONF_VALUE_TYPE]], + await cg.process_lambda( + server_register[CONF_READ_LAMBDA], + [], + return_type=cg.float_, + ), + ) + ) + ) await register_modbus_device(var, config) diff --git a/esphome/components/modbus_controller/modbus_controller.cpp b/esphome/components/modbus_controller/modbus_controller.cpp index 7565dc5e1b..9f73988b03 100644 --- a/esphome/components/modbus_controller/modbus_controller.cpp +++ b/esphome/components/modbus_controller/modbus_controller.cpp @@ -7,10 +7,7 @@ namespace modbus_controller { static const char *const TAG = "modbus_controller"; -void ModbusController::setup() { - // Modbus::setup(); - this->create_register_ranges_(); -} +void ModbusController::setup() { this->create_register_ranges_(); } /* To work with the existing modbus class and avoid polling for responses a command queue is used. @@ -102,6 +99,51 @@ void ModbusController::on_modbus_error(uint8_t function_code, uint8_t exception_ } } +void ModbusController::on_modbus_read_registers(uint8_t function_code, uint16_t start_address, + uint16_t number_of_registers) { + ESP_LOGD(TAG, + "Received read holding/input registers for device 0x%X. FC: 0x%X. Start address: 0x%X. Number of registers: " + "0x%X.", + this->address_, function_code, start_address, number_of_registers); + + std::vector sixteen_bit_response; + for (uint16_t current_address = start_address; current_address < start_address + number_of_registers;) { + bool found = false; + for (auto *server_register : this->server_registers_) { + if (server_register->address == current_address) { + float value = server_register->read_lambda(); + + ESP_LOGD(TAG, "Matched register. Address: 0x%02X. Value type: %zu. Register count: %u. Value: %0.1f.", + server_register->address, static_cast(server_register->value_type), + server_register->register_count, value); + number_to_payload(sixteen_bit_response, value, server_register->value_type); + current_address += server_register->register_count; + found = true; + break; + } + } + + if (!found) { + ESP_LOGW(TAG, "Could not match any register to address %02X. Sending exception response.", current_address); + std::vector error_response; + error_response.push_back(this->address_); + error_response.push_back(0x81); + error_response.push_back(0x02); + this->send_raw(error_response); + return; + } + } + + std::vector response; + for (auto v : sixteen_bit_response) { + auto decoded_value = decode_value(v); + response.push_back(decoded_value[0]); + response.push_back(decoded_value[1]); + } + + this->send(function_code, start_address, number_of_registers, response.size(), response.data()); +} + SensorSet ModbusController::find_sensors_(ModbusRegisterType register_type, uint16_t start_address) const { auto reg_it = find_if(begin(register_ranges_), end(register_ranges_), [=](RegisterRange const &r) { return (r.start_address == start_address && r.register_type == register_type); @@ -190,7 +232,7 @@ void ModbusController::update() { // walk through the sensors and determine the register ranges to read size_t ModbusController::create_register_ranges_() { register_ranges_.clear(); - if (sensorset_.empty()) { + if (this->parent_->role == modbus::ModbusRole::CLIENT && sensorset_.empty()) { ESP_LOGW(TAG, "No sensors registered"); return 0; } @@ -309,6 +351,11 @@ void ModbusController::dump_config() { ESP_LOGCONFIG(TAG, " Range type=%zu start=0x%X count=%d skip_updates=%d", static_cast(it.register_type), it.start_address, it.register_count, it.skip_updates); } + ESP_LOGCONFIG(TAG, "server registers"); + for (auto &r : server_registers_) { + ESP_LOGCONFIG(TAG, " Address=0x%02X value_type=%zu register_count=%u", r->address, + static_cast(r->value_type), r->register_count); + } #endif } diff --git a/esphome/components/modbus_controller/modbus_controller.h b/esphome/components/modbus_controller/modbus_controller.h index a389375523..9b7d59c93f 100644 --- a/esphome/components/modbus_controller/modbus_controller.h +++ b/esphome/components/modbus_controller/modbus_controller.h @@ -8,6 +8,7 @@ #include #include #include +#include #include namespace esphome { @@ -251,6 +252,21 @@ class SensorItem { bool force_new_range{false}; }; +class ServerRegister { + public: + ServerRegister(uint16_t address, SensorValueType value_type, uint8_t register_count, + std::function read_lambda) { + this->address = address; + this->value_type = value_type; + this->register_count = register_count; + this->read_lambda = std::move(read_lambda); + } + uint16_t address; + SensorValueType value_type; + uint8_t register_count; + std::function read_lambda; +}; + // ModbusController::create_register_ranges_ tries to optimize register range // for this the sensors must be ordered by register_type, start_address and bitmask class SensorItemsComparator { @@ -418,10 +434,14 @@ class ModbusController : public PollingComponent, public modbus::ModbusDevice { void queue_command(const ModbusCommandItem &command); /// Registers a sensor with the controller. Called by esphomes code generator void add_sensor_item(SensorItem *item) { sensorset_.insert(item); } + /// Registers a server register with the controller. Called by esphomes code generator + void add_server_register(ServerRegister *server_register) { server_registers_.push_back(server_register); } /// called when a modbus response was parsed without errors void on_modbus_data(const std::vector &data) override; /// called when a modbus error response was received void on_modbus_error(uint8_t function_code, uint8_t exception_code) override; + /// called when a modbus request (function code 3 or 4) was parsed without errors + void on_modbus_read_registers(uint8_t function_code, uint16_t start_address, uint16_t number_of_registers) final; /// default delegate called by process_modbus_data when a response has retrieved from the incoming queue void on_register_data(ModbusRegisterType register_type, uint16_t start_address, const std::vector &data); /// default delegate called by process_modbus_data when a response for a write response has retrieved from the @@ -452,6 +472,8 @@ class ModbusController : public PollingComponent, public modbus::ModbusDevice { void dump_sensors_(); /// Collection of all sensors for this component SensorSet sensorset_; + /// Collection of all server registers for this component + std::vector server_registers_; /// Continuous range of modbus registers std::vector register_ranges_; /// Hold the pending requests to be sent diff --git a/tests/components/modbus_controller/test.esp32.yaml b/tests/components/modbus_controller/test.esp32.yaml index c5fe3fd057..3e022b10ab 100644 --- a/tests/components/modbus_controller/test.esp32.yaml +++ b/tests/components/modbus_controller/test.esp32.yaml @@ -1,14 +1,30 @@ uart: - - id: uart_modbus + - id: uart_modbus_client tx_pin: 17 rx_pin: 16 baud_rate: 9600 + - id: uart_modbus_server + tx_pin: 1 + rx_pin: 3 + baud_rate: 9600 modbus: - id: mod_bus1 - flow_control_pin: 15 + - id: mod_bus1 + uart_id: uart_modbus_client + flow_control_pin: 15 + - id: mod_bus2 + uart_id: uart_modbus_server + role: server modbus_controller: - id: modbus_controller1 address: 0x2 modbus_id: mod_bus1 + - id: modbus_controller2 + address: 0x2 + modbus_id: mod_bus2 + server_registers: + - address: 0x0000 + value_type: S_DWORD_R + read_lambda: |- + return 42.3; From 9a6fde21ee2fa4049e8c12760b8a12cf49cf4c94 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Wed, 22 May 2024 14:43:13 -0500 Subject: [PATCH 3/4] Add on_safe_mode trigger (#6790) --- esphome/components/safe_mode/__init__.py | 14 ++++++++++++++ esphome/components/safe_mode/automation.h | 17 +++++++++++++++++ esphome/components/safe_mode/safe_mode.cpp | 2 ++ esphome/components/safe_mode/safe_mode.h | 5 +++++ tests/components/safe_mode/common.yaml | 2 ++ 5 files changed, 40 insertions(+) create mode 100644 esphome/components/safe_mode/automation.h diff --git a/esphome/components/safe_mode/__init__.py b/esphome/components/safe_mode/__init__.py index 7f227d7dd1..92b285e279 100644 --- a/esphome/components/safe_mode/__init__.py +++ b/esphome/components/safe_mode/__init__.py @@ -7,15 +7,20 @@ from esphome.const import ( CONF_NUM_ATTEMPTS, CONF_REBOOT_TIMEOUT, CONF_SAFE_MODE, + CONF_TRIGGER_ID, KEY_PAST_SAFE_MODE, ) from esphome.core import CORE, coroutine_with_priority +from esphome import automation CODEOWNERS = ["@paulmonigatti", "@jsuanet", "@kbx81"] +CONF_ON_SAFE_MODE = "on_safe_mode" + safe_mode_ns = cg.esphome_ns.namespace("safe_mode") SafeModeComponent = safe_mode_ns.class_("SafeModeComponent", cg.Component) +SafeModeTrigger = safe_mode_ns.class_("SafeModeTrigger", automation.Trigger.template()) def _remove_id_if_disabled(value): @@ -34,6 +39,11 @@ CONFIG_SCHEMA = cv.All( cv.Optional( CONF_REBOOT_TIMEOUT, default="5min" ): cv.positive_time_period_milliseconds, + cv.Optional(CONF_ON_SAFE_MODE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SafeModeTrigger), + } + ), } ).extend(cv.COMPONENT_SCHEMA), _remove_id_if_disabled, @@ -48,6 +58,10 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) + for conf in config.get(CONF_ON_SAFE_MODE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + condition = var.should_enter_safe_mode( config[CONF_NUM_ATTEMPTS], config[CONF_REBOOT_TIMEOUT] ) diff --git a/esphome/components/safe_mode/automation.h b/esphome/components/safe_mode/automation.h new file mode 100644 index 0000000000..d1388449ee --- /dev/null +++ b/esphome/components/safe_mode/automation.h @@ -0,0 +1,17 @@ +#pragma once +#include "safe_mode.h" + +#include "esphome/core/automation.h" + +namespace esphome { +namespace safe_mode { + +class SafeModeTrigger : public Trigger<> { + public: + explicit SafeModeTrigger(SafeModeComponent *parent) { + parent->add_on_safe_mode_callback([this, parent]() { trigger(); }); + } +}; + +} // namespace safe_mode +} // namespace esphome diff --git a/esphome/components/safe_mode/safe_mode.cpp b/esphome/components/safe_mode/safe_mode.cpp index 06772ae1e0..6934dcb9d9 100644 --- a/esphome/components/safe_mode/safe_mode.cpp +++ b/esphome/components/safe_mode/safe_mode.cpp @@ -94,6 +94,8 @@ bool SafeModeComponent::should_enter_safe_mode(uint8_t num_attempts, uint32_t en ESP_LOGW(TAG, "SAFE MODE IS ACTIVE"); + this->safe_mode_callback_.call(); + return true; } else { // increment counter diff --git a/esphome/components/safe_mode/safe_mode.h b/esphome/components/safe_mode/safe_mode.h index 6c7450a6ff..0ec3c29529 100644 --- a/esphome/components/safe_mode/safe_mode.h +++ b/esphome/components/safe_mode/safe_mode.h @@ -25,6 +25,10 @@ class SafeModeComponent : public Component { void on_safe_shutdown() override; + void add_on_safe_mode_callback(std::function &&callback) { + this->safe_mode_callback_.add(std::move(callback)); + } + protected: void write_rtc_(uint32_t val); uint32_t read_rtc_(); @@ -35,6 +39,7 @@ class SafeModeComponent : public Component { uint32_t safe_mode_rtc_value_; uint8_t safe_mode_num_attempts_; ESPPreferenceObject rtc_; + CallbackManager safe_mode_callback_{}; static const uint32_t ENTER_SAFE_MODE_MAGIC = 0x5afe5afe; ///< a magic number to indicate that safe mode should be entered on next boot diff --git a/tests/components/safe_mode/common.yaml b/tests/components/safe_mode/common.yaml index 9c1d1ad3f9..ce8bf2f0cf 100644 --- a/tests/components/safe_mode/common.yaml +++ b/tests/components/safe_mode/common.yaml @@ -5,6 +5,8 @@ wifi: safe_mode: num_attempts: 3 reboot_timeout: 2min + on_safe_mode: + - logger.log: Time for safe mode button: - platform: safe_mode From 7f9383c83b1a4758b3950bf91333222d4ba40ca3 Mon Sep 17 00:00:00 2001 From: Penny Wood Date: Thu, 23 May 2024 05:31:56 +0800 Subject: [PATCH 4/4] [sx1509] Output open drain pin mode (#6788) --- esphome/components/sx1509/__init__.py | 4 ++ esphome/components/sx1509/sx1509.cpp | 56 ++++++++++++++----- .../components/sx1509/test.esp32-c3-idf.yaml | 18 ++++++ tests/components/sx1509/test.esp32-c3.yaml | 18 ++++++ tests/components/sx1509/test.esp32-idf.yaml | 18 ++++++ tests/components/sx1509/test.esp32.yaml | 18 ++++++ tests/components/sx1509/test.esp8266.yaml | 18 ++++++ tests/components/sx1509/test.rp2040.yaml | 18 ++++++ 8 files changed, 155 insertions(+), 13 deletions(-) diff --git a/esphome/components/sx1509/__init__.py b/esphome/components/sx1509/__init__.py index faef940125..e4f79dc2bc 100644 --- a/esphome/components/sx1509/__init__.py +++ b/esphome/components/sx1509/__init__.py @@ -11,6 +11,7 @@ from esphome.const import ( CONF_OUTPUT, CONF_PULLDOWN, CONF_PULLUP, + CONF_OPEN_DRAIN, ) CONF_KEYPAD = "keypad" @@ -79,6 +80,8 @@ def validate_mode(value): raise cv.Invalid("Pulldown only available with input") if value[CONF_PULLUP] and value[CONF_PULLDOWN]: raise cv.Invalid("Can only have one of pullup or pulldown") + if value[CONF_OPEN_DRAIN] and not value[CONF_OUTPUT]: + raise cv.Invalid("Open drain available only with output") return value @@ -94,6 +97,7 @@ SX1509_PIN_SCHEMA = cv.All( cv.Optional(CONF_PULLUP, default=False): cv.boolean, cv.Optional(CONF_PULLDOWN, default=False): cv.boolean, cv.Optional(CONF_OUTPUT, default=False): cv.boolean, + cv.Optional(CONF_OPEN_DRAIN, default=False): cv.boolean, }, validate_mode, ), diff --git a/esphome/components/sx1509/sx1509.cpp b/esphome/components/sx1509/sx1509.cpp index ee90e0e410..855a90bacd 100644 --- a/esphome/components/sx1509/sx1509.cpp +++ b/esphome/components/sx1509/sx1509.cpp @@ -86,33 +86,63 @@ void SX1509Component::digital_write(uint8_t pin, bool bit_value) { } void SX1509Component::pin_mode(uint8_t pin, gpio::Flags flags) { + ESP_LOGI(TAG, "Configuring pin %u with flags %x", pin, flags); + + uint16_t temp_word = 0; + this->read_byte_16(REG_DIR_B, &this->ddr_mask_); - if (flags == gpio::FLAG_OUTPUT) { + if (flags & gpio::FLAG_OUTPUT) { + // Always disable input buffer + this->read_byte_16(REG_INPUT_DISABLE_B, &temp_word); + temp_word |= (1 << pin); + this->write_byte_16(REG_INPUT_DISABLE_B, temp_word); + + if (flags & gpio::FLAG_OPEN_DRAIN) { + // Pullup must be disabled for open drain mode + this->read_byte_16(REG_PULL_UP_B, &temp_word); + temp_word &= ~(1 << pin); + this->write_byte_16(REG_PULL_UP_B, temp_word); + this->read_byte_16(REG_OPEN_DRAIN_B, &temp_word); + temp_word |= (1 << pin); + this->write_byte_16(REG_OPEN_DRAIN_B, temp_word); + ESP_LOGD(TAG, "Open drain output mode set for %u", pin); + } else { + ESP_LOGD(TAG, "Output Mode for %u", pin); + } + + // Set direction to output this->ddr_mask_ &= ~(1 << pin); + this->write_byte_16(REG_DIR_B, this->ddr_mask_); } else { - this->ddr_mask_ |= (1 << pin); + ESP_LOGD(TAG, "Input Mode for %u", pin); - uint16_t temp_pullup; - this->read_byte_16(REG_PULL_UP_B, &temp_pullup); - uint16_t temp_pulldown; - this->read_byte_16(REG_PULL_DOWN_B, &temp_pulldown); + // Always enable input buffer + this->read_byte_16(REG_INPUT_DISABLE_B, &temp_word); + temp_word &= ~(1 << pin); + this->write_byte_16(REG_INPUT_DISABLE_B, temp_word); + // Pullup + this->read_byte_16(REG_PULL_UP_B, &temp_word); if (flags & gpio::FLAG_PULLUP) { - temp_pullup |= (1 << pin); + temp_word |= (1 << pin); } else { - temp_pullup &= ~(1 << pin); + temp_word &= ~(1 << pin); } + this->write_byte_16(REG_PULL_UP_B, temp_word); + // Pulldown + this->read_byte_16(REG_PULL_DOWN_B, &temp_word); if (flags & gpio::FLAG_PULLDOWN) { - temp_pulldown |= (1 << pin); + temp_word |= (1 << pin); } else { - temp_pulldown &= ~(1 << pin); + temp_word &= ~(1 << pin); } + this->write_byte_16(REG_PULL_DOWN_B, temp_word); - this->write_byte_16(REG_PULL_UP_B, temp_pullup); - this->write_byte_16(REG_PULL_DOWN_B, temp_pulldown); + // Set direction to input + this->ddr_mask_ |= (1 << pin); + this->write_byte_16(REG_DIR_B, this->ddr_mask_); } - this->write_byte_16(REG_DIR_B, this->ddr_mask_); } void SX1509Component::setup_led_driver(uint8_t pin) { diff --git a/tests/components/sx1509/test.esp32-c3-idf.yaml b/tests/components/sx1509/test.esp32-c3-idf.yaml index ced849b3df..0397812880 100644 --- a/tests/components/sx1509/test.esp32-c3-idf.yaml +++ b/tests/components/sx1509/test.esp32-c3-idf.yaml @@ -13,3 +13,21 @@ binary_sensor: pin: sx1509: sx1509_hub number: 3 + +switch: + - platform: gpio + name: GPIO SX1509 Test Out Open Drain + pin: + sx1509: sx1509_hub + number: 0 + mode: + output: true + open_drain: true + + - platform: gpio + name: GPIO SX1509 Test Out Standard + pin: + sx1509: sx1509_hub + number: 1 + mode: + output: true diff --git a/tests/components/sx1509/test.esp32-c3.yaml b/tests/components/sx1509/test.esp32-c3.yaml index ced849b3df..0397812880 100644 --- a/tests/components/sx1509/test.esp32-c3.yaml +++ b/tests/components/sx1509/test.esp32-c3.yaml @@ -13,3 +13,21 @@ binary_sensor: pin: sx1509: sx1509_hub number: 3 + +switch: + - platform: gpio + name: GPIO SX1509 Test Out Open Drain + pin: + sx1509: sx1509_hub + number: 0 + mode: + output: true + open_drain: true + + - platform: gpio + name: GPIO SX1509 Test Out Standard + pin: + sx1509: sx1509_hub + number: 1 + mode: + output: true diff --git a/tests/components/sx1509/test.esp32-idf.yaml b/tests/components/sx1509/test.esp32-idf.yaml index 1698f2abc4..aa1d161a43 100644 --- a/tests/components/sx1509/test.esp32-idf.yaml +++ b/tests/components/sx1509/test.esp32-idf.yaml @@ -13,3 +13,21 @@ binary_sensor: pin: sx1509: sx1509_hub number: 3 + +switch: + - platform: gpio + name: GPIO SX1509 Test Out Open Drain + pin: + sx1509: sx1509_hub + number: 0 + mode: + output: true + open_drain: true + + - platform: gpio + name: GPIO SX1509 Test Out Standard + pin: + sx1509: sx1509_hub + number: 1 + mode: + output: true diff --git a/tests/components/sx1509/test.esp32.yaml b/tests/components/sx1509/test.esp32.yaml index 1698f2abc4..aa1d161a43 100644 --- a/tests/components/sx1509/test.esp32.yaml +++ b/tests/components/sx1509/test.esp32.yaml @@ -13,3 +13,21 @@ binary_sensor: pin: sx1509: sx1509_hub number: 3 + +switch: + - platform: gpio + name: GPIO SX1509 Test Out Open Drain + pin: + sx1509: sx1509_hub + number: 0 + mode: + output: true + open_drain: true + + - platform: gpio + name: GPIO SX1509 Test Out Standard + pin: + sx1509: sx1509_hub + number: 1 + mode: + output: true diff --git a/tests/components/sx1509/test.esp8266.yaml b/tests/components/sx1509/test.esp8266.yaml index ced849b3df..0397812880 100644 --- a/tests/components/sx1509/test.esp8266.yaml +++ b/tests/components/sx1509/test.esp8266.yaml @@ -13,3 +13,21 @@ binary_sensor: pin: sx1509: sx1509_hub number: 3 + +switch: + - platform: gpio + name: GPIO SX1509 Test Out Open Drain + pin: + sx1509: sx1509_hub + number: 0 + mode: + output: true + open_drain: true + + - platform: gpio + name: GPIO SX1509 Test Out Standard + pin: + sx1509: sx1509_hub + number: 1 + mode: + output: true diff --git a/tests/components/sx1509/test.rp2040.yaml b/tests/components/sx1509/test.rp2040.yaml index ced849b3df..0397812880 100644 --- a/tests/components/sx1509/test.rp2040.yaml +++ b/tests/components/sx1509/test.rp2040.yaml @@ -13,3 +13,21 @@ binary_sensor: pin: sx1509: sx1509_hub number: 3 + +switch: + - platform: gpio + name: GPIO SX1509 Test Out Open Drain + pin: + sx1509: sx1509_hub + number: 0 + mode: + output: true + open_drain: true + + - platform: gpio + name: GPIO SX1509 Test Out Standard + pin: + sx1509: sx1509_hub + number: 1 + mode: + output: true