diff --git a/esphome/components/dfrobot_sen0609/button/__init__.py b/esphome/components/dfrobot_sen0609/button/__init__.py new file mode 100644 index 00000000000..1db608e62c5 --- /dev/null +++ b/esphome/components/dfrobot_sen0609/button/__init__.py @@ -0,0 +1,57 @@ +import esphome.codegen as cg +from esphome.components import button +from esphome.components.const import CONF_QUERY_PARAMS +import esphome.config_validation as cv +from esphome.const import ( + CONF_FACTORY_RESET, + CONF_ID, + CONF_RESTART, + DEVICE_CLASS_RESTART, + ENTITY_CATEGORY_CONFIG, + ENTITY_CATEGORY_DIAGNOSTIC, + ICON_DATABASE, + ICON_RESTART, + ICON_RESTART_ALERT, +) +from esphome.types import ConfigType + +from .. import CONF_DFROBOT_SEN0609_ID, DFRobotSEN0609Component, dfrobot_sen0609_ns + +DFRobotSEN0609Button = dfrobot_sen0609_ns.class_("DFRobotSEN0609Button", button.Button) +ButtonAction = dfrobot_sen0609_ns.enum("ButtonAction") + +CONFIG_SCHEMA = { + cv.GenerateID(CONF_ID): cv.declare_id(cg.EntityBase), + cv.GenerateID(CONF_DFROBOT_SEN0609_ID): cv.use_id(DFRobotSEN0609Component), + cv.Optional(CONF_FACTORY_RESET): button.button_schema( + DFRobotSEN0609Button, + device_class=DEVICE_CLASS_RESTART, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_RESTART_ALERT, + ), + cv.Optional(CONF_QUERY_PARAMS): button.button_schema( + DFRobotSEN0609Button, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + icon=ICON_DATABASE, + ), + cv.Optional(CONF_RESTART): button.button_schema( + DFRobotSEN0609Button, + device_class=DEVICE_CLASS_RESTART, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + icon=ICON_RESTART, + ), +} + + +async def to_code(config: ConfigType) -> None: + hub = await cg.get_variable(config[CONF_DFROBOT_SEN0609_ID]) + buttons = button.sub_buttons(config, parent=hub) + await buttons( + CONF_FACTORY_RESET, + hub.set_factory_reset_button, + ButtonAction.BUTTON_FACTORY_RESET, + ) + await buttons( + CONF_QUERY_PARAMS, hub.set_query_button, ButtonAction.BUTTON_QUERY_PARAMS + ) + await buttons(CONF_RESTART, hub.set_restart_button, ButtonAction.BUTTON_RESTART) diff --git a/esphome/components/dfrobot_sen0609/button/dfrobot_sen0609_button.h b/esphome/components/dfrobot_sen0609/button/dfrobot_sen0609_button.h new file mode 100644 index 00000000000..c7c03aa2b87 --- /dev/null +++ b/esphome/components/dfrobot_sen0609/button/dfrobot_sen0609_button.h @@ -0,0 +1,30 @@ +#pragma once + +#include "esphome/components/button/button.h" +#include "../dfrobot_sen0609.h" + +namespace esphome::dfrobot_sen0609 { + +class DFRobotSEN0609Button final : public button::Button, public Parented { + public: + explicit DFRobotSEN0609Button(ButtonAction action) : action_(action) {} + + protected: + void press_action() override { + switch (this->action_) { + case BUTTON_RESTART: + this->parent_->restart(); + break; + case BUTTON_FACTORY_RESET: + this->parent_->factory_reset(); + break; + case BUTTON_QUERY_PARAMS: + this->parent_->query_params(); + break; + } + } + + ButtonAction action_; +}; + +} // namespace esphome::dfrobot_sen0609 diff --git a/esphome/components/dfrobot_sen0609/dfrobot_sen0609.cpp b/esphome/components/dfrobot_sen0609/dfrobot_sen0609.cpp index 26538998a2f..5f42d05a827 100644 --- a/esphome/components/dfrobot_sen0609/dfrobot_sen0609.cpp +++ b/esphome/components/dfrobot_sen0609/dfrobot_sen0609.cpp @@ -21,6 +21,9 @@ static constexpr uint32_t COMMAND_GAP_MS = 100; static constexpr uint32_t COALESCE_MS = 1000; static constexpr uint32_t DEFAULT_TIMEOUT_MS = 1000; static constexpr uint32_t LONG_TIMEOUT_MS = 2000; +static constexpr uint32_t FACTORY_RESET_TIMEOUT_MS = 3000; +// resetSystem has no answer; this is how long the module takes to boot again +static constexpr uint32_t RESTART_MS = 1500; static constexpr uint32_t GIVE_UP_BACKOFF_MS = 30000; // Presence frames come every second (setUartOutput period); this long without one means the module lost them static constexpr uint32_t FRAME_TIMEOUT_MS = 10000; @@ -113,6 +116,10 @@ static const LogString *operation_to_str(Operation operation) { return LOG_STR("stop"); case Operation::OPERATION_START: return LOG_STR("start"); + case Operation::OPERATION_RESTART: + return LOG_STR("restart"); + case Operation::OPERATION_FACTORY_RESET: + return LOG_STR("factory reset"); case Operation::OPERATION_WRITE: return LOG_STR("write"); case Operation::OPERATION_SAVE: @@ -161,6 +168,11 @@ void DFRobotSEN0609Component::dump_config() { #ifdef USE_SELECT LOG_SELECT(" ", "OutPinLevel", this->out_pin_level_select_); #endif +#ifdef USE_BUTTON + LOG_BUTTON(" ", "FactoryReset", this->factory_reset_button_); + LOG_BUTTON(" ", "Query", this->query_button_); + LOG_BUTTON(" ", "Restart", this->restart_button_); +#endif } void DFRobotSEN0609Component::setup() { @@ -436,6 +448,21 @@ void DFRobotSEN0609Component::request_write_(Param param) { this->hold_(COALESCE_MS); } +void DFRobotSEN0609Component::restart() { + this->restart_pending_ = true; + this->hold_(COMMAND_GAP_MS); +} + +void DFRobotSEN0609Component::factory_reset() { + this->factory_reset_pending_ = true; + this->hold_(COMMAND_GAP_MS); +} + +void DFRobotSEN0609Component::query_params() { + this->pending_reads_ |= this->wanted_reads_(); + this->hold_(COMMAND_GAP_MS); +} + #ifdef USE_NUMBER void DFRobotSEN0609Component::control_number(NumberSetting setting, float value) { // The module needs min_range <= trigger_range <= max_range; a value not read yet is NAN and passes @@ -511,20 +538,42 @@ void DFRobotSEN0609Component::hold_(uint32_t duration_ms) { void DFRobotSEN0609Component::run_sequencer_() { const uint32_t now = App.get_loop_component_start_time(); if (this->operation_ != Operation::OPERATION_NONE) { - const bool slow = this->operation_ == Operation::OPERATION_STOP || this->operation_ == Operation::OPERATION_SAVE; - if (now - this->sent_ms_ < (slow ? LONG_TIMEOUT_MS : DEFAULT_TIMEOUT_MS)) + uint32_t timeout = DEFAULT_TIMEOUT_MS; + switch (this->operation_) { + case Operation::OPERATION_RESTART: + timeout = RESTART_MS; + break; + case Operation::OPERATION_FACTORY_RESET: + timeout = FACTORY_RESET_TIMEOUT_MS; + break; + case Operation::OPERATION_STOP: + case Operation::OPERATION_SAVE: + timeout = LONG_TIMEOUT_MS; + break; + default: + break; + } + if (now - this->sent_ms_ < timeout) return; - ESP_LOGD(TAG, "No answer to '%s'", this->command_); - this->fail_operation_(); + if (this->operation_ == Operation::OPERATION_RESTART) { + this->finish_operation_(); + } else { + ESP_LOGD(TAG, "No answer to '%s'", this->command_); + this->fail_operation_(); + } return; } if (now - this->hold_start_ms_ < this->hold_ms_) return; - const bool work_pending = this->save_pending_ || this->pending_writes_ != 0 || this->pending_reads_ != 0; + const bool work_pending = + this->factory_reset_pending_ || this->save_pending_ || this->pending_writes_ != 0 || this->pending_reads_ != 0; Operation operation; Param param = PARAM_COUNT; - if (!work_pending) { + if (this->restart_pending_) { + // The reboot drops a write the module took but did not save; writes not sent yet are kept for after it + operation = this->stopped_ && this->save_pending_ ? Operation::OPERATION_SAVE : Operation::OPERATION_RESTART; + } else if (!work_pending) { if (!this->stopped_) { this->check_frames_(now); // Work given up on is tried again in a later sequence, so the sensor runs in between @@ -539,6 +588,8 @@ void DFRobotSEN0609Component::run_sequencer_() { operation = Operation::OPERATION_START; } else if (!this->stopped_) { operation = Operation::OPERATION_STOP; + } else if (this->factory_reset_pending_) { + operation = Operation::OPERATION_FACTORY_RESET; } else if (this->pending_writes_ != 0) { operation = Operation::OPERATION_WRITE; param = static_cast(__builtin_ctz(this->pending_writes_)); @@ -611,6 +662,12 @@ bool DFRobotSEN0609Component::build_command_(Operation operation, Param param) { case Operation::OPERATION_START: ESPHOME_strncpy_P(buf, ESPHOME_PSTR("sensorStart"), len); return true; + case Operation::OPERATION_RESTART: + ESPHOME_strncpy_P(buf, ESPHOME_PSTR("resetSystem"), len); + return true; + case Operation::OPERATION_FACTORY_RESET: + ESPHOME_strncpy_P(buf, ESPHOME_PSTR("resetCfg"), len); + return true; case Operation::OPERATION_SAVE: ESPHOME_strncpy_P(buf, ESPHOME_PSTR("saveConfig"), len); return true; @@ -785,6 +842,16 @@ void DFRobotSEN0609Component::finish_operation_() { this->status_clear_warning(); this->sequence_failed_ = false; break; + case Operation::OPERATION_RESTART: + // The module starts sensing on its own after a reboot + this->restart_pending_ = false; + this->stopped_ = false; + this->queue_configuration_(); + break; + case Operation::OPERATION_FACTORY_RESET: + this->factory_reset_pending_ = false; + this->queue_configuration_(); + break; case Operation::OPERATION_WRITE: this->deferred_writes_ &= ~param_bit(this->param_); if (PERSISTENT_PARAMS & param_bit(this->param_)) @@ -846,6 +913,12 @@ void DFRobotSEN0609Component::fail_operation_() { ESP_LOGW(TAG, "Giving up on %s '%s'", LOG_STR_ARG(operation_to_str(operation)), this->command_); this->sequence_failed_ = true; switch (operation) { + case Operation::OPERATION_RESTART: + this->restart_pending_ = false; + break; + case Operation::OPERATION_FACTORY_RESET: + this->factory_reset_pending_ = false; + break; case Operation::OPERATION_WRITE: #ifdef USE_SWITCH if (is_led(this->param_)) { diff --git a/esphome/components/dfrobot_sen0609/dfrobot_sen0609.h b/esphome/components/dfrobot_sen0609/dfrobot_sen0609.h index 719aa6f5fe0..ca70fa14943 100644 --- a/esphome/components/dfrobot_sen0609/dfrobot_sen0609.h +++ b/esphome/components/dfrobot_sen0609/dfrobot_sen0609.h @@ -21,6 +21,9 @@ #ifdef USE_SELECT #include "esphome/components/select/select.h" #endif +#ifdef USE_BUTTON +#include "esphome/components/button/button.h" +#endif #include "esphome/components/uart/uart.h" #include @@ -58,6 +61,12 @@ enum SwitchSetting : uint8_t { SWITCH_COUNT, }; +enum ButtonAction : uint8_t { + BUTTON_RESTART, + BUTTON_FACTORY_RESET, + BUTTON_QUERY_PARAMS, +}; + /// Module parameters; a write or read of each is one command. Pending work runs in this order. enum Param : uint8_t { PARAM_RUN_APP, @@ -81,6 +90,8 @@ enum class Operation : uint8_t { OPERATION_NONE, OPERATION_STOP, OPERATION_START, + OPERATION_RESTART, + OPERATION_FACTORY_RESET, OPERATION_WRITE, OPERATION_SAVE, OPERATION_READ, @@ -102,6 +113,11 @@ class DFRobotSEN0609Component final : public Component, public uart::UARTDevice #ifdef USE_SELECT SUB_SELECT(out_pin_level) #endif +#ifdef USE_BUTTON + SUB_BUTTON(factory_reset) + SUB_BUTTON(query) + SUB_BUTTON(restart) +#endif public: explicit DFRobotSEN0609Component(RunMode mode); @@ -121,6 +137,12 @@ class DFRobotSEN0609Component final : public Component, public uart::UARTDevice #ifdef USE_SELECT void control_out_pin_level(size_t index); #endif + /// Reboot the module, then apply the configuration and read the settings again. + void restart(); + /// Restore the module's factory settings, then apply the configuration and read the settings again. + void factory_reset(); + /// Read all settings shown by entities from the module. + void query_params(); protected: void readline_(uint8_t c); @@ -207,6 +229,8 @@ class DFRobotSEN0609Component final : public Component, public uart::UARTDevice bool stopped_{false}; bool save_pending_{false}; bool deferred_save_{false}; + bool restart_pending_{false}; + bool factory_reset_pending_{false}; bool sequence_failed_{false}; }; diff --git a/tests/components/dfrobot_sen0609/benchmark.yaml b/tests/components/dfrobot_sen0609/benchmark.yaml index 2a35b04794d..3c20e117389 100644 --- a/tests/components/dfrobot_sen0609/benchmark.yaml +++ b/tests/components/dfrobot_sen0609/benchmark.yaml @@ -6,3 +6,4 @@ text_sensor: number: switch: select: +button: diff --git a/tests/components/dfrobot_sen0609/common.yaml b/tests/components/dfrobot_sen0609/common.yaml index aca15b906fc..bb7ad5d622f 100644 --- a/tests/components/dfrobot_sen0609/common.yaml +++ b/tests/components/dfrobot_sen0609/common.yaml @@ -49,3 +49,13 @@ select: dfrobot_sen0609_id: sen0609_presence out_pin_level: name: Out Pin Level + +button: + - platform: dfrobot_sen0609 + dfrobot_sen0609_id: sen0609_presence + factory_reset: + name: Factory Reset + query_params: + name: Query Params + restart: + name: Restart diff --git a/tests/components/dfrobot_sen0609/dfrobot_sen0609_sequencer.cpp b/tests/components/dfrobot_sen0609/dfrobot_sen0609_sequencer.cpp index aa39c4f6a4d..528c788490a 100644 --- a/tests/components/dfrobot_sen0609/dfrobot_sen0609_sequencer.cpp +++ b/tests/components/dfrobot_sen0609/dfrobot_sen0609_sequencer.cpp @@ -340,6 +340,18 @@ TEST_F(SEN0609SequencerTest, FailedWriteShowsModuleValueAgain) { EXPECT_TRUE(this->hub_.status_has_warning()); } +TEST_F(SEN0609SequencerTest, QueryKeepsGapAfterAnswer) { + this->boot_(); + this->hub_.query_params(); + this->run_(); + EXPECT_EQ(this->uart_.tx, ""); + this->expect_and_answer_("sensorStop"); + this->expect_and_answer_("getRange", "Response 0.6 12.0\r\n"); + this->expect_and_answer_("getSensitivity", "Response 7 5\r\n"); + this->expect_and_answer_("getSWV", "SoftwareVersion:V1.0.4\r\n"); + this->expect_and_answer_("sensorStart"); +} + TEST_F(SEN0609SequencerTest, AlreadyStoppedCountsAsStopped) { this->hub_.setup(); ASSERT_EQ(this->next_command_(), "sensorStop"); @@ -480,6 +492,50 @@ TEST_F(SEN0609SequencerTest, TriggerRangeOutsideRangeIsRefused) { this->expect_and_answer_("setTrigRange 8.00"); } +TEST_F(SEN0609SequencerTest, RestartReappliesConfiguration) { + this->boot_(); + this->hub_.restart(); + ASSERT_EQ(this->next_command_(), "resetSystem"); + // resetSystem has no answer; the hub waits for the reboot + EXPECT_EQ(this->next_command_(1000), ""); + this->expect_and_answer_("sensorStop"); + this->expect_and_answer_("setRunApp 0"); +} + +TEST_F(SEN0609SequencerTest, RestartDuringSequenceSavesWrittenChange) { + this->boot_(); + this->hub_.control_number(NUMBER_MIN_RANGE, 1.0f); + this->expect_and_answer_("sensorStop"); + ASSERT_EQ(this->next_command_(), "setRange 1.00 12.00"); + this->hub_.control_number(NUMBER_HOLD_SENSITIVITY, 3.0f); + this->hub_.restart(); + this->answer_("setRange 1.00 12.00"); + this->expect_and_answer_("saveConfig"); + ASSERT_EQ(this->next_command_(), "resetSystem"); + EXPECT_EQ(this->next_command_(1000), ""); + // The change not sent before the reboot is written after it + this->expect_and_answer_("sensorStop"); + this->expect_and_answer_("setRunApp 0"); + this->expect_and_answer_("setUartOutput 1 1 1 1.0"); + this->expect_and_answer_("setSensitivity 3 5"); + this->expect_and_answer_("saveConfig"); + this->expect_and_answer_("getRange", "Response 1.0 12.0\r\n"); + this->expect_and_answer_("getSensitivity", "Response 3 5\r\n"); + EXPECT_FLOAT_EQ(this->min_range_.state, 1.0f); + EXPECT_FLOAT_EQ(this->hold_sensitivity_.state, 3.0f); +} + +TEST_F(SEN0609SequencerTest, FactoryResetRereadsSettings) { + this->boot_(); + this->hub_.factory_reset(); + this->expect_and_answer_("sensorStop"); + this->expect_and_answer_("resetCfg"); + this->expect_and_answer_("setRunApp 0"); + this->expect_and_answer_("setUartOutput 1 1 1 1.0"); + this->expect_and_answer_("getRange", "Response 0.6 25.0\r\n"); + EXPECT_FLOAT_EQ(this->max_range_.state, 25.0f); +} + class SEN0609SettingsTest : public SEN0609Test { protected: void SetUp() override {