mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 07:03:55 +00:00 
			
		
		
		
	[factory_reset] Allow factory reset by rapid power cycle (#9749)
This commit is contained in:
		| @@ -1,5 +1,97 @@ | ||||
| from esphome.automation import Trigger, build_automation, validate_automation | ||||
| import esphome.codegen as cg | ||||
| from esphome.components.esp8266 import CONF_RESTORE_FROM_FLASH, KEY_ESP8266 | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
|     CONF_ID, | ||||
|     CONF_TRIGGER_ID, | ||||
|     PLATFORM_BK72XX, | ||||
|     PLATFORM_ESP32, | ||||
|     PLATFORM_ESP8266, | ||||
|     PLATFORM_LN882X, | ||||
|     PLATFORM_RTL87XX, | ||||
| ) | ||||
| from esphome.core import CORE | ||||
| from esphome.final_validate import full_config | ||||
|  | ||||
| CODEOWNERS = ["@anatoly-savchenkov"] | ||||
|  | ||||
| factory_reset_ns = cg.esphome_ns.namespace("factory_reset") | ||||
| FactoryResetComponent = factory_reset_ns.class_("FactoryResetComponent", cg.Component) | ||||
| FastBootTrigger = factory_reset_ns.class_("FastBootTrigger", Trigger, cg.Component) | ||||
|  | ||||
| CONF_MAX_DELAY = "max_delay" | ||||
| CONF_RESETS_REQUIRED = "resets_required" | ||||
| CONF_ON_INCREMENT = "on_increment" | ||||
|  | ||||
|  | ||||
| def _validate(config): | ||||
|     if CONF_RESETS_REQUIRED in config: | ||||
|         return cv.only_on( | ||||
|             [ | ||||
|                 PLATFORM_BK72XX, | ||||
|                 PLATFORM_ESP32, | ||||
|                 PLATFORM_ESP8266, | ||||
|                 PLATFORM_LN882X, | ||||
|                 PLATFORM_RTL87XX, | ||||
|             ] | ||||
|         )(config) | ||||
|  | ||||
|     if CONF_ON_INCREMENT in config: | ||||
|         raise cv.Invalid( | ||||
|             f"'{CONF_ON_INCREMENT}' requires a value for '{CONF_RESETS_REQUIRED}'" | ||||
|         ) | ||||
|     return config | ||||
|  | ||||
|  | ||||
| CONFIG_SCHEMA = cv.All( | ||||
|     cv.Schema( | ||||
|         { | ||||
|             cv.GenerateID(): cv.declare_id(FactoryResetComponent), | ||||
|             cv.Optional(CONF_MAX_DELAY, default="10s"): cv.All( | ||||
|                 cv.positive_time_period_seconds, | ||||
|                 cv.Range(min=cv.TimePeriod(milliseconds=1000)), | ||||
|             ), | ||||
|             cv.Optional(CONF_RESETS_REQUIRED): cv.positive_not_null_int, | ||||
|             cv.Optional(CONF_ON_INCREMENT): validate_automation( | ||||
|                 { | ||||
|                     cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(FastBootTrigger), | ||||
|                 } | ||||
|             ), | ||||
|         } | ||||
|     ).extend(cv.COMPONENT_SCHEMA), | ||||
|     _validate, | ||||
| ) | ||||
|  | ||||
|  | ||||
| def _final_validate(config): | ||||
|     if CORE.is_esp8266 and CONF_RESETS_REQUIRED in config: | ||||
|         fconfig = full_config.get() | ||||
|         if not fconfig.get_config_for_path([KEY_ESP8266, CONF_RESTORE_FROM_FLASH]): | ||||
|             raise cv.Invalid( | ||||
|                 "'resets_required' needs 'restore_from_flash' to be enabled in the  'esp8266' configuration" | ||||
|             ) | ||||
|     return config | ||||
|  | ||||
|  | ||||
| FINAL_VALIDATE_SCHEMA = _final_validate | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     if reset_count := config.get(CONF_RESETS_REQUIRED): | ||||
|         var = cg.new_Pvariable( | ||||
|             config[CONF_ID], | ||||
|             reset_count, | ||||
|             config[CONF_MAX_DELAY].total_milliseconds, | ||||
|         ) | ||||
|         await cg.register_component(var, config) | ||||
|         for conf in config.get(CONF_ON_INCREMENT, []): | ||||
|             trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) | ||||
|             await build_automation( | ||||
|                 trigger, | ||||
|                 [ | ||||
|                     (cg.uint8, "x"), | ||||
|                     (cg.uint8, "target"), | ||||
|                 ], | ||||
|                 conf, | ||||
|             ) | ||||
|   | ||||
							
								
								
									
										76
									
								
								esphome/components/factory_reset/factory_reset.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								esphome/components/factory_reset/factory_reset.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,76 @@ | ||||
| #include "factory_reset.h" | ||||
|  | ||||
| #include "esphome/core/application.h" | ||||
| #include "esphome/core/hal.h" | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| #include <cinttypes> | ||||
|  | ||||
| #if !defined(USE_RP2040) && !defined(USE_HOST) | ||||
|  | ||||
| namespace esphome { | ||||
| namespace factory_reset { | ||||
|  | ||||
| static const char *const TAG = "factory_reset"; | ||||
| static const uint32_t POWER_CYCLES_KEY = 0xFA5C0DE; | ||||
|  | ||||
| static bool was_power_cycled() { | ||||
| #ifdef USE_ESP32 | ||||
|   return esp_reset_reason() == ESP_RST_POWERON; | ||||
| #endif | ||||
| #ifdef USE_ESP8266 | ||||
|   auto reset_reason = EspClass::getResetReason(); | ||||
|   return strcasecmp(reset_reason.c_str(), "power On") == 0 || strcasecmp(reset_reason.c_str(), "external system") == 0; | ||||
| #endif | ||||
| #ifdef USE_LIBRETINY | ||||
|   auto reason = lt_get_reboot_reason(); | ||||
|   return reason == REBOOT_REASON_POWER || reason == REBOOT_REASON_HARDWARE; | ||||
| #endif | ||||
| } | ||||
|  | ||||
| void FactoryResetComponent::dump_config() { | ||||
|   uint8_t count = 0; | ||||
|   this->flash_.load(&count); | ||||
|   ESP_LOGCONFIG(TAG, "Factory Reset by Reset:"); | ||||
|   ESP_LOGCONFIG(TAG, | ||||
|                 "  Max interval between resets %" PRIu32 " seconds\n" | ||||
|                 "  Current count: %u\n" | ||||
|                 "  Factory reset after %u resets", | ||||
|                 this->max_interval_ / 1000, count, this->required_count_); | ||||
| } | ||||
|  | ||||
| void FactoryResetComponent::save_(uint8_t count) { | ||||
|   this->flash_.save(&count); | ||||
|   global_preferences->sync(); | ||||
|   this->defer([count, this] { this->increment_callback_.call(count, this->required_count_); }); | ||||
| } | ||||
|  | ||||
| void FactoryResetComponent::setup() { | ||||
|   this->flash_ = global_preferences->make_preference<uint8_t>(POWER_CYCLES_KEY, true); | ||||
|   if (was_power_cycled()) { | ||||
|     uint8_t count = 0; | ||||
|     this->flash_.load(&count); | ||||
|     // this is a power on reset or external system reset | ||||
|     count++; | ||||
|     if (count == this->required_count_) { | ||||
|       ESP_LOGW(TAG, "Reset count reached, factory resetting"); | ||||
|       global_preferences->reset(); | ||||
|       // delay to allow log to be sent | ||||
|       delay(100);         // NOLINT | ||||
|       App.safe_reboot();  // should not return | ||||
|     } | ||||
|     this->save_(count); | ||||
|     ESP_LOGD(TAG, "Power on reset detected, incremented count to %u", count); | ||||
|     this->set_timeout(this->max_interval_, [this]() { | ||||
|       ESP_LOGD(TAG, "No reset in the last %" PRIu32 " seconds, resetting count", this->max_interval_ / 1000); | ||||
|       this->save_(0);  // reset count | ||||
|     }); | ||||
|   } else { | ||||
|     this->save_(0);  // reset count if not a power cycle | ||||
|   } | ||||
| } | ||||
|  | ||||
| }  // namespace factory_reset | ||||
| }  // namespace esphome | ||||
|  | ||||
| #endif  // !defined(USE_RP2040) && !defined(USE_HOST) | ||||
							
								
								
									
										43
									
								
								esphome/components/factory_reset/factory_reset.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								esphome/components/factory_reset/factory_reset.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,43 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/automation.h" | ||||
| #include "esphome/core/preferences.h" | ||||
| #if !defined(USE_RP2040) && !defined(USE_HOST) | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
| #include <esp_system.h> | ||||
| #endif | ||||
|  | ||||
| namespace esphome { | ||||
| namespace factory_reset { | ||||
| class FactoryResetComponent : public Component { | ||||
|  public: | ||||
|   FactoryResetComponent(uint8_t required_count, uint32_t max_interval) | ||||
|       : required_count_(required_count), max_interval_(max_interval) {} | ||||
|  | ||||
|   void dump_config() override; | ||||
|   void setup() override; | ||||
|   void add_increment_callback(std::function<void(uint8_t, uint8_t)> &&callback) { | ||||
|     this->increment_callback_.add(std::move(callback)); | ||||
|   } | ||||
|  | ||||
|  protected: | ||||
|   ~FactoryResetComponent() = default; | ||||
|   void save_(uint8_t count); | ||||
|   ESPPreferenceObject flash_{};  // saves the number of fast power cycles | ||||
|   uint8_t required_count_;       // The number of boot attempts before fast boot is enabled | ||||
|   uint32_t max_interval_;        // max interval between power cycles | ||||
|   CallbackManager<void(uint8_t, uint8_t)> increment_callback_{}; | ||||
| }; | ||||
|  | ||||
| class FastBootTrigger : public Trigger<uint8_t, uint8_t> { | ||||
|  public: | ||||
|   explicit FastBootTrigger(FactoryResetComponent *parent) { | ||||
|     parent->add_increment_callback([this](uint8_t current, uint8_t target) { this->trigger(current, target); }); | ||||
|   } | ||||
| }; | ||||
| }  // namespace factory_reset | ||||
| }  // namespace esphome | ||||
|  | ||||
| #endif  // !defined(USE_RP2040) && !defined(USE_HOST) | ||||
		Reference in New Issue
	
	Block a user