mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-30 22:53:59 +00:00 
			
		
		
		
	Uncouple safe_mode from OTA (#6759)
This commit is contained in:
		| @@ -306,7 +306,7 @@ esphome/components/rp2040_pwm/* @jesserockz | |||||||
| esphome/components/rpi_dpi_rgb/* @clydebarrow | esphome/components/rpi_dpi_rgb/* @clydebarrow | ||||||
| esphome/components/rtl87xx/* @kuba2k2 | esphome/components/rtl87xx/* @kuba2k2 | ||||||
| esphome/components/rtttl/* @glmnet | esphome/components/rtttl/* @glmnet | ||||||
| esphome/components/safe_mode/* @jsuanet @paulmonigatti | esphome/components/safe_mode/* @jsuanet @kbx81 @paulmonigatti | ||||||
| esphome/components/scd4x/* @martgras @sjtrny | esphome/components/scd4x/* @martgras @sjtrny | ||||||
| esphome/components/script/* @esphome/core | esphome/components/script/* @esphome/core | ||||||
| esphome/components/sdm_meter/* @jesserockz @polyfaces | esphome/components/sdm_meter/* @jesserockz @polyfaces | ||||||
|   | |||||||
| @@ -1,19 +1,16 @@ | |||||||
| from esphome.cpp_generator import RawExpression |  | ||||||
| import esphome.codegen as cg | import esphome.codegen as cg | ||||||
| import esphome.config_validation as cv | import esphome.config_validation as cv | ||||||
| from esphome.components.ota import BASE_OTA_SCHEMA, ota_to_code, OTAComponent | from esphome.components.ota import BASE_OTA_SCHEMA, ota_to_code, OTAComponent | ||||||
| from esphome.const import ( | from esphome.const import ( | ||||||
|     CONF_ID, |     CONF_ID, | ||||||
|     CONF_NUM_ATTEMPTS, |     CONF_NUM_ATTEMPTS, | ||||||
|     CONF_OTA, |  | ||||||
|     CONF_PASSWORD, |     CONF_PASSWORD, | ||||||
|     CONF_PORT, |     CONF_PORT, | ||||||
|     CONF_REBOOT_TIMEOUT, |     CONF_REBOOT_TIMEOUT, | ||||||
|     CONF_SAFE_MODE, |     CONF_SAFE_MODE, | ||||||
|     CONF_VERSION, |     CONF_VERSION, | ||||||
|     KEY_PAST_SAFE_MODE, |  | ||||||
| ) | ) | ||||||
| from esphome.core import CORE, coroutine_with_priority | from esphome.core import coroutine_with_priority | ||||||
|  |  | ||||||
|  |  | ||||||
| CODEOWNERS = ["@esphome/core"] | CODEOWNERS = ["@esphome/core"] | ||||||
| @@ -28,7 +25,6 @@ CONFIG_SCHEMA = ( | |||||||
|     cv.Schema( |     cv.Schema( | ||||||
|         { |         { | ||||||
|             cv.GenerateID(): cv.declare_id(ESPHomeOTAComponent), |             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.Optional(CONF_VERSION, default=2): cv.one_of(1, 2, int=True), | ||||||
|             cv.SplitDefault( |             cv.SplitDefault( | ||||||
|                 CONF_PORT, |                 CONF_PORT, | ||||||
| @@ -39,10 +35,15 @@ CONFIG_SCHEMA = ( | |||||||
|                 rtl87xx=8892, |                 rtl87xx=8892, | ||||||
|             ): cv.port, |             ): cv.port, | ||||||
|             cv.Optional(CONF_PASSWORD): cv.string, |             cv.Optional(CONF_PASSWORD): cv.string, | ||||||
|             cv.Optional( |             cv.Optional(CONF_NUM_ATTEMPTS): cv.invalid( | ||||||
|                 CONF_REBOOT_TIMEOUT, default="5min" |                 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.positive_time_period_milliseconds, |             ), | ||||||
|             cv.Optional(CONF_NUM_ATTEMPTS, default="10"): cv.positive_not_null_int, |             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) |     .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): | async def to_code(config): | ||||||
|     CORE.data[CONF_OTA] = {} |  | ||||||
|  |  | ||||||
|     var = cg.new_Pvariable(config[CONF_ID]) |     var = cg.new_Pvariable(config[CONF_ID]) | ||||||
|     await ota_to_code(var, config) |     await ota_to_code(var, config) | ||||||
|     cg.add(var.set_port(config[CONF_PORT])) |     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]) |     cg.add_define("USE_OTA_VERSION", config[CONF_VERSION]) | ||||||
|  |  | ||||||
|     await cg.register_component(var, config) |     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 |  | ||||||
|   | |||||||
| @@ -78,23 +78,9 @@ void ESPHomeOTAComponent::dump_config() { | |||||||
|     ESP_LOGCONFIG(TAG, "  Password configured"); |     ESP_LOGCONFIG(TAG, "  Password configured"); | ||||||
|   } |   } | ||||||
| #endif | #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() { | void ESPHomeOTAComponent::loop() { this->handle_(); } | ||||||
|   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(); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| static const uint8_t FEATURE_SUPPORTS_COMPRESSION = 0x01; | 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; } | float ESPHomeOTAComponent::get_setup_priority() const { return setup_priority::AFTER_WIFI; } | ||||||
| uint16_t ESPHomeOTAComponent::get_port() const { return this->port_; } | uint16_t ESPHomeOTAComponent::get_port() const { return this->port_; } | ||||||
| void ESPHomeOTAComponent::set_port(uint16_t port) { this->port_ = 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<uint32_t>(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 | }  // namespace esphome | ||||||
|   | |||||||
| @@ -15,17 +15,9 @@ class ESPHomeOTAComponent : public ota::OTAComponent { | |||||||
|   void set_auth_password(const std::string &password) { password_ = password; } |   void set_auth_password(const std::string &password) { password_ = password; } | ||||||
| #endif  // USE_OTA_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); |   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 setup() override; | ||||||
|   void dump_config() override; |   void dump_config() override; | ||||||
|   float get_setup_priority() const override; |   float get_setup_priority() const override; | ||||||
| @@ -33,14 +25,7 @@ class ESPHomeOTAComponent : public ota::OTAComponent { | |||||||
|  |  | ||||||
|   uint16_t get_port() const; |   uint16_t get_port() const; | ||||||
|  |  | ||||||
|   void clean_rtc(); |  | ||||||
|  |  | ||||||
|   void on_safe_shutdown() override; |  | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|   void write_rtc_(uint32_t val); |  | ||||||
|   uint32_t read_rtc_(); |  | ||||||
|  |  | ||||||
|   void handle_(); |   void handle_(); | ||||||
|   bool readall_(uint8_t *buf, size_t len); |   bool readall_(uint8_t *buf, size_t len); | ||||||
|   bool writeall_(const 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<socket::Socket> server_; |   std::unique_ptr<socket::Socket> server_; | ||||||
|   std::unique_ptr<socket::Socket> client_; |   std::unique_ptr<socket::Socket> 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 | }  // namespace esphome | ||||||
|   | |||||||
| @@ -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 | from esphome.const import CONF_ESPHOME, CONF_OTA, CONF_PLATFORM, CONF_TRIGGER_ID | ||||||
|  |  | ||||||
| CODEOWNERS = ["@esphome/core"] | CODEOWNERS = ["@esphome/core"] | ||||||
| AUTO_LOAD = ["md5"] | AUTO_LOAD = ["md5", "safe_mode"] | ||||||
|  |  | ||||||
| IS_PLATFORM_COMPONENT = True | 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): | async def to_code(config): | ||||||
|     cg.add_define("USE_OTA") |     cg.add_define("USE_OTA") | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,5 +1,56 @@ | |||||||
|  | from esphome.cpp_generator import RawExpression | ||||||
| import esphome.codegen as cg | 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") | 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 | ||||||
|   | |||||||
| @@ -1,16 +1,15 @@ | |||||||
| import esphome.codegen as cg | import esphome.codegen as cg | ||||||
| import esphome.config_validation as cv | import esphome.config_validation as cv | ||||||
| from esphome.components import button | from esphome.components import button | ||||||
| from esphome.components.esphome.ota import ESPHomeOTAComponent |  | ||||||
| from esphome.const import ( | from esphome.const import ( | ||||||
|     CONF_ESPHOME, |     CONF_SAFE_MODE, | ||||||
|     DEVICE_CLASS_RESTART, |     DEVICE_CLASS_RESTART, | ||||||
|     ENTITY_CATEGORY_CONFIG, |     ENTITY_CATEGORY_CONFIG, | ||||||
|     ICON_RESTART_ALERT, |     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) | SafeModeButton = safe_mode_ns.class_("SafeModeButton", button.Button, cg.Component) | ||||||
|  |  | ||||||
| @@ -21,7 +20,7 @@ CONFIG_SCHEMA = ( | |||||||
|         entity_category=ENTITY_CATEGORY_CONFIG, |         entity_category=ENTITY_CATEGORY_CONFIG, | ||||||
|         icon=ICON_RESTART_ALERT, |         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) |     .extend(cv.COMPONENT_SCHEMA) | ||||||
| ) | ) | ||||||
|  |  | ||||||
| @@ -30,5 +29,5 @@ async def to_code(config): | |||||||
|     var = await button.new_button(config) |     var = await button.new_button(config) | ||||||
|     await cg.register_component(var, config) |     await cg.register_component(var, config) | ||||||
|  |  | ||||||
|     ota = await cg.get_variable(config[CONF_ESPHOME]) |     safe_mode_component = await cg.get_variable(config[CONF_SAFE_MODE]) | ||||||
|     cg.add(var.set_ota(ota)) |     cg.add(var.set_safe_mode(safe_mode_component)) | ||||||
|   | |||||||
| @@ -8,11 +8,13 @@ namespace safe_mode { | |||||||
|  |  | ||||||
| static const char *const TAG = "safe_mode.button"; | 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() { | void SafeModeButton::press_action() { | ||||||
|   ESP_LOGI(TAG, "Restarting device in safe mode..."); |   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 |   // Let MQTT settle a bit | ||||||
|   delay(100);  // NOLINT |   delay(100);  // NOLINT | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| #pragma once | #pragma once | ||||||
|  |  | ||||||
| #include "esphome/components/button/button.h" | #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" | #include "esphome/core/component.h" | ||||||
|  |  | ||||||
| namespace esphome { | namespace esphome { | ||||||
| @@ -10,10 +10,10 @@ namespace safe_mode { | |||||||
| class SafeModeButton : public button::Button, public Component { | class SafeModeButton : public button::Button, public Component { | ||||||
|  public: |  public: | ||||||
|   void dump_config() override; |   void dump_config() override; | ||||||
|   void set_ota(esphome::ESPHomeOTAComponent *ota); |   void set_safe_mode(SafeModeComponent *safe_mode_component); | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|   esphome::ESPHomeOTAComponent *ota_; |   SafeModeComponent *safe_mode_component_; | ||||||
|   void press_action() override; |   void press_action() override; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										125
									
								
								esphome/components/safe_mode/safe_mode.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										125
									
								
								esphome/components/safe_mode/safe_mode.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -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 <cerrno> | ||||||
|  | #include <cinttypes> | ||||||
|  | #include <cstdio> | ||||||
|  |  | ||||||
|  | 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<uint32_t>(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 | ||||||
							
								
								
									
										44
									
								
								esphome/components/safe_mode/safe_mode.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								esphome/components/safe_mode/safe_mode.h
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||||
| @@ -1,15 +1,14 @@ | |||||||
| import esphome.codegen as cg | import esphome.codegen as cg | ||||||
| import esphome.config_validation as cv | import esphome.config_validation as cv | ||||||
| from esphome.components import switch | from esphome.components import switch | ||||||
| from esphome.components.esphome.ota import ESPHomeOTAComponent |  | ||||||
| from esphome.const import ( | from esphome.const import ( | ||||||
|     CONF_ESPHOME, |     CONF_SAFE_MODE, | ||||||
|     ENTITY_CATEGORY_CONFIG, |     ENTITY_CATEGORY_CONFIG, | ||||||
|     ICON_RESTART_ALERT, |     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) | SafeModeSwitch = safe_mode_ns.class_("SafeModeSwitch", switch.Switch, cg.Component) | ||||||
|  |  | ||||||
| @@ -20,7 +19,7 @@ CONFIG_SCHEMA = ( | |||||||
|         entity_category=ENTITY_CATEGORY_CONFIG, |         entity_category=ENTITY_CATEGORY_CONFIG, | ||||||
|         icon=ICON_RESTART_ALERT, |         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) |     .extend(cv.COMPONENT_SCHEMA) | ||||||
| ) | ) | ||||||
|  |  | ||||||
| @@ -29,5 +28,5 @@ async def to_code(config): | |||||||
|     var = await switch.new_switch(config) |     var = await switch.new_switch(config) | ||||||
|     await cg.register_component(var, config) |     await cg.register_component(var, config) | ||||||
|  |  | ||||||
|     ota = await cg.get_variable(config[CONF_ESPHOME]) |     safe_mode_component = await cg.get_variable(config[CONF_SAFE_MODE]) | ||||||
|     cg.add(var.set_ota(ota)) |     cg.add(var.set_safe_mode(safe_mode_component)) | ||||||
|   | |||||||
| @@ -6,9 +6,11 @@ | |||||||
| namespace esphome { | namespace esphome { | ||||||
| namespace safe_mode { | 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) { | void SafeModeSwitch::write_state(bool state) { | ||||||
|   // Acknowledge |   // Acknowledge | ||||||
| @@ -16,13 +18,14 @@ void SafeModeSwitch::write_state(bool state) { | |||||||
|  |  | ||||||
|   if (state) { |   if (state) { | ||||||
|     ESP_LOGI(TAG, "Restarting device in safe mode..."); |     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 |     // Let MQTT settle a bit | ||||||
|     delay(100);  // NOLINT |     delay(100);  // NOLINT | ||||||
|     App.safe_reboot(); |     App.safe_reboot(); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| void SafeModeSwitch::dump_config() { LOG_SWITCH("", "Safe Mode Switch", this); } | void SafeModeSwitch::dump_config() { LOG_SWITCH("", "Safe Mode Switch", this); } | ||||||
|  |  | ||||||
| }  // namespace safe_mode | }  // namespace safe_mode | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| #pragma once | #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/components/switch/switch.h" | ||||||
| #include "esphome/core/component.h" | #include "esphome/core/component.h" | ||||||
|  |  | ||||||
| @@ -10,10 +10,10 @@ namespace safe_mode { | |||||||
| class SafeModeSwitch : public switch_::Switch, public Component { | class SafeModeSwitch : public switch_::Switch, public Component { | ||||||
|  public: |  public: | ||||||
|   void dump_config() override; |   void dump_config() override; | ||||||
|   void set_ota(esphome::ESPHomeOTAComponent *ota); |   void set_safe_mode(SafeModeComponent *safe_mode_component); | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|   esphome::ESPHomeOTAComponent *ota_; |   SafeModeComponent *safe_mode_component_; | ||||||
|   void write_state(bool state) override; |   void write_state(bool state) override; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -3,12 +3,9 @@ import logging | |||||||
| from esphome.const import ( | from esphome.const import ( | ||||||
|     CONF_DISABLED_BY_DEFAULT, |     CONF_DISABLED_BY_DEFAULT, | ||||||
|     CONF_ENTITY_CATEGORY, |     CONF_ENTITY_CATEGORY, | ||||||
|     CONF_ESPHOME, |  | ||||||
|     CONF_ICON, |     CONF_ICON, | ||||||
|     CONF_INTERNAL, |     CONF_INTERNAL, | ||||||
|     CONF_NAME, |     CONF_NAME, | ||||||
|     CONF_OTA, |  | ||||||
|     CONF_PLATFORM, |  | ||||||
|     CONF_SAFE_MODE, |     CONF_SAFE_MODE, | ||||||
|     CONF_SETUP_PRIORITY, |     CONF_SETUP_PRIORITY, | ||||||
|     CONF_TYPE_ID, |     CONF_TYPE_ID, | ||||||
| @@ -141,22 +138,12 @@ async def build_registry_list(registry, config): | |||||||
|  |  | ||||||
|  |  | ||||||
| async def past_safe_mode(): | async def past_safe_mode(): | ||||||
|     ota_conf = {} |     if CONF_SAFE_MODE not in CORE.config: | ||||||
|     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: |  | ||||||
|         return |         return | ||||||
|  |  | ||||||
|     def _safe_mode_generator(): |     def _safe_mode_generator(): | ||||||
|         while True: |         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 |                 return | ||||||
|             yield |             yield | ||||||
|  |  | ||||||
|   | |||||||
| @@ -4,11 +4,8 @@ wifi: | |||||||
|  |  | ||||||
| ota: | ota: | ||||||
|   - platform: esphome |   - platform: esphome | ||||||
|     safe_mode: true |  | ||||||
|     password: "superlongpasswordthatnoonewillknow" |     password: "superlongpasswordthatnoonewillknow" | ||||||
|     port: 3286 |     port: 3286 | ||||||
|     reboot_timeout: 2min |  | ||||||
|     num_attempts: 5 |  | ||||||
|     on_begin: |     on_begin: | ||||||
|       then: |       then: | ||||||
|         - logger.log: "OTA start" |         - logger.log: "OTA start" | ||||||
|   | |||||||
| @@ -2,9 +2,9 @@ wifi: | |||||||
|   ssid: MySSID |   ssid: MySSID | ||||||
|   password: password1 |   password: password1 | ||||||
|  |  | ||||||
| ota: | safe_mode: | ||||||
|   - platform: esphome |   num_attempts: 3 | ||||||
|     safe_mode: true |   reboot_timeout: 2min | ||||||
|  |  | ||||||
| button: | button: | ||||||
|   - platform: safe_mode |   - platform: safe_mode | ||||||
|   | |||||||
| @@ -264,13 +264,14 @@ uart: | |||||||
|     parity: EVEN |     parity: EVEN | ||||||
|     baud_rate: 9600 |     baud_rate: 9600 | ||||||
|  |  | ||||||
|  | safe_mode: | ||||||
|  |   num_attempts: 3 | ||||||
|  |   reboot_timeout: 2min | ||||||
|  |  | ||||||
| ota: | ota: | ||||||
|   - platform: esphome |   - platform: esphome | ||||||
|     safe_mode: true |  | ||||||
|     password: "superlongpasswordthatnoonewillknow" |     password: "superlongpasswordthatnoonewillknow" | ||||||
|     port: 3286 |     port: 3286 | ||||||
|     reboot_timeout: 2min |  | ||||||
|     num_attempts: 5 |  | ||||||
|     on_state_change: |     on_state_change: | ||||||
|       then: |       then: | ||||||
|         lambda: >- |         lambda: >- | ||||||
|   | |||||||
| @@ -79,11 +79,11 @@ uart: | |||||||
|     sequence: |     sequence: | ||||||
|       - lambda: UARTDebug::log_hex(direction, bytes, ':'); |       - lambda: UARTDebug::log_hex(direction, bytes, ':'); | ||||||
|  |  | ||||||
|  | safe_mode: | ||||||
|  |  | ||||||
| ota: | ota: | ||||||
|   - platform: esphome |   - platform: esphome | ||||||
|     safe_mode: true |  | ||||||
|     port: 3286 |     port: 3286 | ||||||
|     num_attempts: 15 |  | ||||||
|  |  | ||||||
| logger: | logger: | ||||||
|   level: DEBUG |   level: DEBUG | ||||||
|   | |||||||
| @@ -327,11 +327,13 @@ modbus: | |||||||
| vbus: | vbus: | ||||||
|   uart_id: uart_4 |   uart_id: uart_4 | ||||||
|  |  | ||||||
|  | safe_mode: | ||||||
|  |   num_attempts: 5 | ||||||
|  |   reboot_timeout: 10min | ||||||
|  |  | ||||||
| ota: | ota: | ||||||
|   - platform: esphome |   - platform: esphome | ||||||
|     safe_mode: true |  | ||||||
|     port: 3286 |     port: 3286 | ||||||
|     reboot_timeout: 15min |  | ||||||
|  |  | ||||||
| logger: | logger: | ||||||
|   hardware_uart: UART1 |   hardware_uart: UART1 | ||||||
|   | |||||||
| @@ -102,9 +102,10 @@ uart: | |||||||
|     baud_rate: 1200 |     baud_rate: 1200 | ||||||
|     parity: EVEN |     parity: EVEN | ||||||
|  |  | ||||||
|  | safe_mode: | ||||||
|  |  | ||||||
| ota: | ota: | ||||||
|   - platform: esphome |   - platform: esphome | ||||||
|     safe_mode: true |  | ||||||
|     port: 3286 |     port: 3286 | ||||||
|  |  | ||||||
| logger: | logger: | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user