Compare commits

..
15 changed files with 159 additions and 107 deletions

No files matched your search

-2
View File
@@ -449,8 +449,6 @@ file does, and it is the authority when they disagree. The most useful starting
```
Restore an owned or standalone switch in `setup()` with `sw->restore()`, or `restore(default)`
to still drive a state when the restore mode is disabled.
A hub that applies its switches' restored states itself reads `sw->get_initial_write_state()`, which
already applies `inverted`.
- **Other codegen helpers:** `uart.new_uart_component(config, *ctor_args)` (and `i2c.new_i2c_component`,
`spi.new_spi_component`, `modbus.new_modbus_client_component`, `ble_client.new_ble_node_component`)
@@ -173,8 +173,8 @@ void DFRobotSEN0609Component::setup() {
switch_::Switch *sw = this->switches_[i];
if (sw == nullptr)
continue;
// A disabled restore mode leaves the switch on, which an inverted switch writes as off
this->publish_switch_(static_cast<SwitchSetting>(i), sw->get_initial_write_state().value_or(!sw->is_inverted()));
const bool on = sw->get_initial_state_with_restore_mode().value_or(true);
this->publish_switch_(static_cast<SwitchSetting>(i), on != sw->is_inverted());
}
#endif
this->queue_configuration_();
@@ -155,9 +155,12 @@ void HitachiClimate::setup() {
set_swing_v_(swing_vertical(restore->swing_mode));
}
#ifdef USE_SWITCH
// The restored value is the switch state, which an inverted switch flips relative to the frame bit
if (this->mildew_proof_switch_ != nullptr) {
if (auto initial = this->mildew_proof_switch_->get_initial_write_state())
assign_bits(this->remote_state_[HITACHI_AC344_MILDEWPROOF_BYTE], 1 << HITACHI_AC344_MILDEWPROOF_OFFSET, *initial);
if (auto initial = this->mildew_proof_switch_->get_initial_state_with_restore_mode()) {
assign_bits(this->remote_state_[HITACHI_AC344_MILDEWPROOF_BYTE], 1 << HITACHI_AC344_MILDEWPROOF_OFFSET,
*initial != this->mildew_proof_switch_->is_inverted());
}
}
#endif
this->setup_(restore);
+32 -76
View File
@@ -9,6 +9,7 @@
#include "esphome/core/application.h"
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
#include "esphome/core/progmem.h"
#include <algorithm>
#include <cinttypes>
@@ -19,23 +20,6 @@ namespace esphome::ld2450 {
ESPHOME_LOG_TAG(TAG, "ld2450");
enum BaudRate : uint8_t {
BAUD_RATE_9600 = 1,
BAUD_RATE_19200 = 2,
BAUD_RATE_38400 = 3,
BAUD_RATE_57600 = 4,
BAUD_RATE_115200 = 5,
BAUD_RATE_230400 = 6,
BAUD_RATE_256000 = 7,
BAUD_RATE_460800 = 8
};
enum ZoneType : uint8_t {
ZONE_DISABLED = 0,
ZONE_DETECTION = 1,
ZONE_FILTER = 2,
};
enum PeriodicData : uint8_t {
TARGET_X = 4,
TARGET_Y = 6,
@@ -54,61 +38,20 @@ enum AckData : uint8_t {
COMMAND_STATUS = 7,
};
// Memory-efficient lookup tables
struct StringToUint8 {
const char *str;
const uint8_t value;
};
// Index + 1 is the BaudRate value; the select options use the same order
PROGMEM_STRING_TABLE(BaudRateStrings, "9600", "19200", "38400", "57600", "115200", "230400", "256000", "460800");
static_assert(BAUD_RATE_9600 == 1 && BAUD_RATE_460800 == BaudRateStrings::COUNT, "BaudRate is index + 1");
// Index is the Direction value
PROGMEM_STRING_TABLE(DirectionStrings, "Approaching", "Moving away", "Stationary", "NA");
static_assert(DIRECTION_APPROACHING == 0 && DIRECTION_MOVING_AWAY == 1 && DIRECTION_STATIONARY == 2 &&
DIRECTION_NA == 3,
"Direction is the table index");
// Index is the ZoneType value; the select options use the same order
PROGMEM_STRING_TABLE(ZoneTypeStrings, "Disabled", "Detection", "Filter");
static_assert(ZONE_DISABLED == 0 && ZONE_DETECTION == 1 && ZONE_FILTER == 2, "ZoneType is the table index");
struct Uint8ToString {
const uint8_t value;
const char *str;
};
constexpr StringToUint8 BAUD_RATES_BY_STR[] = {
{"9600", BAUD_RATE_9600}, {"19200", BAUD_RATE_19200}, {"38400", BAUD_RATE_38400},
{"57600", BAUD_RATE_57600}, {"115200", BAUD_RATE_115200}, {"230400", BAUD_RATE_230400},
{"256000", BAUD_RATE_256000}, {"460800", BAUD_RATE_460800},
};
constexpr Uint8ToString DIRECTION_BY_UINT[] = {
{DIRECTION_APPROACHING, "Approaching"},
{DIRECTION_MOVING_AWAY, "Moving away"},
{DIRECTION_STATIONARY, "Stationary"},
{DIRECTION_NA, "NA"},
};
constexpr Uint8ToString ZONE_TYPE_BY_UINT[] = {
{ZONE_DISABLED, "Disabled"},
{ZONE_DETECTION, "Detection"},
{ZONE_FILTER, "Filter"},
};
constexpr StringToUint8 ZONE_TYPE_BY_STR[] = {
{"Disabled", ZONE_DISABLED},
{"Detection", ZONE_DETECTION},
{"Filter", ZONE_FILTER},
};
// Baud rates in the same order as BAUD_RATES_BY_STR for index-based lookup
constexpr uint32_t BAUD_RATES[] = {9600, 19200, 38400, 57600, 115200, 230400, 256000, 460800};
// Helper functions for lookups
template<size_t N> uint8_t find_uint8(const StringToUint8 (&arr)[N], const std::string &str) {
for (const auto &entry : arr) {
if (str == entry.str)
return entry.value;
}
return 0xFF; // Not found
}
template<size_t N> const char *find_str(const Uint8ToString (&arr)[N], uint8_t value) {
for (const auto &entry : arr) {
if (value == entry.value)
return entry.str;
}
return ""; // Not found
}
// Baud rates in the same order as BaudRateStrings for index-based lookup
static constexpr uint32_t BAUD_RATES[] PROGMEM = {9600, 19200, 38400, 57600, 115200, 230400, 256000, 460800};
// LD2450 UART Serial Commands
static constexpr uint8_t CMD_ENABLE_CONF = 0xFF;
@@ -546,7 +489,7 @@ void LD2450Component::handle_periodic_data_() {
if (this->direction_dedup_[index].next(direction)) {
text_sensor::TextSensor *tsd = this->direction_text_sensors_[index];
if (tsd != nullptr) {
tsd->publish_state(find_str(ld2450::DIRECTION_BY_UINT, direction));
tsd->publish_state(DirectionStrings::get_progmem_str(direction, DirectionStrings::LAST_INDEX));
}
}
#endif
@@ -864,8 +807,15 @@ void LD2450Component::set_bluetooth(bool enable) {
// Set Baud rate
void LD2450Component::set_baud_rate(const char *state) {
const int16_t index = progmem_string_table_find<BaudRateStrings>(state, strlen(state));
if (index >= 0) {
this->set_baud_rate(static_cast<BaudRate>(index + 1));
}
}
void LD2450Component::set_baud_rate(BaudRate rate) {
this->set_config_mode_(true);
const uint8_t cmd_value[2] = {find_uint8(BAUD_RATES_BY_STR, state), 0x00};
const uint8_t cmd_value[2] = {rate, 0x00};
this->send_command_(CMD_SET_BAUD_RATE, cmd_value, sizeof(cmd_value));
this->set_timeout(200, [this]() { this->restart_(); });
}
@@ -873,8 +823,14 @@ void LD2450Component::set_baud_rate(const char *state) {
// Set Zone Type - one of: Disabled, Detection, Filter
void LD2450Component::set_zone_type(const char *state) {
ESP_LOGV(TAG, "Set zone type: %s", state);
uint8_t zone_type = find_uint8(ZONE_TYPE_BY_STR, state);
this->zone_type_ = zone_type;
const int16_t index = progmem_string_table_find<ZoneTypeStrings>(state, strlen(state));
if (index >= 0) {
this->set_zone_type(static_cast<ZoneType>(index));
}
}
void LD2450Component::set_zone_type(ZoneType type) {
this->zone_type_ = type;
this->send_set_zone_command_();
}
@@ -882,7 +838,7 @@ void LD2450Component::set_zone_type(const char *state) {
void LD2450Component::publish_zone_type() {
#ifdef USE_SELECT
if (this->zone_type_select_ != nullptr) {
this->zone_type_select_->publish_state(find_str(ZONE_TYPE_BY_UINT, this->zone_type_));
this->zone_type_select_->publish_state(static_cast<size_t>(this->zone_type_));
}
#endif
}
+19
View File
@@ -55,6 +55,23 @@ enum Direction : uint8_t {
DIRECTION_UNDEFINED = 4,
};
enum BaudRate : uint8_t {
BAUD_RATE_9600 = 1,
BAUD_RATE_19200 = 2,
BAUD_RATE_38400 = 3,
BAUD_RATE_57600 = 4,
BAUD_RATE_115200 = 5,
BAUD_RATE_230400 = 6,
BAUD_RATE_256000 = 7,
BAUD_RATE_460800 = 8
};
enum ZoneType : uint8_t {
ZONE_DISABLED = 0,
ZONE_DETECTION = 1,
ZONE_FILTER = 2,
};
// Target coordinate struct
struct Target {
int16_t x;
@@ -121,7 +138,9 @@ class LD2450Component : public Component, public uart::UARTDevice {
void set_bluetooth(bool enable);
void set_multi_target(bool enable);
void set_baud_rate(const char *state);
void set_baud_rate(BaudRate rate);
void set_zone_type(const char *state);
void set_zone_type(ZoneType type);
void publish_zone_type();
void factory_reset();
#ifdef USE_TEXT_SENSOR
@@ -35,6 +35,7 @@ CONFIG_SCHEMA = {
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_LD2450_ID])
selects = select.sub_selects(config, parent=hub)
# Option lists follow the order of the C++ enums; the hub maps by index
await selects(
CONF_BAUD_RATE,
hub.set_baud_rate_select,
@@ -4,7 +4,7 @@ namespace esphome::ld2450 {
void BaudRateSelect::control(size_t index) {
this->publish_state(index);
this->parent_->set_baud_rate(this->option_at(index));
this->parent_->set_baud_rate(static_cast<BaudRate>(index + 1));
}
} // namespace esphome::ld2450
@@ -4,7 +4,7 @@ namespace esphome::ld2450 {
void ZoneTypeSelect::control(size_t index) {
this->publish_state(index);
this->parent_->set_zone_type(this->option_at(index));
this->parent_->set_zone_type(static_cast<ZoneType>(index));
}
} // namespace esphome::ld2450
+11 -3
View File
@@ -1,6 +1,7 @@
#pragma once
#include "esphome/core/defines.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include <memory>
@@ -24,10 +25,17 @@
namespace esphome::ld24xx {
// Helper to find index of value in constexpr array
template<size_t N> optional<size_t> find_index(const uint32_t (&arr)[N], uint32_t value) {
// Index of value in a constant table that may live in flash, or none
template<typename T, size_t N> optional<size_t> find_index(const T (&arr)[N], T value) {
static_assert(sizeof(T) == 1 || sizeof(T) == 4, "flash tables are read as bytes or aligned 32 bit words");
for (size_t i = 0; i < N; i++) {
if (arr[i] == value)
T entry;
if constexpr (sizeof(T) == 1) {
entry = progmem_read_byte(reinterpret_cast<const uint8_t *>(&arr[i]));
} else {
entry = arr[i]; // an aligned 32 bit read from flash is safe on ESP8266
}
if (entry == value)
return i;
}
return {};
@@ -25,7 +25,7 @@ void SeeedMultiChannelRelay::setup() {
switch_::Switch *sw = this->switches_[channel];
if (sw == nullptr)
continue;
const bool on = sw->get_initial_write_state().value_or(false);
const bool on = sw->get_initial_state_with_restore_mode().value_or(sw->is_inverted()) != sw->is_inverted();
assign_bits(value, 1 << channel, on);
}
if (!this->status_comm_result(this->write_byte(CMD_CHANNEL_CTRL, value)))
-5
View File
@@ -60,11 +60,6 @@ optional<bool> Switch::get_initial_state_with_restore_mode() {
}
return initial_state;
}
optional<bool> Switch::get_initial_write_state() {
if (auto initial = this->get_initial_state_with_restore_mode())
return *initial != this->inverted_;
return {};
}
void Switch::restore() {
if (auto initial = this->get_initial_state_with_restore_mode())
this->control(*initial);
-2
View File
@@ -114,8 +114,6 @@ class Switch : public EntityBase {
* state.
*/
optional<bool> get_initial_state_with_restore_mode();
/// get_initial_state_with_restore_mode() with inverted applied: the level write_state() takes.
optional<bool> get_initial_write_state();
/// Drive the switch to the state its restore mode yields, through control(), so inversion and
/// write_state() apply; a disabled restore mode drives \p when_disabled, or nothing without one.
+1
View File
@@ -30,6 +30,7 @@ class TestableLD2450 : public LD2450Component {
using LD2450Component::buffer_pos_;
using LD2450Component::readline_;
using LD2450Component::zone_config_;
using LD2450Component::zone_type_;
void feed(const std::vector<uint8_t> &data) {
for (uint8_t byte : data) {
@@ -0,0 +1,85 @@
#include <algorithm>
#include "common.h"
namespace esphome::ld2450::testing {
namespace {
class CapturingUARTComponent : public MockUARTComponent {
public:
void write_array(const uint8_t *data, size_t len) override {
this->written.insert(this->written.end(), data, data + len);
}
std::vector<uint8_t> written;
};
// Header, length 4, command 0xA1 (set baud rate): the rate byte and a zero follow.
constexpr uint8_t SET_BAUD_RATE_PREFIX[] = {0xFD, 0xFC, 0xFB, 0xFA, 0x04, 0x00, 0xA1, 0x00};
// Header, length 28, command 0xC2 (set zone): the zone type byte and a zero follow.
constexpr uint8_t SET_ZONE_PREFIX[] = {0xFD, 0xFC, 0xFB, 0xFA, 0x1C, 0x00, 0xC2, 0x00};
// The two payload bytes after the first frame with this prefix, or -1 when it was not written.
template<size_t N> std::pair<int, int> payload_after(const std::vector<uint8_t> &w, const uint8_t (&prefix)[N]) {
auto it = std::search(w.begin(), w.end(), std::begin(prefix), std::end(prefix));
if (it == w.end() || static_cast<size_t>(w.end() - it) < N + 2)
return {-1, -1};
return {*(it + N), *(it + N + 1)};
}
} // namespace
class LD2450SelectNamesTest : public ::testing::Test {
protected:
void SetUp() override { this->ld2450_.set_uart_parent(&this->uart_); }
CapturingUARTComponent uart_;
TestableLD2450 ld2450_;
};
TEST_F(LD2450SelectNamesTest, BaudRateNamesSendTheirDeviceValue) {
static constexpr const char *NAMES[] = {"9600", "19200", "38400", "57600", "115200", "230400", "256000", "460800"};
for (uint8_t i = 0; i < 8; i++) {
this->uart_.written.clear();
this->ld2450_.set_baud_rate(NAMES[i]);
const auto payload = payload_after(this->uart_.written, SET_BAUD_RATE_PREFIX);
EXPECT_EQ(payload.first, i + 1) << NAMES[i];
EXPECT_EQ(payload.second, 0) << NAMES[i];
}
}
TEST_F(LD2450SelectNamesTest, BaudRateEnumMatchesTheSelectOrder) {
this->ld2450_.set_baud_rate(BAUD_RATE_256000);
EXPECT_EQ(payload_after(this->uart_.written, SET_BAUD_RATE_PREFIX).first, 7);
}
TEST_F(LD2450SelectNamesTest, UnknownBaudRateNameWritesNothing) {
this->ld2450_.set_baud_rate("1200");
EXPECT_TRUE(this->uart_.written.empty());
}
TEST_F(LD2450SelectNamesTest, ZoneTypeNamesSetTheTypeAndSendIt) {
static constexpr const char *NAMES[] = {"Disabled", "Detection", "Filter"};
for (uint8_t i = 0; i < 3; i++) {
this->uart_.written.clear();
this->ld2450_.set_zone_type(NAMES[i]);
EXPECT_EQ(this->ld2450_.zone_type_, i) << NAMES[i];
const auto payload = payload_after(this->uart_.written, SET_ZONE_PREFIX);
EXPECT_EQ(payload.first, i) << NAMES[i];
EXPECT_EQ(payload.second, 0) << NAMES[i];
}
}
TEST_F(LD2450SelectNamesTest, ZoneTypeEnumMatchesTheSelectOrder) {
this->ld2450_.set_zone_type(ZONE_FILTER);
EXPECT_EQ(this->ld2450_.zone_type_, 2);
this->ld2450_.set_zone_type(ZONE_DETECTION);
EXPECT_EQ(this->ld2450_.zone_type_, 1);
}
TEST_F(LD2450SelectNamesTest, UnknownZoneTypeNameChangesNothing) {
this->ld2450_.set_zone_type(ZONE_DETECTION);
this->uart_.written.clear();
this->ld2450_.set_zone_type("Bogus");
EXPECT_EQ(this->ld2450_.zone_type_, 1);
EXPECT_TRUE(this->uart_.written.empty());
}
} // namespace esphome::ld2450::testing
-12
View File
@@ -46,16 +46,4 @@ TEST(SwitchRestore, DefaultAppliesOnlyWhenTheModeIsDisabled) {
EXPECT_EQ(on.writes, std::vector<bool>{true});
}
TEST(SwitchRestore, InitialWriteStateAppliesInversion) {
RecordingSwitch on, inverted, disabled;
on.set_restore_mode(SWITCH_ALWAYS_ON);
inverted.set_restore_mode(SWITCH_ALWAYS_ON);
inverted.set_inverted(true);
disabled.set_restore_mode(SWITCH_RESTORE_DISABLED);
disabled.set_inverted(true);
EXPECT_EQ(on.get_initial_write_state(), optional<bool>(true));
EXPECT_EQ(inverted.get_initial_write_state(), optional<bool>(false));
EXPECT_FALSE(disabled.get_initial_write_state().has_value());
}
} // namespace esphome::switch_::testing