mirror of
https://github.com/esphome/esphome.git
synced 2025-07-29 14:46:40 +00:00
[factory_reset] Allow factory reset by rapid power cycle (#9749)
This commit is contained in:
parent
ba1de5feff
commit
ba72298a63
@ -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)
|
@ -1,3 +1,7 @@
|
||||
button:
|
||||
- platform: factory_reset
|
||||
name: Reset to Factory Default Settings
|
||||
|
||||
factory_reset:
|
||||
resets_required: 5
|
||||
max_delay: 10s
|
||||
|
1
tests/components/factory_reset/test.bk72xx-ard.yaml
Normal file
1
tests/components/factory_reset/test.bk72xx-ard.yaml
Normal file
@ -0,0 +1 @@
|
||||
<<: !include common.yaml
|
@ -1 +1,4 @@
|
||||
esp8266:
|
||||
restore_from_flash: true
|
||||
|
||||
<<: !include common.yaml
|
||||
|
@ -1 +1,3 @@
|
||||
<<: !include common.yaml
|
||||
button:
|
||||
- platform: factory_reset
|
||||
name: Reset to Factory Default Settings
|
||||
|
Loading…
x
Reference in New Issue
Block a user