[dfrobot_sen0609] Add restart, factory reset and query buttons (#20877)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
This commit is contained in:
Remco van Essen
2026-10-16 17:31:58 +00:00
committed by GitHub
co-authored by Claude Opus 5.5 J. Nick Koston J. Nick Koston
parent dc0d79635b
commit 1368ce4342
7 changed files with 257 additions and 6 deletions
@@ -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)
@@ -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<DFRobotSEN0609Component> {
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
@@ -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<Param>(__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_)) {
@@ -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 <array>
@@ -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};
};
@@ -6,3 +6,4 @@ text_sensor:
number:
switch:
select:
button:
@@ -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
@@ -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 {