mirror of
https://github.com/esphome/esphome.git
synced 2026-10-17 19:52:50 +00:00
[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:
co-authored by
Claude Opus 5.5
J. Nick Koston
J. Nick Koston
parent
dc0d79635b
commit
1368ce4342
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user