Compare commits

..
Author SHA1 Message Date
J. Nick Koston f0473e7419 Merge branch 'dev' into ld2450_flash_strings 2026-10-21 18:02:58 -10:00
J. Nick Koston 3325de5e28 Avoid optional access in the select names test for clang-tidy 2026-10-21 17:50:06 -10:00
cc89a011e2 Bump clang-tidy + clang-format from 22.1.8 to 23.1.3 (#21296)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
Co-authored-by: esphome[bot] <115708604+esphome[bot]@users.noreply.github.com>
Signed-off-by: dependabot[bot] <support@github.com>
2026-10-21 17:45:36 -10:00
J. Nick Koston afffe3ba36 Test the baud rate and zone type name and enum paths 2026-10-21 17:32:14 -10:00
J. Nick Koston 6bf3e07a74 Note the select option order 2026-10-21 17:20:27 -10:00
J. Nick Koston c6c1fce2bf Read flash tables without memcpy and assert the table order 2026-10-21 17:13:04 -10:00
Craig Andrews c76e9e1d62 [epaper_spi] Support Good Display GDEY029Z95 2.9" 3-color e-paper (128x296, SSD1680) (#21218) 2026-10-21 17:09:44 -10:00
J. Nick Koston 4293d12008 Drive the selects by index 2026-10-21 17:04:06 -10:00
esphome[bot] 7a3aac407f Bump aioesphomeapi from 46.18.0 to 46.19.0 (#21345) 2026-10-21 17:03:34 -10:00
J. Nick Koston 51819d40cb Keep the baud rate table in flash 2026-10-21 17:01:47 -10:00
J. Nick Koston b0a1054e8d [ld2410] Publish queried number values directly (#21346) 2026-10-21 16:56:51 -10:00
J. Nick Koston e7ad8f9c80 [ld2450] Keep the baud rate, direction and zone type names in flash 2026-10-21 16:45:21 -10:00
a8a8acb918 [sensor] Add Relative Humidity Correction Filter (#18287)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2026-10-21 16:33:11 -10:00
41 changed files with 1039 additions and 397 deletions
+8
View File
@@ -12,6 +12,7 @@ Checks: >-
-bugprone-multi-level-implicit-pointer-conversion,
-bugprone-narrowing-conversions,
-bugprone-tagged-union-member-count,
-bugprone-signed-bitwise,
-bugprone-signed-char-misuse,
-bugprone-switch-missing-default-case,
-cert-dcl50-cpp,
@@ -39,6 +40,7 @@ Checks: >-
-cppcoreguidelines-avoid-const-or-ref-data-members,
-cppcoreguidelines-avoid-do-while,
-cppcoreguidelines-avoid-magic-numbers,
-cppcoreguidelines-explicit-constructor,
-cppcoreguidelines-init-variables,
-cppcoreguidelines-macro-to-enum,
-cppcoreguidelines-macro-usage,
@@ -84,6 +86,7 @@ Checks: >-
-llvm-use-ranges,
-llvmlibc-*,
-misc-const-correctness,
-misc-explicit-constructor,
-misc-include-cleaner,
-misc-multiple-inheritance,
-misc-no-recursion,
@@ -113,6 +116,7 @@ Checks: >-
-objc-*,
-performance-enum-size,
-portability-avoid-pragma-once,
-portability-no-assembler,
-portability-template-virtual-member-function,
-readability-ambiguous-smartptr-reset-call,
-readability-avoid-nested-conditional-operator,
@@ -129,9 +133,13 @@ Checks: >-
-readability-named-parameter,
-readability-redundant-casting,
-readability-redundant-inline-specifier,
-readability-redundant-lambda-parameter-list,
-readability-redundant-member-init,
-readability-redundant-nested-if,
-readability-redundant-parentheses,
-readability-redundant-typename,
-readability-trailing-comma,
-readability-trivial-switch,
-readability-uppercase-literal-suffix,
-readability-use-anyofallof,
-readability-use-std-min-max,
+1 -1
View File
@@ -38,7 +38,7 @@ repos:
- id: yamllint
exclude: ^(\.clang-format|\.clang-tidy)$
- repo: https://github.com/pre-commit/mirrors-clang-format
rev: v22.1.8
rev: v23.1.3
hooks:
- id: clang-format
types_or: [c, c++]
+1 -1
View File
@@ -301,7 +301,7 @@ constexpr uint32_t VARINT_MAX_1_BYTE = 1 << 7; // 128
constexpr uint32_t VARINT_MAX_2_BYTE = 1 << 14; // 16384
/// Generated encode body: writes the fields at pos, returns the cursor past them.
using ProtoEncodeFn = uint8_t *(*) (const void *, uint8_t *PROTO_ENCODE_DEBUG_PARAM);
using ProtoEncodeFn = uint8_t *(*)(const void *, uint8_t *PROTO_ENCODE_DEBUG_PARAM);
/// Static encode helpers for the generated encode bodies. Each takes the write cursor by value and
/// returns it advanced, so outlined calls at -Os chain through the return register instead of a
+4 -5
View File
@@ -111,10 +111,9 @@ static const uint8_t BL0906_USR_WRPROT = 0x9E;
// Reset Register
static const uint8_t BL0906_SOFT_RESET = 0x9F;
const uint8_t BL0906_INIT[2][6] = {
// Reset to default
{BL0906_WRITE_COMMAND, BL0906_SOFT_RESET, 0x5A, 0x5A, 0x5A, 0x52},
// Enable User Operation Write
{BL0906_WRITE_COMMAND, BL0906_USR_WRPROT, 0x55, 0x55, 0x00, 0xB7}};
const uint8_t BL0906_INIT[2][6] = {// Reset to default
{BL0906_WRITE_COMMAND, BL0906_SOFT_RESET, 0x5A, 0x5A, 0x5A, 0x52},
// Enable User Operation Write
{BL0906_WRITE_COMMAND, BL0906_USR_WRPROT, 0x55, 0x55, 0x00, 0xB7}};
} // namespace esphome::bl0906
@@ -0,0 +1,12 @@
#include "epaper_goodisplay_3c.h"
namespace esphome::epaper_spi {
void EPaperGoodDisplay3C::power_on() {}
void EPaperGoodDisplay3C::refresh_screen(bool partial) {
EPaperWeAct3C::refresh_screen(partial);
this->next_delay_ = REFRESH_START_MS;
}
} // namespace esphome::epaper_spi
@@ -0,0 +1,28 @@
#pragma once
#include "epaper_weact_3c.h"
namespace esphome::epaper_spi {
/**
* Good Display 3-color e-paper displays (SSD1680 controller), e.g. GDEY029Z95.
*
* Driven as EPaperWeAct3C, except that there is no separate power-on: the full refresh (0xF7) powers the panel
* up itself. Busy then only goes high a little after the refresh starts, so the next state waits before
* checking it.
*/
class EPaperGoodDisplay3C : public EPaperWeAct3C {
public:
using EPaperWeAct3C::EPaperWeAct3C;
protected:
// Time from master activation (0x20) until busy can be relied on
static constexpr uint16_t REFRESH_START_MS = 50;
// With EPaperWeAct3C's power-on (0x22 0xF8) first, this panel's refresh ends almost at once and the image
// does not change.
void power_on() override;
void refresh_screen(bool partial) override;
};
} // namespace esphome::epaper_spi
@@ -4,11 +4,17 @@ Supported models:
- weact-2.13in-3c: 122x250 pixels (2.13" display)
- weact-2.9in-3c: 128x296 pixels (2.9" display)
- weact-4.2in-3c: 400x300 pixels (4.2" display)
- goodisplay-gdey029z95-2.9: 128x296 pixels (Good Display GDEY029Z95 2.9" display)
These displays use SSD1680 or SSD1683 controller and require a specific initialization
sequence. The DRV_OUT_CTL command is calculated from the display height.
The Good Display panel is driven as the WeAct ones, except that it must not get the
separate power-on before the refresh, so it has its own driver class, EPaperGoodDisplay3C.
"""
import esphome.config_validation as cv
from esphome.const import CONF_BUSY_PIN, CONF_RESET_PIN
from . import EpaperModel
@@ -17,8 +23,10 @@ class WeActBWR(EpaperModel):
source_files = ("epaper_weact_3c.cpp",)
def __init__(self, name, **defaults):
super().__init__(name, "EPaperWeAct3C", **defaults)
def __init__(
self, name: str, class_name: str = "EPaperWeAct3C", **defaults: object
) -> None:
super().__init__(name, class_name, **defaults)
def get_init_sequence(self, config):
"""Generate initialization sequence for WeAct BWR displays.
@@ -75,3 +83,27 @@ weact_4p2in3c = WeActBWR(
data_rate="10MHz",
minimum_update_interval="10s",
)
class GoodDisplayBWR(WeActBWR):
"""Good Display Black/White/Red displays using SSD1680 controller."""
source_files = ("epaper_weact_3c.cpp", "epaper_goodisplay_3c.cpp")
def __init__(self, name: str, **defaults: object) -> None:
super().__init__(name, "EPaperGoodDisplay3C", **defaults)
def option(self, name, fallback=cv.UNDEFINED) -> cv.Optional | cv.Required:
# Only a hardware reset wakes the panel from deep sleep, and refreshes are timed by busy
if name in (CONF_RESET_PIN, CONF_BUSY_PIN):
return cv.Required(name)
return super().option(name, fallback)
# Model: Good Display GDEY029Z95 2.9" BWR - 128x296 pixels, SSD1680 controller
GoodDisplayBWR(
"goodisplay-gdey029z95-2.9",
width=128,
height=296,
minimum_update_interval="10s",
)
+106 -63
View File
@@ -9,12 +9,27 @@
#include "esphome/core/application.h"
#include "esphome/core/helpers.h"
#include "esphome/core/progmem.h"
namespace esphome::ld2410 {
ESPHOME_LOG_TAG(TAG, "ld2410");
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 DistanceResolution : uint8_t {
DISTANCE_RESOLUTION_0_2 = 0x01,
DISTANCE_RESOLUTION_0_75 = 0x00,
};
enum LightFunction : uint8_t {
LIGHT_FUNCTION_OFF = 0x00,
LIGHT_FUNCTION_BELOW = 0x01,
@@ -54,17 +69,73 @@ enum AckData : uint8_t {
COMMAND_STATUS = 7,
};
// 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");
// Same order as DISTANCE_RESOLUTIONS
PROGMEM_STRING_TABLE(DistanceResolutionStrings, "0.2m", "0.75m");
// The light function and out pin level select options are in enum order
static_assert(LIGHT_FUNCTION_OFF == 0 && LIGHT_FUNCTION_BELOW == 1 && LIGHT_FUNCTION_ABOVE == 2,
"LightFunction is the select option index");
static_assert(OUT_PIN_LEVEL_LOW == 0 && OUT_PIN_LEVEL_HIGH == 1, "OutPinLevel is the select option index");
// Memory-efficient lookup tables
struct StringToUint8 {
const char *str;
const uint8_t value;
};
static constexpr uint32_t BAUD_RATES[] PROGMEM = {9600, 19200, 38400, 57600, 115200, 230400, 256000, 460800};
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 StringToUint8 DISTANCE_RESOLUTIONS_BY_STR[] = {
{"0.2m", DISTANCE_RESOLUTION_0_2},
{"0.75m", DISTANCE_RESOLUTION_0_75},
};
constexpr Uint8ToString DISTANCE_RESOLUTIONS_BY_UINT[] = {
{DISTANCE_RESOLUTION_0_2, "0.2m"},
{DISTANCE_RESOLUTION_0_75, "0.75m"},
};
constexpr StringToUint8 LIGHT_FUNCTIONS_BY_STR[] = {
{"off", LIGHT_FUNCTION_OFF},
{"below", LIGHT_FUNCTION_BELOW},
{"above", LIGHT_FUNCTION_ABOVE},
};
constexpr Uint8ToString LIGHT_FUNCTIONS_BY_UINT[] = {
{LIGHT_FUNCTION_OFF, "off"},
{LIGHT_FUNCTION_BELOW, "below"},
{LIGHT_FUNCTION_ABOVE, "above"},
};
constexpr StringToUint8 OUT_PIN_LEVELS_BY_STR[] = {
{"low", OUT_PIN_LEVEL_LOW},
{"high", OUT_PIN_LEVEL_HIGH},
};
constexpr Uint8ToString OUT_PIN_LEVELS_BY_UINT[] = {
{OUT_PIN_LEVEL_LOW, "low"},
{OUT_PIN_LEVEL_HIGH, "high"},
};
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 char *str) {
for (const auto &entry : arr) {
if (strcmp(str, entry.str) == 0)
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
}
// Commands
static constexpr uint8_t CMD_ENABLE_CONF = 0xFF;
@@ -336,12 +407,10 @@ void LD2410Component::handle_periodic_data_() {
}
#ifdef USE_NUMBER
std::function<void()> set_number_value(number::Number *n, float value) {
void set_number_value(number::Number *n, float value) {
if (n != nullptr && (!n->has_state() || n->state != value)) {
n->state = value;
return [n, value]() { n->publish_state(value); };
n->publish_state(value);
}
return []() {};
}
#endif
@@ -398,16 +467,11 @@ bool LD2410Component::handle_ack_data_() {
}
case CMD_QUERY_DISTANCE_RESOLUTION: {
const auto index = ld24xx::find_index(DISTANCE_RESOLUTIONS, this->buffer_data_[10]);
if (!index) {
ESP_LOGW(TAG, "Unknown distance resolution %u", this->buffer_data_[10]);
break;
}
ESP_LOGV(TAG, "Distance resolution: %s",
LOG_STR_ARG(DistanceResolutionStrings::get_log_str(*index, DistanceResolutionStrings::LAST_INDEX)));
const auto *distance_resolution = find_str(DISTANCE_RESOLUTIONS_BY_UINT, this->buffer_data_[10]);
ESP_LOGV(TAG, "Distance resolution: %s", distance_resolution);
#ifdef USE_SELECT
if (this->distance_resolution_select_ != nullptr) {
this->distance_resolution_select_->publish_state(*index);
this->distance_resolution_select_->publish_state(distance_resolution);
}
#endif
break;
@@ -417,14 +481,16 @@ bool LD2410Component::handle_ack_data_() {
this->light_function_ = this->buffer_data_[10];
this->light_threshold_ = this->buffer_data_[11];
this->out_pin_level_ = this->buffer_data_[12];
ESP_LOGV(TAG, "Light function: %u, threshold: %u, out pin level: %u", this->light_function_,
this->light_threshold_, this->out_pin_level_);
const auto *light_function_str = find_str(LIGHT_FUNCTIONS_BY_UINT, this->light_function_);
const auto *out_pin_level_str = find_str(OUT_PIN_LEVELS_BY_UINT, this->out_pin_level_);
ESP_LOGV(TAG, "Light function: %s, threshold: %u, out pin level: %s", light_function_str, this->light_threshold_,
out_pin_level_str);
#ifdef USE_SELECT
if (this->light_function_select_ != nullptr) {
this->light_function_select_->publish_state(static_cast<size_t>(this->light_function_));
this->light_function_select_->publish_state(light_function_str);
}
if (this->out_pin_level_select_ != nullptr) {
this->out_pin_level_select_->publish_state(static_cast<size_t>(this->out_pin_level_));
this->out_pin_level_select_->publish_state(out_pin_level_str);
}
#endif
#ifdef USE_NUMBER
@@ -492,29 +558,24 @@ bool LD2410Component::handle_ack_data_() {
Moving distance range: 13th byte
Still distance range: 14th byte
*/
std::vector<std::function<void()>> updates;
updates.push_back(set_number_value(this->max_move_distance_gate_number_, this->buffer_data_[12]));
updates.push_back(set_number_value(this->max_still_distance_gate_number_, this->buffer_data_[13]));
set_number_value(this->max_move_distance_gate_number_, this->buffer_data_[12]);
set_number_value(this->max_still_distance_gate_number_, this->buffer_data_[13]);
/*
Moving Sensitivities: 15~23th bytes
*/
for (std::vector<number::Number *>::size_type i = 0; i != this->gate_move_threshold_numbers_.size(); i++) {
updates.push_back(set_number_value(this->gate_move_threshold_numbers_[i], this->buffer_data_[14 + i]));
for (size_t i = 0; i < TOTAL_GATES; i++) {
set_number_value(this->gate_move_threshold_numbers_[i], this->buffer_data_[14 + i]);
}
/*
Still Sensitivities: 24~32th bytes
*/
for (std::vector<number::Number *>::size_type i = 0; i != this->gate_still_threshold_numbers_.size(); i++) {
updates.push_back(set_number_value(this->gate_still_threshold_numbers_[i], this->buffer_data_[23 + i]));
for (size_t i = 0; i < TOTAL_GATES; i++) {
set_number_value(this->gate_still_threshold_numbers_[i], this->buffer_data_[23 + i]);
}
/*
None Duration: 33~34th bytes
*/
updates.push_back(
set_number_value(this->timeout_number_, encode_uint16(this->buffer_data_[33], this->buffer_data_[32])));
for (auto &update : updates) {
update();
}
set_number_value(this->timeout_number_, encode_uint16(this->buffer_data_[33], this->buffer_data_[32]));
#endif
break;
}
@@ -570,29 +631,15 @@ void LD2410Component::set_bluetooth(bool enable) {
}
void LD2410Component::set_distance_resolution(const char *state) {
const int16_t index = progmem_string_table_find<DistanceResolutionStrings>(state, strlen(state));
if (index >= 0) {
this->set_distance_resolution(static_cast<DistanceResolution>(progmem_read_byte(&DISTANCE_RESOLUTIONS[index])));
}
}
void LD2410Component::set_distance_resolution(DistanceResolution resolution) {
this->set_config_mode_(true);
const uint8_t cmd_value[2] = {resolution, 0x00};
const uint8_t cmd_value[2] = {find_uint8(DISTANCE_RESOLUTIONS_BY_STR, state), 0x00};
this->send_command_(CMD_SET_DISTANCE_RESOLUTION, cmd_value, sizeof(cmd_value));
this->set_timeout(200, [this]() { this->restart_and_read_all_info(); });
}
void LD2410Component::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 LD2410Component::set_baud_rate(BaudRate rate) {
this->set_config_mode_(true);
const uint8_t cmd_value[2] = {rate, 0x00};
const uint8_t cmd_value[2] = {find_uint8(BAUD_RATES_BY_STR, state), 0x00};
this->send_command_(CMD_SET_BAUD_RATE, cmd_value, sizeof(cmd_value));
this->set_timeout(200, [this]() { this->restart_(); });
}
@@ -715,15 +762,11 @@ void LD2410Component::set_light_out_control() {
}
#endif
#ifdef USE_SELECT
if (this->light_function_select_ != nullptr) {
if (const auto index = this->light_function_select_->active_index()) {
this->light_function_ = *index;
}
if (this->light_function_select_ != nullptr && this->light_function_select_->has_state()) {
this->light_function_ = find_uint8(LIGHT_FUNCTIONS_BY_STR, this->light_function_select_->current_option().c_str());
}
if (this->out_pin_level_select_ != nullptr) {
if (const auto index = this->out_pin_level_select_->active_index()) {
this->out_pin_level_ = *index;
}
if (this->out_pin_level_select_ != nullptr && this->out_pin_level_select_->has_state()) {
this->out_pin_level_ = find_uint8(OUT_PIN_LEVELS_BY_STR, this->out_pin_level_select_->current_option().c_str());
}
#endif
this->set_config_mode_(true);
-21
View File
@@ -37,25 +37,6 @@ using namespace ld24xx;
static constexpr uint8_t MAX_LINE_LENGTH = 50;
static constexpr uint8_t TOTAL_GATES = 9; // Total number of gates supported by the LD2410
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 DistanceResolution : uint8_t {
DISTANCE_RESOLUTION_0_2 = 0x01,
DISTANCE_RESOLUTION_0_75 = 0x00,
};
// Device values of the distance resolution select options, in option order; in flash on ESP8266
static constexpr uint8_t DISTANCE_RESOLUTIONS[] PROGMEM = {DISTANCE_RESOLUTION_0_2, DISTANCE_RESOLUTION_0_75};
class LD2410Component final : public Component, public uart::UARTDevice {
#ifdef USE_BINARY_SENSOR
SUB_BINARY_SENSOR(out_pin_presence_status)
@@ -118,9 +99,7 @@ class LD2410Component final : public Component, public uart::UARTDevice {
void restart_and_read_all_info();
void set_bluetooth(bool enable);
void set_distance_resolution(const char *state);
void set_distance_resolution(DistanceResolution resolution);
void set_baud_rate(const char *state);
void set_baud_rate(BaudRate rate);
void factory_reset();
protected:
@@ -52,7 +52,6 @@ CONFIG_SCHEMA = {
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_LD2410_ID])
selects = select.sub_selects(config, parent=hub)
# Option lists follow the order of the C++ enums and tables; the hub maps by index
await selects(
CONF_DISTANCE_RESOLUTION,
hub.set_distance_resolution_select,
@@ -4,7 +4,7 @@ namespace esphome::ld2410 {
void BaudRateSelect::control(size_t index) {
this->publish_state(index);
this->parent_->set_baud_rate(static_cast<BaudRate>(index + 1));
this->parent_->set_baud_rate(this->option_at(index));
}
} // namespace esphome::ld2410
@@ -4,8 +4,7 @@ namespace esphome::ld2410 {
void DistanceResolutionSelect::control(size_t index) {
this->publish_state(index);
this->parent_->set_distance_resolution(
static_cast<DistanceResolution>(progmem_read_byte(&DISTANCE_RESOLUTIONS[index])));
this->parent_->set_distance_resolution(this->option_at(index));
}
} // namespace esphome::ld2410
+37 -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,12 @@ 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));
char direction_str[DirectionStrings::MAX_LENGTH + 1];
ESPHOME_strncpy_P(
direction_str,
reinterpret_cast<ESPHOME_PGM_P>(DirectionStrings::get_progmem_str(direction, DirectionStrings::LAST_INDEX)),
sizeof(direction_str));
tsd->publish_state(direction_str);
}
}
#endif
@@ -864,8 +812,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 +828,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 +843,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
+2 -2
View File
@@ -244,9 +244,9 @@ class MipiSpi : public display::Display,
char hex_buf[format_hex_pretty_size(MIPI_SPI_MAX_CMD_LOG_BYTES)];
// Don't spam the log after setup
if (this->init_sequence_len_ == 0) {
esph_log_v(TAG, "Command %02X, length %d, bytes %s", cmd, len, format_hex_pretty_to(hex_buf, bytes, len));
esph_log_v(TAG, "Command %02X, length %zu, bytes %s", cmd, len, format_hex_pretty_to(hex_buf, bytes, len));
} else {
esph_log_d(TAG, "Command %02X, length %d, bytes %s", cmd, len, format_hex_pretty_to(hex_buf, bytes, len));
esph_log_d(TAG, "Command %02X, length %zu, bytes %s", cmd, len, format_hex_pretty_to(hex_buf, bytes, len));
}
if constexpr (BUS_TYPE == BUS_TYPE_QUAD) {
this->enable();
+21
View File
@@ -39,13 +39,16 @@ from esphome.const import (
CONF_REFERENCE_TEMPERATURE,
CONF_SEND_EVERY,
CONF_SEND_FIRST_AT,
CONF_SENSOR_ID,
CONF_STATE_CLASS,
CONF_TEMPERATURE,
CONF_TEMPERATURE_OFFSET,
CONF_TIMEOUT,
CONF_TO,
CONF_TRIGGER_ID,
CONF_TYPE_ID,
CONF_UNIT_OF_MEASUREMENT,
CONF_USE_FAHRENHEIT,
CONF_VALUE,
CONF_WEB_SERVER,
CONF_WINDOW_SIZE,
@@ -310,6 +313,7 @@ OrFilter = sensor_ns.class_("OrFilter", Filter)
CalibrateLinearFilter = sensor_ns.class_("CalibrateLinearFilter", Filter)
ToNTCResistanceFilter = sensor_ns.class_("ToNTCResistanceFilter", Filter)
ToNTCTemperatureFilter = sensor_ns.class_("ToNTCTemperatureFilter", Filter)
RHCorrectionFilter = sensor_ns.class_("RHCorrectionFilter", Filter)
CalibratePolynomialFilter = sensor_ns.class_("CalibratePolynomialFilter", Filter)
SensorInRangeCondition = sensor_ns.class_("SensorInRangeCondition", Filter)
ClampFilter = sensor_ns.class_("ClampFilter", Filter)
@@ -1281,6 +1285,23 @@ async def calibrate_ntc_temperature_filter_to_code(config, filter_id):
)
@FILTER_REGISTRY.register(
"rh_correction",
RHCorrectionFilter,
cv.Schema(
{
cv.Required(CONF_TEMPERATURE_OFFSET): cv.templatable(cv.float_),
cv.Required(CONF_SENSOR_ID): cv.use_id(Sensor),
cv.Optional(CONF_USE_FAHRENHEIT, default=False): cv.boolean,
}
),
)
async def rh_correction_filter_to_code(config: ConfigType, filter_id: ID) -> MockObj:
sensor = await cg.get_variable(config[CONF_SENSOR_ID])
offset = await cg.templatable(config[CONF_TEMPERATURE_OFFSET], [], cg.float_)
return cg.new_Pvariable(filter_id, offset, sensor, config[CONF_USE_FAHRENHEIT])
def _mean(xs):
return sum(xs) / len(xs)
+42
View File
@@ -493,6 +493,48 @@ optional<float> ToNTCTemperatureFilter::new_value(float value) {
return temp;
}
// RHCorrectionFilter
static constexpr float MAGNUS_B = 17.67f, MAGNUS_C = 243.5f;
static bool rh_correction_temp_in_band(float t) { return t >= -40.0f && t <= 50.0f; }
optional<float> RHCorrectionFilter::new_value(float value) {
if (!this->temperature_sensor_->has_state()) {
if (!this->warned_) {
ESP_LOGW(TAG, "RHCorrectionFilter(%p): no temperature yet, passing humidity through uncorrected", this);
this->warned_ = true;
}
return value;
}
float offset = this->temperature_offset_.value();
float tc = this->temperature_sensor_->get_state();
if (this->use_fahrenheit_) {
tc = fahrenheit_to_celsius(tc);
offset /= 1.8f;
}
const float tnc = tc - offset;
// A NaN temperature or offset is a faulted input: keep the last corrected value
if (std::isnan(tnc)) {
if (!this->warned_) {
ESP_LOGW(TAG, "RHCorrectionFilter(%p): temperature or offset is NaN, dropping humidity updates", this);
this->warned_ = true;
}
return {};
}
if (!rh_correction_temp_in_band(tc) || !rh_correction_temp_in_band(tnc)) {
if (!this->warned_) {
ESP_LOGW(TAG, "RHCorrectionFilter(%p): Temperature out of range -40..50 °C. Tc=%f Tnc=%f", this, tc, tnc);
this->warned_ = true;
}
return value;
}
this->warned_ = false;
const float rh =
clamp(value * std::exp(MAGNUS_B * MAGNUS_C * (tnc - tc) / ((tnc + MAGNUS_C) * (tc + MAGNUS_C))), 0.0f, 100.0f);
ESP_LOGVV(TAG, "RHCorrectionFilter(%p)::new_value(%f) -> %f", this, value, rh);
return rh;
}
// StreamingFilter (base class)
StreamingFilter::StreamingFilter(uint16_t window_size, uint16_t send_first_at)
: window_size_(window_size), send_first_at_(send_first_at) {}
+20
View File
@@ -647,6 +647,26 @@ class ToNTCTemperatureFilter : public Filter {
double c_;
};
/** RH Temperature correction filter
*
* Allows for Relative Humidity Correction when a temperature offset is applied to the temperature sensor.
* Uses the Magnus formula, rh = rh_nc * exp(b*(t_nc/(c+t_nc) - t/(c+t))) with b = 17.67, c = 243.5 °C;
* see https://en.wikipedia.org/wiki/Dew_point
*/
class RHCorrectionFilter : public Filter {
public:
RHCorrectionFilter(TemplatableFn<float> offset, const Sensor *temperature_sensor, bool use_fahrenheit)
: temperature_offset_(offset), temperature_sensor_(temperature_sensor), use_fahrenheit_(use_fahrenheit) {}
optional<float> new_value(float value) override;
protected:
TemplatableFn<float> temperature_offset_;
const Sensor *temperature_sensor_;
bool use_fahrenheit_;
bool warned_{false};
};
/** Base class for streaming filters (batch windows where window_size == send_every).
*
* When window_size equals send_every, we don't need a sliding window.
+13 -13
View File
@@ -1043,7 +1043,7 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa
if (is_on) {
parse_cstr_param_(
request, ESPHOME_F("effect"), call,
static_cast<light::LightCall &(light::LightCall::*) (const char *, size_t)>(&decltype(call)::set_effect));
static_cast<light::LightCall &(light::LightCall::*)(const char *, size_t)>(&decltype(call)::set_effect));
}
DEFER_ACTION(call, call.perform());
@@ -1404,7 +1404,7 @@ void WebServer::handle_text_request(AsyncWebServerRequest *request, const UrlMat
auto call = obj->make_call();
parse_cstr_param_(
request, ESPHOME_F("value"), call,
static_cast<text::TextCall &(text::TextCall::*) (const char *, size_t)>(&decltype(call)::set_value));
static_cast<text::TextCall &(text::TextCall::*)(const char *, size_t)>(&decltype(call)::set_value));
DEFER_ACTION(call, call.perform());
request->send(200);
@@ -1464,7 +1464,7 @@ void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlM
auto call = obj->make_call();
parse_cstr_param_(
request, ESPHOME_F("option"), call,
static_cast<select::SelectCall &(select::SelectCall::*) (const char *, size_t)>(&decltype(call)::set_option));
static_cast<select::SelectCall &(select::SelectCall::*)(const char *, size_t)>(&decltype(call)::set_option));
DEFER_ACTION(call, call.perform());
request->send(200);
@@ -1522,26 +1522,26 @@ void WebServer::handle_climate_request(AsyncWebServerRequest *request, const Url
// Parse string mode parameters
parse_cstr_param_(
request, ESPHOME_F("mode"), call,
static_cast<climate::ClimateCall &(climate::ClimateCall::*) (const char *, size_t)>(&decltype(call)::set_mode));
static_cast<climate::ClimateCall &(climate::ClimateCall::*)(const char *, size_t)>(&decltype(call)::set_mode));
parse_cstr_param_(request, ESPHOME_F("fan_mode"), call,
static_cast<climate::ClimateCall &(climate::ClimateCall::*) (const char *, size_t)>(
static_cast<climate::ClimateCall &(climate::ClimateCall::*)(const char *, size_t)>(
&decltype(call)::set_fan_mode));
parse_cstr_param_(request, ESPHOME_F("swing_mode"), call,
static_cast<climate::ClimateCall &(climate::ClimateCall::*) (const char *, size_t)>(
static_cast<climate::ClimateCall &(climate::ClimateCall::*)(const char *, size_t)>(
&decltype(call)::set_swing_mode));
parse_cstr_param_(request, ESPHOME_F("preset"), call,
static_cast<climate::ClimateCall &(climate::ClimateCall::*) (const char *, size_t)>(
static_cast<climate::ClimateCall &(climate::ClimateCall::*)(const char *, size_t)>(
&decltype(call)::set_preset));
// Parse temperature parameters
// static_cast needed to disambiguate overloaded setters (float vs optional<float>)
using ClimateCall = decltype(call);
parse_num_param_(request, ESPHOME_F("target_temperature_high"), call,
static_cast<ClimateCall &(ClimateCall::*) (float)>(&ClimateCall::set_target_temperature_high));
static_cast<ClimateCall &(ClimateCall::*)(float)>(&ClimateCall::set_target_temperature_high));
parse_num_param_(request, ESPHOME_F("target_temperature_low"), call,
static_cast<ClimateCall &(ClimateCall::*) (float)>(&ClimateCall::set_target_temperature_low));
static_cast<ClimateCall &(ClimateCall::*)(float)>(&ClimateCall::set_target_temperature_low));
parse_num_param_(request, ESPHOME_F("target_temperature"), call,
static_cast<ClimateCall &(ClimateCall::*) (float)>(&ClimateCall::set_target_temperature));
static_cast<ClimateCall &(ClimateCall::*)(float)>(&ClimateCall::set_target_temperature));
DEFER_ACTION(call, call.perform());
request->send(200);
@@ -1818,8 +1818,8 @@ void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *reques
auto call = obj->make_call();
parse_cstr_param_(
request, ESPHOME_F("code"), call,
static_cast<alarm_control_panel::AlarmControlPanelCall &(
alarm_control_panel::AlarmControlPanelCall::*) (const char *, size_t)>(&decltype(call)::set_code));
static_cast<alarm_control_panel::AlarmControlPanelCall &(alarm_control_panel::AlarmControlPanelCall::*)(
const char *, size_t)>(&decltype(call)::set_code));
static constexpr ActionEntry<alarm_control_panel::AlarmControlPanelCall> ACTIONS[] PROGMEM = {
{"disarm", &alarm_control_panel::AlarmControlPanelCall::disarm},
@@ -1893,7 +1893,7 @@ void WebServer::handle_water_heater_request(AsyncWebServerRequest *request, cons
// Parse mode parameter
parse_cstr_param_(
request, ESPHOME_F("mode"), base_call,
static_cast<water_heater::WaterHeaterCall &(water_heater::WaterHeaterCall::*) (const char *, size_t)>(
static_cast<water_heater::WaterHeaterCall &(water_heater::WaterHeaterCall::*)(const char *, size_t)>(
&water_heater::WaterHeaterCall::set_mode));
// Parse temperature parameters
@@ -5,7 +5,7 @@ namespace esphome::xiaomi_hhccpot002 {
ESPHOME_LOG_TAG(TAG, "xiaomi_hhccpot002");
void XiaomiHHCCPOT002 ::dump_config() {
void XiaomiHHCCPOT002::dump_config() {
ESP_LOGCONFIG(TAG, "Xiaomi HHCCPOT002");
LOG_SENSOR(" ", "Moisture", this->moisture_);
LOG_SENSOR(" ", "Conductivity", this->conductivity_);
+4 -2
View File
@@ -59,13 +59,15 @@ template<typename T> class PlainAtomic {
constexpr PlainAtomic(T value) : value_(value) {}
T load(std::memory_order order = std::memory_order_seq_cst) const {
T value = value_;
if (order != std::memory_order_relaxed)
if (order != std::memory_order_relaxed) {
ESPHOME_LFQ_COMPILER_BARRIER(); // acquire: later reads may not hoist above this load
}
return value;
}
void store(T value, std::memory_order order = std::memory_order_seq_cst) {
if (order != std::memory_order_relaxed)
if (order != std::memory_order_relaxed) {
ESPHOME_LFQ_COMPILER_BARRIER(); // release: earlier writes may not sink below this store
}
value_ = value;
}
T fetch_add(T amount, std::memory_order /*order*/ = std::memory_order_seq_cst) {
+1 -1
View File
@@ -12,7 +12,7 @@ pyserial==3.5
platformio==6.2.0
esptool==5.6.0
click==8.5.0
aioesphomeapi==46.18.0
aioesphomeapi==46.19.0
aiohappyeyeballs==2.7.1 # Happy Eyeballs for requests downloads; already pulled in by aioesphomeapi
zeroconf==0.151.5
puremagic==2.2.0
+2 -2
View File
@@ -1,4 +1,4 @@
# Useful stuff when working in a development environment
clang-format==22.1.8 # .pre-commit-config.yaml rev synced by script/sync_dependency_versions.py
clang-tidy==22.1.8
clang-format==23.1.3 # .pre-commit-config.yaml rev synced by script/sync_dependency_versions.py
clang-tidy==23.1.3
yamllint==1.38.0 # .pre-commit-config.yaml rev synced by script/sync_dependency_versions.py
+8 -14
View File
@@ -319,21 +319,15 @@ def run_tidy(
disabled_checks = []
if args.environment.startswith(("rp2", "bk72xx", "ln882h", "rtl87xx")):
# MMIO peripheral access on these bare-metal platforms is all
# fixed-address.
disabled_checks.append("-clang-analyzer-core.FixedAddressDereference")
# fixed-address. clang-tidy 23 moved the checker under optin.
disabled_checks += [
"-clang-analyzer-core.FixedAddressDereference",
"-clang-analyzer-optin.core.FixedAddressDereference",
]
if args.environment.startswith("host"):
# glibc's assert() expands to a call the check mistakes for a
# constant condition.
disabled_checks += ["-misc-static-assert", "-cert-dcl03-c"]
if args.environment.startswith(("rp2", "bk72xx", "ln882h", "rtl87xx", "host")):
# bugprone-pointer-arithmetic-on-polymorphic-object (and its
# cert-ctr56-cpp alias) crashes clang-tidy 22 with infinite matcher
# recursion on lvgl_esphome.h under the RP2 defines and on the
# all-include file under the host defines.
disabled_checks += [
"-bugprone-pointer-arithmetic-on-polymorphic-object",
"-cert-ctr56-cpp",
]
if os.path.normpath(path) in ANALYZER_SKIPPED_FILES:
disabled_checks.append("-clang-analyzer-*")
if disabled_checks:
@@ -523,7 +517,7 @@ def main():
failed_files = []
timings = []
try:
executable = get_binary("clang-tidy", 22)
executable = get_binary("clang-tidy", 23)
task_queue = queue.Queue(args.jobs)
lock = threading.Lock()
for _ in range(args.jobs):
@@ -571,13 +565,13 @@ def main():
try:
try:
subprocess.call(
["clang-apply-replacements-22", tmpdir], close_fds=False
["clang-apply-replacements-23", tmpdir], close_fds=False
)
except FileNotFoundError:
subprocess.call(["clang-apply-replacements", tmpdir], close_fds=False)
except FileNotFoundError:
print(
"Error please install clang-apply-replacements-22 or clang-apply-replacements.\n",
"Error please install clang-apply-replacements-23 or clang-apply-replacements.\n",
file=sys.stderr,
)
except:
@@ -621,6 +621,69 @@ def test_uc8279_width_must_be_a_multiple_of_8(
)
GDEY029Z95 = "goodisplay-gdey029z95-2.9"
def _gdey029z95_config(**overrides: Any) -> ConfigType:
config: ConfigType = {
"id": "test_display",
"model": GDEY029Z95,
CONF_DC_PIN: 21,
CONF_RESET_PIN: 22,
CONF_BUSY_PIN: 23,
}
config.update(overrides)
return config
def test_gdey029z95_uses_the_good_display_driver() -> None:
"""The GDEY029Z95 uses EPaperGoodDisplay3C with the WeAct init sequence."""
model = MODELS[GDEY029Z95.upper()]
assert model.class_name == "EPaperGoodDisplay3C"
assert model.get_init_sequence({}) == (
(0x01, 0x27, 0x01, 0x00),
(0x11, 0x03),
(0x3C, 0x05),
(0x18, 0x80),
(0x21, 0x00, 0x80),
)
@pytest.mark.parametrize("pin", [CONF_RESET_PIN, CONF_BUSY_PIN])
def test_gdey029z95_requires_reset_and_busy_pins(
set_core_config: SetCoreConfigCallable,
set_component_config: Callable[[str, Any], None],
pin: str,
) -> None:
"""Only a hardware reset wakes the panel from deep sleep, and refreshes are timed by busy."""
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
)
set_component_config("spi", {"id": "spi_bus", "clk_pin": 18, "mosi_pin": 19})
config = _gdey029z95_config()
del config[pin]
with pytest.raises(cv.Invalid, match=pin):
run_schema_validation(config)
def test_gdey029z95_has_no_partial_update(
set_core_config: SetCoreConfigCallable,
set_component_config: Callable[[str, Any], None],
) -> None:
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
)
set_component_config("spi", {"id": "spi_bus", "clk_pin": 18, "mosi_pin": 19})
run_schema_validation(_gdey029z95_config())
with pytest.raises(cv.Invalid, match="does not support partial update"):
run_schema_validation(_gdey029z95_config(full_update_every=2))
def test_uc8279_x4pro_code_generation(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
@@ -0,0 +1,24 @@
esphome:
name: test
esp32:
board: esp32dev
sensor:
- platform: template
id: temperature
filters:
- offset: -7.3
- platform: template
id: constant_offset
filters:
- rh_correction:
sensor_id: temperature
temperature_offset: -7.3
- platform: template
id: templated_offset
filters:
- rh_correction:
sensor_id: temperature
temperature_offset: !lambda return -7.3;
use_fahrenheit: true
@@ -0,0 +1,60 @@
"""The rh_correction filter: schema and the constructor call it generates."""
from collections.abc import Callable
from pathlib import Path
import re
import pytest
from esphome.components.sensor import validate_filters
import esphome.config_validation as cv
@pytest.mark.parametrize(
"filters",
[
pytest.param([{"rh_correction": {"sensor_id": "t"}}], id="no_offset"),
pytest.param([{"rh_correction": {"temperature_offset": -7.3}}], id="no_sensor"),
pytest.param(
[
{
"rh_correction": {
"sensor_id": "t",
"temperature_offset": -7.3,
"use_fahrenheit": "maybe",
}
}
],
id="bad_fahrenheit",
),
],
)
def test_rh_correction_rejects_incomplete_config(filters: list[dict]) -> None:
with pytest.raises(cv.Invalid):
validate_filters(filters)
def test_rh_correction_defaults_to_celsius() -> None:
config = validate_filters(
[{"rh_correction": {"sensor_id": "t", "temperature_offset": -7.3}}]
)
assert config[0]["rh_correction"]["use_fahrenheit"] is False
def test_rh_correction_codegen(
generate_main: Callable[[str | Path], str],
) -> None:
main_cpp = generate_main("tests/component_tests/sensor/rh_correction.yaml")
# A constant offset is wrapped in a stateless lambda so the filter stores a function pointer
assert re.search(
r"sensor::RHCorrectionFilter\(\[\]\(\) -> float \{\s*return -7\.3f;\s*\}, "
r"temperature, false\);\s*constant_offset->add_filter\(",
main_cpp,
)
# A templated offset keeps the user's lambda body and passes use_fahrenheit through
assert re.search(
r"sensor::RHCorrectionFilter\(\[\]\(\) -> float \{[^}]*return -7\.3;[^}]*\}, "
r"temperature, true\);\s*templated_offset->add_filter\(",
main_cpp,
)
@@ -0,0 +1,164 @@
#include <gtest/gtest.h>
#include <utility>
#include <vector>
#include "../common.h"
#include "esphome/components/epaper_spi/epaper_goodisplay_3c.h"
namespace esphome::epaper_spi::testing {
namespace {
class TestableGoodDisplay3C : public EPaperGoodDisplay3C {
public:
TestableGoodDisplay3C(uint16_t width, uint16_t height) : EPaperGoodDisplay3C("test", width, height, nullptr, 0) {}
void install(spi::SPIDelegate *delegate) {
this->delegate_ = delegate;
this->set_dc_pin(&this->dc);
ASSERT_TRUE(this->init_buffer_(this->buffer_length_));
}
void draw(int x, int y, Color color) { this->draw_pixel_at(x, y, color); }
/// Both planes of one push; returns how many calls it took.
int run_push() {
int calls = 1;
while (!this->transfer_data())
calls++;
return calls;
}
/// A whole update with the frame already drawn, run through loop() as the main loop would.
void run_update() {
this->set_auto_clear(false);
this->state_ = EPaperState::UPDATE;
const uint32_t deadline = millis() + 2000;
while (this->state_ != EPaperState::IDLE) {
ASSERT_LT(millis(), deadline) << "update did not finish";
this->loop();
}
}
using EPaperGoodDisplay3C::REFRESH_START_MS;
RecordingPin dc;
};
/// Also notes when each command byte was sent.
class TimedCommandDelegate : public RecordingDelegate {
public:
using RecordingDelegate::RecordingDelegate;
void write_array(const uint8_t *ptr, size_t length) override {
if (!this->dc_->level) {
for (size_t i = 0; i != length; i++)
this->sent.emplace_back(ptr[i], millis());
}
RecordingDelegate::write_array(ptr, length);
}
/// When the n-th (from 0) copy of a command was sent.
uint32_t time_of(uint8_t command, int n = 0) const {
for (const auto &[byte, time] : this->sent) {
if (byte == command && n-- == 0)
return time;
}
ADD_FAILURE() << "command " << int(command) << " not sent";
return 0;
}
std::vector<std::pair<uint8_t, uint32_t>> sent;
};
using Bytes = std::vector<uint8_t>;
const Color RED(255, 0, 0);
/// One row of 16 pixels: white, black, red, then white. The red plane goes first as 1=red, then the black/white
/// plane as 1=white with red pixels sent as 0.
TEST(EPaperGoodDisplay3CTransfer, PixelsReachThePanel) {
TestableGoodDisplay3C display(16, 1);
RecordingDelegate bus(&display.dc);
display.install(&bus);
display.fill(Color::WHITE);
display.draw(1, 0, Color::BLACK);
display.draw(2, 0, RED);
EXPECT_EQ(display.run_push(), 1);
EXPECT_EQ(bus.commands, (Bytes{0x44, 0x45, 0x4E, 0x4F, 0x26, 0x24}));
EXPECT_EQ(bus.data[0x44], (Bytes{0x00, 0x01}));
EXPECT_EQ(bus.data[0x45], (Bytes{0x00, 0x00, 0x00, 0x00}));
EXPECT_EQ(bus.data[0x24], (Bytes{0x9F, 0xFF}));
EXPECT_EQ(bus.data[0x26], (Bytes{0x20, 0x00}));
}
/// A push that yields to the main loop after every chunk sends the same commands and bytes as one that runs to
/// completion in one call, and a second push starts again from the window setup.
TEST(EPaperGoodDisplay3CTransfer, ResumedPushMatches) {
// 128x20 pixels is 320 bytes per plane, so each plane takes three chunks of up to MAX_TRANSFER_SIZE.
constexpr int width = 128;
constexpr int height = 20;
const Color colors[] = {Color::WHITE, Color::BLACK, RED};
TestableGoodDisplay3C reference(width, height);
RecordingDelegate reference_bus(&reference.dc);
reference.install(&reference_bus);
TestableGoodDisplay3C display(width, height);
// Every data write takes longer than MAX_TRANSFER_TIME, so the push yields after each chunk.
RecordingDelegate bus(&display.dc, MAX_TRANSFER_TIME + 1);
display.install(&bus);
for (int y = 0; y != height; y++) {
for (int x = 0; x != width; x++) {
reference.draw(x, y, colors[(x * 7 + y * 3) % 3]);
display.draw(x, y, colors[(x * 7 + y * 3) % 3]);
}
}
EXPECT_EQ(reference.run_push(), 1);
EXPECT_GT(display.run_push(), 2);
EXPECT_EQ(bus.commands, reference_bus.commands);
EXPECT_EQ(bus.data[0x24].size(), 320u);
EXPECT_EQ(bus.data[0x24], reference_bus.data[0x24]);
EXPECT_EQ(bus.data[0x26], reference_bus.data[0x26]);
bus.clear();
display.run_push();
EXPECT_EQ(bus.commands, reference_bus.commands);
}
/// A whole update sends no power-on (0x22 0xF8) before the refresh (0x22 0xF7): with it, this panel does not
/// refresh. Power off (0x83) and deep sleep follow.
TEST(EPaperGoodDisplay3CTransfer, UpdateHasNoSeparatePowerOn) {
TestableGoodDisplay3C display(16, 1);
RecordingDelegate bus(&display.dc);
display.install(&bus);
display.fill(RED);
display.run_update();
EXPECT_EQ(bus.commands, (Bytes{0x44, 0x45, 0x4E, 0x4F, 0x26, 0x24, 0x4E, 0x4F, 0x22, 0x20, 0x22, 0x20, 0x10}));
EXPECT_EQ(bus.data[0x26], (Bytes{0xFF, 0xFF}));
EXPECT_EQ(bus.data[0x24], (Bytes{0x00, 0x00}));
EXPECT_EQ(bus.data[0x22], (Bytes{0xF7, 0x83}));
EXPECT_EQ(bus.data[0x10], (Bytes{0x01}));
}
/// Busy only goes high a little after master activation (0x20), so power off is not sent until REFRESH_START_MS
/// after the refresh starts.
TEST(EPaperGoodDisplay3CTransfer, WaitsForBusyAfterTheRefreshStarts) {
TestableGoodDisplay3C display(16, 1);
TimedCommandDelegate bus(&display.dc);
display.install(&bus);
display.fill(RED);
display.run_update();
// The first 0x20 starts the refresh; the second 0x22 selects power off.
EXPECT_GE(bus.time_of(0x22, 1) - bus.time_of(0x20, 0), TestableGoodDisplay3C::REFRESH_START_MS);
}
} // namespace
} // namespace esphome::epaper_spi::testing
@@ -209,6 +209,27 @@ display:
it.circle(it.get_width() / 2, it.get_height() / 2, 20, Color::BLACK);
it.circle(it.get_width() / 2, it.get_height() / 2, 15, Color(255, 0, 0));
# Good Display GDEY029Z95 2.9" 3-color e-paper (128x296, SSD1680)
- platform: epaper_spi
spi_id: spi_bus
model: goodisplay-gdey029z95-2.9
cs_pin:
allow_other_uses: true
number: GPIO5
dc_pin:
allow_other_uses: true
number: GPIO17
reset_pin:
allow_other_uses: true
number: GPIO16
busy_pin:
allow_other_uses: true
number: GPIO4
lambda: |-
it.filled_rectangle(0, 0, it.get_width(), it.get_height(), Color::WHITE);
it.circle(it.get_width() / 2, it.get_height() / 2, 20, Color::BLACK);
it.circle(it.get_width() / 2, it.get_height() / 2, 15, Color(255, 0, 0));
# WeAct 4.2" 3-color e-paper (400x300, SSD1683)
- platform: epaper_spi
spi_id: spi_bus
+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
+11
View File
@@ -0,0 +1,11 @@
import esphome.codegen as cg
from esphome.types import ConfigType
from tests.testing_helpers import ComponentManifestOverride
def override_manifest(manifest: ComponentManifestOverride) -> None:
async def to_code_testing(config: ConfigType) -> None:
# filter.cpp only compiles with the define the real to_code emits for configured filters
cg.add_define("USE_SENSOR_FILTER")
manifest.to_code = to_code_testing
+18
View File
@@ -254,3 +254,21 @@ sensor:
lambda: return 42.0;
update_interval: 1s
state_class: "measurement_angle"
# Testing rh_correction filter
- platform: copy
source_id: source_sensor
name: "RH Correction Filter Fahrenheit"
filters:
- rh_correction:
sensor_id: source_sensor
temperature_offset: -10.0
use_fahrenheit: true
- platform: copy
source_id: source_sensor
name: "RH Correction Filter Template"
filters:
- rh_correction:
sensor_id: source_sensor
temperature_offset: !lambda return -10.0;
+66
View File
@@ -0,0 +1,66 @@
#include <gtest/gtest.h>
#include <cmath>
#include "esphome/components/sensor/filter.h"
#include "esphome/components/sensor/sensor.h"
namespace esphome::sensor::testing {
// 40 % read 7.3 °C warm at a true 25 °C, in single precision Magnus arithmetic
static constexpr float CORRECTED = 61.13f;
struct RhFixture {
Sensor temperature;
RHCorrectionFilter filter;
RhFixture(float (*offset)(), bool use_fahrenheit = false) : filter(offset, &this->temperature, use_fahrenheit) {}
};
TEST(RHCorrectionFilter, CorrectsForTheOffset) {
RhFixture f([]() -> float { return -7.3f; });
f.temperature.publish_state(25.0f);
EXPECT_NEAR(f.filter.new_value(40.0f).value_or(NAN), CORRECTED, 0.05f);
}
TEST(RHCorrectionFilter, PositiveOffsetLowersTheHumidity) {
RhFixture f([]() -> float { return 7.3f; });
f.temperature.publish_state(25.0f);
EXPECT_LT(f.filter.new_value(40.0f).value_or(NAN), 40.0f);
}
TEST(RHCorrectionFilter, FahrenheitReadingsGiveTheSameResult) {
// 77 °F is 25 °C and -13.14 °F is -7.3 °C of offset
RhFixture f([]() -> float { return -13.14f; }, true);
f.temperature.publish_state(77.0f);
EXPECT_NEAR(f.filter.new_value(40.0f).value_or(NAN), CORRECTED, 0.05f);
}
TEST(RHCorrectionFilter, ClampsToOneHundredPercent) {
RhFixture f([]() -> float { return -7.3f; });
f.temperature.publish_state(25.0f);
EXPECT_FLOAT_EQ(f.filter.new_value(90.0f).value_or(NAN), 100.0f);
}
TEST(RHCorrectionFilter, PassesThroughWithoutAUsableTemperature) {
RhFixture f([]() -> float { return -7.3f; });
// No temperature reading yet
EXPECT_FLOAT_EQ(f.filter.new_value(40.0f).value_or(NAN), 40.0f);
// Out of the -40..50 °C band
f.temperature.publish_state(60.0f);
EXPECT_FLOAT_EQ(f.filter.new_value(40.0f).value_or(NAN), 40.0f);
// The band also covers the reconstructed enclosure temperature
f.temperature.publish_state(45.0f);
EXPECT_FLOAT_EQ(f.filter.new_value(40.0f).value_or(NAN), 40.0f);
}
TEST(RHCorrectionFilter, DropsTheUpdateOnNaN) {
RhFixture f([]() -> float { return -7.3f; });
f.temperature.publish_state(NAN);
EXPECT_FALSE(f.filter.new_value(40.0f).has_value());
RhFixture nan_offset([]() -> float { return NAN; });
nan_offset.temperature.publish_state(25.0f);
EXPECT_FALSE(nan_offset.filter.new_value(40.0f).has_value());
}
} // namespace esphome::sensor::testing
@@ -0,0 +1,102 @@
esphome:
name: test-rh-correction
host:
api:
batch_delay: 0ms # Disable batching to receive all state updates
logger:
level: DEBUG
sensor:
- platform: template
name: "Temperature C"
id: temperature_c
accuracy_decimals: 1
- platform: template
name: "Temperature F"
id: temperature_f
accuracy_decimals: 1
- platform: template
name: "Humidity Source"
id: humidity_source
accuracy_decimals: 1
- platform: copy
source_id: humidity_source
name: "RH Celsius"
id: rh_celsius
accuracy_decimals: 2
filters:
- rh_correction:
sensor_id: temperature_c
temperature_offset: -7.3
- platform: copy
source_id: humidity_source
name: "RH Fahrenheit"
id: rh_fahrenheit
accuracy_decimals: 2
filters:
- rh_correction:
sensor_id: temperature_f
temperature_offset: -13.14
use_fahrenheit: true
script:
- id: test_rh_correction
then:
# Both temperature sensors read 25 °C after a -7.3 °C (-13.14 °F) offset
- sensor.template.publish:
id: temperature_c
state: 25.0
- sensor.template.publish:
id: temperature_f
state: 77.0
- sensor.template.publish:
id: humidity_source
state: 40.0
- delay: 20ms
# Clamped to 100 %
- sensor.template.publish:
id: humidity_source
state: 90.0
- delay: 20ms
# Out of band: passes through unchanged
- sensor.template.publish:
id: temperature_c
state: 60.0
- sensor.template.publish:
id: temperature_f
state: 140.0
- sensor.template.publish:
id: humidity_source
state: 40.0
- delay: 20ms
# NaN temperature: the update is dropped
- sensor.template.publish:
id: temperature_c
state: !lambda "return NAN;"
- sensor.template.publish:
id: temperature_f
state: !lambda "return NAN;"
- sensor.template.publish:
id: humidity_source
state: 50.0
- delay: 20ms
- sensor.template.publish:
id: temperature_c
state: 25.0
- sensor.template.publish:
id: temperature_f
state: 77.0
- sensor.template.publish:
id: humidity_source
state: 40.0
button:
- platform: template
name: "Test RH Correction"
on_press:
- script.execute: test_rh_correction
@@ -1,68 +0,0 @@
esphome:
name: uart-mock-ld2410-selects
host:
api:
batch_delay: 0ms # Disable batching to receive all state updates
logger:
level: VERBOSE
external_components:
- source:
type: local
path: EXTERNAL_COMPONENT_PATH
# Dummy uart entry to satisfy ld2410's DEPENDENCIES = ["uart"]
# The actual UART bus used is the uart_mock component below
uart:
baud_rate: 115200
port: /dev/null
# Answers the distance resolution query with 0x00, the module's code for 0.75m
uart_mock:
id: mock_uart
baud_rate: 256000
on_tx:
- then:
- lambda: |-
// The length and command word arrive as one write of their own
static const std::vector<uint8_t> RESOLUTION_QUERY = {0x02, 0x00, 0xAB, 0x00};
if (data != RESOLUTION_QUERY) {
return;
}
id(mock_uart).inject_to_rx_buffer(std::vector<uint8_t>{
0xFD, 0xFC, 0xFB, 0xFA, // header
0x06, 0x00, // length 6
0xAB, 0x01, // answer to command 0x00AB
0x00, 0x00, // status: success
0x00, 0x00, // resolution code 0x00 = 0.75m
0x04, 0x03, 0x02, 0x01, // footer
});
ld2410:
id: ld2410_dev
uart_id: mock_uart
select:
- platform: ld2410
ld2410_id: ld2410_dev
distance_resolution:
name: "Distance Resolution"
baud_rate:
name: "Baud Rate"
# Lambdas reach the name taking setters directly, which the API cannot do with an unknown name
button:
- platform: template
name: "Bogus Names"
on_press:
- lambda: |-
id(ld2410_dev).set_distance_resolution("bogus");
id(ld2410_dev).set_baud_rate("bogus");
ESP_LOGI("test", "bogus names done");
- platform: template
name: "Named Values"
on_press:
- lambda: |-
id(ld2410_dev).set_distance_resolution("0.75m");
id(ld2410_dev).set_baud_rate("57600");
@@ -0,0 +1,55 @@
"""Test the sensor rh_correction filter numerically."""
from __future__ import annotations
from aioesphomeapi import ButtonInfo
import pytest
from .state_utils import InitialStateHelper, SensorStateCollector, require_entity
from .types import APIClientConnectedFactory, RunCompiledFunction
from .wait_utils import wait_or_fail
# 40 % at 25 °C, measured 7.3 °C warmer, in single precision Magnus arithmetic
CORRECTED = pytest.approx(61.13, abs=0.05)
# Corrected, clamped, passed through out of band, corrected again; the NaN step is dropped
EXPECTED = [CORRECTED, 100.0, 40.0, CORRECTED]
@pytest.mark.asyncio
async def test_sensor_filters_rh_correction(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> None:
collector = SensorStateCollector(sensor_names=["rh_celsius", "rh_fahrenheit"])
async with (
run_compiled(yaml_config),
api_client_connected() as client,
):
entities, _ = await client.list_entities_services()
collector.build_key_mapping(entities)
initial_state_helper = InitialStateHelper(entities)
client.subscribe_states(
initial_state_helper.on_state_wrapper(collector.on_state)
)
await initial_state_helper.wait_for_initial_states()
done = collector.add_waiter(
lambda: all(
len(values) == len(EXPECTED)
for values in collector.sensor_states.values()
)
)
button = require_entity(entities, "test_rh_correction", ButtonInfo, exact=True)
client.button_command(button.key)
await wait_or_fail(
done,
2.0,
"Timed out waiting for the corrected humidity values",
values=collector.sensor_states,
)
assert collector.sensor_states["rh_celsius"] == EXPECTED
# The same readings in °F give the same corrected humidity
assert collector.sensor_states["rh_fahrenheit"] == EXPECTED
@@ -1,119 +0,0 @@
"""Integration test for the LD2410 selects with mock UART.
The module answers the distance resolution query with 0x00, so the select starts at 0.75m, and the
baud rate select starts at the bus speed. Choosing an option must send the device code of that
option: 0.2m is 0x01 and 19200 is the second baud rate. The name taking setters map a name the
same way from a lambda, and an unknown name sends nothing at all.
"""
from __future__ import annotations
import asyncio
from aioesphomeapi import ButtonInfo, SelectInfo
import pytest
from .state_utils import InitialStateHelper, require_entity
from .types import APIClientConnectedFactory, RunCompiledFunction
from .wait_utils import wait_or_fail
# Length and command word of each frame as the uart mock logs it; the payload is logged on its own TX line
SET_RESOLUTION = "04:00:AA:00"
SET_BAUD_RATE = "04:00:A1:00"
class _TxLines:
"""Record the TX lines and the marker lines, and hand out the payload line after an awaited command."""
def __init__(self, *markers: str) -> None:
self._markers = markers
self.lines: list[str] = []
# Command text, whether its line was seen, and the future for the line after it
self._awaited: list[tuple[str, bool, asyncio.Future[str]]] = []
def on_line(self, line: str) -> None:
is_tx = "uart_mock" in line and "TX " in line
if not is_tx and not any(marker in line for marker in self._markers):
return
self.lines.append(line)
for i, (text, seen, future) in enumerate(self._awaited):
if seen:
if not future.done():
future.set_result(line)
elif text in line:
self._awaited[i] = (text, True, future)
def next_payload_after(self, text: str) -> asyncio.Future[str]:
"""Resolve with the TX line logged right after the next line holding ``text``."""
future: asyncio.Future[str] = asyncio.get_running_loop().create_future()
self._awaited.append((text, False, future))
return future
@pytest.mark.asyncio
async def test_uart_mock_ld2410_selects(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> None:
"""Test that the selects show the module's values and send the device code of the chosen option."""
bogus_done = "bogus names done"
events = _TxLines(bogus_done)
async with (
run_compiled(yaml_config, line_callback=events.on_line),
api_client_connected() as client,
):
entities, _ = await client.list_entities_services()
initial_state_helper = InitialStateHelper(entities)
client.subscribe_states(initial_state_helper.on_state_wrapper(lambda s: None))
await initial_state_helper.wait_for_initial_states()
resolution = require_entity(entities, "distance_resolution", SelectInfo)
baud_rate = require_entity(entities, "baud_rate", SelectInfo)
assert initial_state_helper.initial_states[resolution.key].state == "0.75m"
# The mock bus runs at 256000, which the select reports at boot
assert initial_state_helper.initial_states[baud_rate.key].state == "256000"
# An unknown name writes nothing, not even the configuration mode frames around a command.
# This runs before any select write, so no restart timer from a write can send a frame meanwhile.
before = len(events.lines)
bogus = events.next_payload_after(bogus_done)
client.button_command(require_entity(entities, "bogus_names", ButtonInfo).key)
# Each payload line follows its length and command line directly
resolution_payload = events.next_payload_after(SET_RESOLUTION)
client.select_command(resolution.key, "0.2m")
await wait_or_fail(
asyncio.gather(bogus, resolution_payload),
5.0,
"Timeout waiting for the distance resolution command",
logged=events.lines,
)
marker = next(
i for i in range(before, len(events.lines)) if bogus_done in events.lines[i]
)
assert all("TX " not in line for line in events.lines[before:marker])
assert "TX 2 bytes: 01:00" in resolution_payload.result()
baud_rate_payload = events.next_payload_after(SET_BAUD_RATE)
client.select_command(baud_rate.key, "19200")
await wait_or_fail(
baud_rate_payload,
5.0,
"Timeout waiting for the baud rate command",
logged=events.lines,
)
assert "TX 2 bytes: 02:00" in baud_rate_payload.result()
named_resolution = events.next_payload_after(SET_RESOLUTION)
named_baud_rate = events.next_payload_after(SET_BAUD_RATE)
client.button_command(require_entity(entities, "named_values", ButtonInfo).key)
await wait_or_fail(
asyncio.gather(named_resolution, named_baud_rate),
5.0,
"Timeout waiting for the named setter commands",
logged=events.lines,
)
# 0.75m is device code 0x00 and 57600 is the fourth baud rate
assert "TX 2 bytes: 00:00" in named_resolution.result()
assert "TX 2 bytes: 04:00" in named_baud_rate.result()