diff --git a/CODEOWNERS b/CODEOWNERS index e63528fabc..85ac107975 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -77,6 +77,7 @@ esphome/components/esp32_improv/* @jesserockz esphome/components/esp8266/* @esphome/core esphome/components/exposure_notifications/* @OttoWinter esphome/components/ezo/* @ssieb +esphome/components/factory_reset/* @anatoly-savchenkov esphome/components/fastled_base/* @OttoWinter esphome/components/feedback/* @ianchi esphome/components/fingerprint_grow/* @OnFreund @loongyh diff --git a/esphome/components/esp32/preferences.cpp b/esphome/components/esp32/preferences.cpp index d070a20d82..6a6305cf87 100644 --- a/esphome/components/esp32/preferences.cpp +++ b/esphome/components/esp32/preferences.cpp @@ -141,7 +141,7 @@ class ESP32Preferences : public ESPPreferences { ESP_LOGD(TAG, "Saving %d preferences to flash: %d cached, %d written, %d failed", cached + written + failed, cached, written, failed); if (failed > 0) { - ESP_LOGD(TAG, "Error saving %d preferences to flash. Last error=%s for key=%s", failed, esp_err_to_name(last_err), + ESP_LOGE(TAG, "Error saving %d preferences to flash. Last error=%s for key=%s", failed, esp_err_to_name(last_err), last_key.c_str()); } @@ -170,6 +170,17 @@ class ESP32Preferences : public ESPPreferences { } return to_save.data != stored_data.data; } + + bool reset() override { + ESP_LOGD(TAG, "Cleaning up preferences in flash..."); + s_pending_save.clear(); + + nvs_flash_deinit(); + nvs_flash_erase(); + // Make the handle invalid to prevent any saves until restart + nvs_handle = 0; + return true; + } }; void setup_preferences() { diff --git a/esphome/components/esp8266/preferences.cpp b/esphome/components/esp8266/preferences.cpp index 0e42cea576..1bd20f16ae 100644 --- a/esphome/components/esp8266/preferences.cpp +++ b/esphome/components/esp8266/preferences.cpp @@ -243,17 +243,34 @@ class ESP8266Preferences : public ESPPreferences { } } if (erase_res != SPI_FLASH_RESULT_OK) { - ESP_LOGV(TAG, "Erase ESP8266 flash failed!"); + ESP_LOGE(TAG, "Erase ESP8266 flash failed!"); return false; } if (write_res != SPI_FLASH_RESULT_OK) { - ESP_LOGV(TAG, "Write ESP8266 flash failed!"); + ESP_LOGE(TAG, "Write ESP8266 flash failed!"); return false; } s_flash_dirty = false; return true; } + + bool reset() override { + ESP_LOGD(TAG, "Cleaning up preferences in flash..."); + SpiFlashOpResult erase_res; + { + InterruptLock lock; + erase_res = spi_flash_erase_sector(get_esp8266_flash_sector()); + } + if (erase_res != SPI_FLASH_RESULT_OK) { + ESP_LOGE(TAG, "Erase ESP8266 flash failed!"); + return false; + } + + // Protect flash from writing till restart + s_prevent_write = true; + return true; + } }; void setup_preferences() { diff --git a/esphome/components/factory_reset/__init__.py b/esphome/components/factory_reset/__init__.py new file mode 100644 index 0000000000..f1bcfd8c55 --- /dev/null +++ b/esphome/components/factory_reset/__init__.py @@ -0,0 +1,5 @@ +import esphome.codegen as cg + +CODEOWNERS = ["@anatoly-savchenkov"] + +factory_reset_ns = cg.esphome_ns.namespace("factory_reset") diff --git a/esphome/components/factory_reset/button/__init__.py b/esphome/components/factory_reset/button/__init__.py new file mode 100644 index 0000000000..d5beac34b5 --- /dev/null +++ b/esphome/components/factory_reset/button/__init__.py @@ -0,0 +1,30 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import button +from esphome.const import ( + CONF_ID, + DEVICE_CLASS_RESTART, + ENTITY_CATEGORY_CONFIG, + ICON_RESTART_ALERT, +) +from .. import factory_reset_ns + +FactoryResetButton = factory_reset_ns.class_( + "FactoryResetButton", button.Button, cg.Component +) + +CONFIG_SCHEMA = ( + button.button_schema( + device_class=DEVICE_CLASS_RESTART, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_RESTART_ALERT, + ) + .extend({cv.GenerateID(): cv.declare_id(FactoryResetButton)}) + .extend(cv.COMPONENT_SCHEMA) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await button.register_button(var, config) diff --git a/esphome/components/factory_reset/button/factory_reset_button.cpp b/esphome/components/factory_reset/button/factory_reset_button.cpp new file mode 100644 index 0000000000..9354a3363e --- /dev/null +++ b/esphome/components/factory_reset/button/factory_reset_button.cpp @@ -0,0 +1,21 @@ +#include "factory_reset_button.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" +#include "esphome/core/application.h" + +namespace esphome { +namespace factory_reset { + +static const char *const TAG = "factory_reset.button"; + +void FactoryResetButton::dump_config() { LOG_BUTTON("", "Factory Reset Button", this); } +void FactoryResetButton::press_action() { + ESP_LOGI(TAG, "Resetting to factory defaults..."); + // Let MQTT settle a bit + delay(100); // NOLINT + global_preferences->reset(); + App.safe_reboot(); +} + +} // namespace factory_reset +} // namespace esphome diff --git a/esphome/components/factory_reset/button/factory_reset_button.h b/esphome/components/factory_reset/button/factory_reset_button.h new file mode 100644 index 0000000000..9996a860d9 --- /dev/null +++ b/esphome/components/factory_reset/button/factory_reset_button.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/button/button.h" + +namespace esphome { +namespace factory_reset { + +class FactoryResetButton : public button::Button, public Component { + public: + void dump_config() override; + + protected: + void press_action() override; +}; + +} // namespace factory_reset +} // namespace esphome diff --git a/esphome/components/factory_reset/switch/__init__.py b/esphome/components/factory_reset/switch/__init__.py new file mode 100644 index 0000000000..3cc19a35a3 --- /dev/null +++ b/esphome/components/factory_reset/switch/__init__.py @@ -0,0 +1,35 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import switch +from esphome.const import ( + CONF_ENTITY_CATEGORY, + CONF_ID, + CONF_INVERTED, + CONF_ICON, + ENTITY_CATEGORY_CONFIG, + ICON_RESTART_ALERT, +) +from .. import factory_reset_ns + +FactoryResetSwitch = factory_reset_ns.class_( + "FactoryResetSwitch", switch.Switch, cg.Component +) + +CONFIG_SCHEMA = switch.SWITCH_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(FactoryResetSwitch), + cv.Optional(CONF_INVERTED): cv.invalid( + "Factory Reset switches do not support inverted mode!" + ), + cv.Optional(CONF_ICON, default=ICON_RESTART_ALERT): cv.icon, + cv.Optional( + CONF_ENTITY_CATEGORY, default=ENTITY_CATEGORY_CONFIG + ): cv.entity_category, + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await switch.register_switch(var, config) diff --git a/esphome/components/factory_reset/switch/factory_reset_switch.cpp b/esphome/components/factory_reset/switch/factory_reset_switch.cpp new file mode 100644 index 0000000000..7bc8676736 --- /dev/null +++ b/esphome/components/factory_reset/switch/factory_reset_switch.cpp @@ -0,0 +1,26 @@ +#include "factory_reset_switch.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" +#include "esphome/core/application.h" + +namespace esphome { +namespace factory_reset { + +static const char *const TAG = "factory_reset.switch"; + +void FactoryResetSwitch::dump_config() { LOG_SWITCH("", "Factory Reset Switch", this); } +void FactoryResetSwitch::write_state(bool state) { + // Acknowledge + this->publish_state(false); + + if (state) { + ESP_LOGI(TAG, "Resetting to factory defaults..."); + // Let MQTT settle a bit + delay(100); // NOLINT + global_preferences->reset(); + App.safe_reboot(); + } +} + +} // namespace factory_reset +} // namespace esphome diff --git a/esphome/components/factory_reset/switch/factory_reset_switch.h b/esphome/components/factory_reset/switch/factory_reset_switch.h new file mode 100644 index 0000000000..2c914ea76d --- /dev/null +++ b/esphome/components/factory_reset/switch/factory_reset_switch.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/switch/switch.h" + +namespace esphome { +namespace factory_reset { + +class FactoryResetSwitch : public switch_::Switch, public Component { + public: + void dump_config() override; + + protected: + void write_state(bool state) override; +}; + +} // namespace factory_reset +} // namespace esphome diff --git a/esphome/core/preferences.h b/esphome/core/preferences.h index 2b13061a59..6d2dd967e9 100644 --- a/esphome/core/preferences.h +++ b/esphome/core/preferences.h @@ -46,6 +46,14 @@ class ESPPreferences { */ virtual bool sync() = 0; + /** + * Forget all unsaved changes and re-initialize the permanent preferences storage. + * Usually followed by a restart which moves the system to "factory" conditions + * + * @return true if operation is successful. + */ + virtual bool reset() = 0; + template::value, bool> = true> ESPPreferenceObject make_preference(uint32_t type, bool in_flash) { return this->make_preference(sizeof(T), type, in_flash); diff --git a/tests/test1.yaml b/tests/test1.yaml index e5e9754d74..e213a8b041 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -2178,6 +2178,8 @@ switch: name: Living Room Restart - platform: safe_mode name: Living Room Restart (Safe Mode) + - platform: factory_reset + name: Living Room Restart (Factory Default Settings) - platform: shutdown name: Living Room Shutdown - platform: output diff --git a/tests/test3.yaml b/tests/test3.yaml index 1d4b4fb076..4eee0fd2c9 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -1529,6 +1529,9 @@ button: target_mac_address: 12:34:56:78:90:ab name: wol_test_1 id: wol_1 + - platform: factory_reset + name: Restart Button (Factory Default Settings) + cd74hc4067: pin_s0: GPIO12