mirror of
https://github.com/esphome/esphome.git
synced 2026-10-28 00:42:11 +00:00
Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
02502b7af2 | ||
|
|
205ef9407f | ||
|
|
d552396650 | ||
|
|
a101d57476 | ||
|
|
195cb3b2b4 | ||
|
|
8529b7a59c | ||
|
|
4e63342350 | ||
|
|
3784a3d56a | ||
|
|
848d1ac4e8 |
No files matched your search
@@ -4,12 +4,11 @@ The platform analog of esp32_ble / rp2040_ble: owns the Beken BDK BLE stack
|
||||
bring-up and the controller BLE address. Consumers (bk72xx_ble_tracker) build
|
||||
on this component and contain no SDK calls of their own.
|
||||
|
||||
Supported SoCs (BLE 5.x): BK7231N/BK7236 (BLE 5.1), BK7252N/BK7253 (BLE 5.2),
|
||||
and any future BLE-5.x SoC. BK7238 (BLE 5.2) is blocked for now: with BLE
|
||||
compiled in, the Beken SDK erases the bootloader flash sector at boot because
|
||||
LibreTiny's partition table has no BLE bonding entry (esphome#18646,
|
||||
libretiny-eu/libretiny#408). Known non-5.x families and BK7238 are rejected in
|
||||
to_code. Unknown families are capability-checked at compile time via
|
||||
Supported SoCs (BLE 5.x): BK7231N/BK7236 (BLE 5.1), BK7238/BK7252N/BK7253 (BLE 5.2),
|
||||
and any future BLE-5.x SoC. LibreTiny 1.14 or newer is required: earlier releases
|
||||
have no BLE bonding partition, so the Beken SDK erases the bootloader flash sector
|
||||
at boot on BK7238 (esphome#18646, libretiny-eu/libretiny#408). Known non-5.x
|
||||
families are rejected in to_code. Unknown families are capability-checked at compile time via
|
||||
`__has_include("app_ble.h")`, a header only on the BLE 5.x include path
|
||||
(ble_api.h ships for every SoC, so it cannot be the probe). A non-5.x build
|
||||
fails with a clear #error.
|
||||
@@ -29,7 +28,6 @@ from esphome.components.libretiny.const import (
|
||||
FAMILY_BK7231N,
|
||||
FAMILY_BK7231Q,
|
||||
FAMILY_BK7231T,
|
||||
FAMILY_BK7238,
|
||||
FAMILY_BK7251,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
@@ -54,7 +52,9 @@ BEKEN_BDK_VERSION_OPTION = "custom_versions.beken-bdk"
|
||||
# bootloader with BLE compiled in; the libretiny platform builds with this one
|
||||
MIN_BDK_VERSION = (3, 0, 78)
|
||||
# Older LibreTiny releases arm the Beken MCU sleep with wifi power save, which halts
|
||||
# the device once the station stops while BLE runs (libretiny-eu/libretiny#414)
|
||||
# the device once the station stops while BLE runs (libretiny-eu/libretiny#414), and
|
||||
# have no BLE bond partition, so the BK7238 bond store erases flash sector 0 and
|
||||
# bricks the chip (libretiny-eu/libretiny#408)
|
||||
MIN_LIBRETINY_VERSION = (1, 14, 0)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -86,14 +86,6 @@ def _unsupported_family_message(family: str) -> str | None:
|
||||
)
|
||||
if family == FAMILY_BK7231Q:
|
||||
return "bk72xx_ble does not support BK7231Q: this SoC has no BLE"
|
||||
if family == FAMILY_BK7238:
|
||||
return (
|
||||
"bk72xx_ble is disabled on BK7238: with BLE compiled in, the Beken SDK "
|
||||
"erases the bootloader flash sector at boot and the device can no longer "
|
||||
"start (see https://github.com/esphome/esphome/issues/18646); support "
|
||||
"returns once the LibreTiny partition table fix "
|
||||
"(libretiny-eu/libretiny#408) is released"
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
@@ -117,14 +109,15 @@ def check_bdk_version(value: str | None) -> None:
|
||||
|
||||
|
||||
def check_libretiny_version(version: cv.Version) -> None:
|
||||
"""Reject a LibreTiny release older than the one whose WiFi sleep is safe with BLE."""
|
||||
"""Reject a LibreTiny release older than the one whose WiFi sleep and bond storage are safe with BLE."""
|
||||
if (version.major, version.minor, version.patch) >= MIN_LIBRETINY_VERSION:
|
||||
return
|
||||
minimum = ".".join(str(part) for part in MIN_LIBRETINY_VERSION)
|
||||
raise cv.Invalid(
|
||||
f"bk72xx_ble needs LibreTiny {minimum} or newer, not {version}: older releases "
|
||||
"arm the Beken MCU sleep with wifi power save and the device halts once the "
|
||||
"station stops while BLE runs"
|
||||
"station stops while BLE runs, and on BK7238 the bond store erases flash "
|
||||
"sector 0 and bricks the chip"
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -102,8 +102,6 @@ static constexpr uint8_t ENGINEERING_FRAME_MIN = OUT_PIN_SENSOR + 3 + HEADER_FOO
|
||||
// Data Header & Footer
|
||||
static constexpr uint8_t DATA_FRAME_HEADER[HEADER_FOOTER_SIZE] = {0xF4, 0xF3, 0xF2, 0xF1};
|
||||
static constexpr uint8_t DATA_FRAME_FOOTER[HEADER_FOOTER_SIZE] = {0xF8, 0xF7, 0xF6, 0xF5};
|
||||
// MAC address the module uses when Bluetooth is disabled
|
||||
static constexpr uint8_t NO_MAC[] = {0x08, 0x05, 0x04, 0x03, 0x02, 0x01};
|
||||
|
||||
void LD2410Component::dump_config() {
|
||||
char mac_s[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||
@@ -425,10 +423,7 @@ void LD2410Component::handle_ack_data_() {
|
||||
break; // too short to hold a MAC address
|
||||
}
|
||||
|
||||
this->bluetooth_on_ = std::memcmp(&this->buffer_data_[10], NO_MAC, sizeof(NO_MAC)) != 0;
|
||||
if (this->bluetooth_on_) {
|
||||
std::memcpy(this->mac_address_, &this->buffer_data_[10], sizeof(this->mac_address_));
|
||||
}
|
||||
this->bluetooth_on_ = ld24xx::store_mac(&this->buffer_data_[10], this->mac_address_);
|
||||
|
||||
char mac_s[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||
const char *mac_str = ld24xx::format_mac_str(this->mac_address_, mac_s);
|
||||
|
||||
@@ -120,8 +120,6 @@ static constexpr uint8_t ENGINEERING_FRAME_MIN = LIGHT_SENSOR + 3 + HEADER_FOOTE
|
||||
// Data Header & Footer
|
||||
static constexpr uint8_t DATA_FRAME_HEADER[HEADER_FOOTER_SIZE] = {0xF4, 0xF3, 0xF2, 0xF1};
|
||||
static constexpr uint8_t DATA_FRAME_FOOTER[HEADER_FOOTER_SIZE] = {0xF8, 0xF7, 0xF6, 0xF5};
|
||||
// MAC address the module uses when Bluetooth is disabled
|
||||
static constexpr uint8_t NO_MAC[] = {0x08, 0x05, 0x04, 0x03, 0x02, 0x01};
|
||||
static constexpr uint8_t GATE_SENS_ACK_SIZE = ACK_PAYLOAD + TOTAL_GATES + HEADER_FOOTER_SIZE;
|
||||
|
||||
void LD2412Component::dump_config() {
|
||||
@@ -477,10 +475,7 @@ void LD2412Component::handle_ack_data_() {
|
||||
break; // too short to hold a MAC address
|
||||
}
|
||||
|
||||
this->bluetooth_on_ = std::memcmp(&this->buffer_data_[10], NO_MAC, sizeof(NO_MAC)) != 0;
|
||||
if (this->bluetooth_on_) {
|
||||
std::memcpy(this->mac_address_, &this->buffer_data_[10], sizeof(this->mac_address_));
|
||||
}
|
||||
this->bluetooth_on_ = ld24xx::store_mac(&this->buffer_data_[10], this->mac_address_);
|
||||
|
||||
char mac_s[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||
const char *mac_str = ld24xx::format_mac_str(this->mac_address_, mac_s);
|
||||
|
||||
@@ -79,8 +79,6 @@ static constexpr uint8_t DATA_FRAME_FOOTER[2] = {0x55, 0xCC};
|
||||
// 3 zones * 8 + footer (4).
|
||||
static constexpr uint8_t ZONE_ACK_FRAME_SIZE = 40;
|
||||
static constexpr uint8_t PERIODIC_FRAME_SIZE = 30; // header, three targets of 8 bytes, footer
|
||||
// MAC address the module uses when Bluetooth is disabled
|
||||
static constexpr uint8_t NO_MAC[] = {0x08, 0x05, 0x04, 0x03, 0x02, 0x01};
|
||||
|
||||
static inline uint32_t convert_seconds_to_ms(uint16_t value) { return (uint32_t) value * 1000; };
|
||||
|
||||
@@ -192,6 +190,11 @@ void LD2450Component::dump_config() {
|
||||
for (text_sensor::TextSensor *s : this->direction_text_sensors_) {
|
||||
LOG_TEXT_SENSOR(" ", "Direction", s);
|
||||
}
|
||||
#ifdef USE_LD2450_TARGET_XY
|
||||
for (text_sensor::TextSensor *s : this->target_xy_text_sensors_) {
|
||||
LOG_TEXT_SENSOR(" ", "TargetXY", s);
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
#ifdef USE_NUMBER
|
||||
ESP_LOGCONFIG(TAG, "Numbers:");
|
||||
@@ -295,9 +298,6 @@ bool LD2450Component::get_timeout_status_(uint32_t check_millis) {
|
||||
if (check_millis == 0) {
|
||||
return true;
|
||||
}
|
||||
if (this->timeout_ == 0) {
|
||||
this->timeout_ = ld2450::convert_seconds_to_ms(DEFAULT_PRESENCE_TIMEOUT);
|
||||
}
|
||||
return App.get_loop_component_start_time() - check_millis >= this->timeout_;
|
||||
}
|
||||
|
||||
@@ -468,9 +468,16 @@ void LD2450Component::handle_periodic_data_() {
|
||||
|
||||
// Store target info for zone target count. Zero out untracked targets (td==0)
|
||||
// so stale coordinates don't produce ghost counts in count_targets_in_zone_().
|
||||
#ifdef USE_LD2450_TARGET_XY
|
||||
const Target previous = this->target_info_[index];
|
||||
#endif
|
||||
this->target_info_[index].x = (td > 0) ? tx : 0;
|
||||
this->target_info_[index].y = (td > 0) ? ty : 0;
|
||||
this->target_info_[index].is_moving = (td > 0) && is_moving;
|
||||
#ifdef USE_LD2450_TARGET_XY
|
||||
this->update_target_xy_(index,
|
||||
previous.x != this->target_info_[index].x || previous.y != this->target_info_[index].y);
|
||||
#endif
|
||||
|
||||
} // End loop thru targets
|
||||
|
||||
@@ -621,10 +628,7 @@ void LD2450Component::handle_ack_data_() {
|
||||
break; // too short to hold a MAC address
|
||||
}
|
||||
|
||||
this->bluetooth_on_ = std::memcmp(&this->buffer_data_[10], NO_MAC, sizeof(NO_MAC)) != 0;
|
||||
if (this->bluetooth_on_) {
|
||||
std::memcpy(this->mac_address_, &this->buffer_data_[10], sizeof(this->mac_address_));
|
||||
}
|
||||
this->bluetooth_on_ = ld24xx::store_mac(&this->buffer_data_[10], this->mac_address_);
|
||||
|
||||
char mac_s[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||
const char *mac_str = ld24xx::format_mac_str(this->mac_address_, mac_s);
|
||||
@@ -862,6 +866,27 @@ void LD2450Component::set_direction_text_sensor(uint8_t target, text_sensor::Tex
|
||||
this->direction_text_sensors_[target] = s;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_LD2450_TARGET_XY
|
||||
void LD2450Component::set_target_xy_text_sensor(uint8_t target, text_sensor::TextSensor *s) {
|
||||
this->target_xy_text_sensors_[target] = s;
|
||||
}
|
||||
|
||||
// Publish the position of a target in one radar frame; the sensor's filters throttle it
|
||||
void LD2450Component::update_target_xy_(uint8_t target, bool changed) {
|
||||
text_sensor::TextSensor *s = this->target_xy_text_sensors_[target];
|
||||
if (s == nullptr || (!changed && s->has_state()))
|
||||
return;
|
||||
const Target &pos = this->target_info_[target];
|
||||
if (pos.x == 0 && pos.y == 0) {
|
||||
s->publish_state(DirectionStrings::get_progmem_str(DIRECTION_NA, DirectionStrings::LAST_INDEX));
|
||||
return;
|
||||
}
|
||||
// Longest is "-32767,-32767"
|
||||
char text[16];
|
||||
const size_t len = buf_append_printf(text, sizeof(text), 0, "%d,%d", pos.x, pos.y);
|
||||
s->publish_state(text, len);
|
||||
}
|
||||
#endif
|
||||
|
||||
// Send Zone coordinates data to LD2450
|
||||
#ifdef USE_NUMBER
|
||||
@@ -900,17 +925,14 @@ void LD2450Component::set_zone_numbers(uint8_t zone, number::Number *x1, number:
|
||||
// Set Presence Timeout load and save from flash
|
||||
#ifdef USE_NUMBER
|
||||
void LD2450Component::set_presence_timeout() {
|
||||
if (this->presence_timeout_number_ != nullptr) {
|
||||
if (this->presence_timeout_number_->state == 0) {
|
||||
float timeout = this->restore_from_flash_();
|
||||
this->presence_timeout_number_->publish_state(timeout);
|
||||
this->timeout_ = ld2450::convert_seconds_to_ms(timeout);
|
||||
}
|
||||
if (this->presence_timeout_number_->has_state()) {
|
||||
this->save_to_flash_(this->presence_timeout_number_->state);
|
||||
this->timeout_ = ld2450::convert_seconds_to_ms(this->presence_timeout_number_->state);
|
||||
}
|
||||
}
|
||||
if (this->presence_timeout_number_ == nullptr)
|
||||
return;
|
||||
// Restore only while the number has no state yet: 0 is a valid presence_timeout (no hold)
|
||||
if (!this->presence_timeout_number_->has_state())
|
||||
this->presence_timeout_number_->publish_state(this->restore_from_flash_());
|
||||
const float timeout = this->presence_timeout_number_->state;
|
||||
this->save_to_flash_(timeout);
|
||||
this->timeout_ = ld2450::convert_seconds_to_ms(timeout);
|
||||
}
|
||||
|
||||
// Save Presence Timeout to flash
|
||||
|
||||
@@ -146,6 +146,9 @@ class LD2450Component : public Component, public uart::UARTDevice {
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
void set_direction_text_sensor(uint8_t target, text_sensor::TextSensor *s);
|
||||
#endif
|
||||
#ifdef USE_LD2450_TARGET_XY
|
||||
void set_target_xy_text_sensor(uint8_t target, text_sensor::TextSensor *s);
|
||||
#endif
|
||||
#ifdef USE_NUMBER
|
||||
void set_zone_coordinate(number::Number *n);
|
||||
void set_zone_numbers(uint8_t zone, number::Number *x1, number::Number *y1, number::Number *x2, number::Number *y2);
|
||||
@@ -189,6 +192,9 @@ class LD2450Component : public Component, public uart::UARTDevice {
|
||||
void save_to_flash_(float value);
|
||||
float restore_from_flash_();
|
||||
bool get_timeout_status_(uint32_t check_millis);
|
||||
#ifdef USE_LD2450_TARGET_XY
|
||||
void update_target_xy_(uint8_t target, bool changed);
|
||||
#endif
|
||||
void count_targets_in_zone_(const Zone &zone, uint8_t &still, uint8_t &moving);
|
||||
|
||||
uint32_t presence_millis_ = 0;
|
||||
@@ -226,6 +232,10 @@ class LD2450Component : public Component, public uart::UARTDevice {
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
std::array<text_sensor::TextSensor *, MAX_TARGETS> direction_text_sensors_{};
|
||||
#endif
|
||||
#ifdef USE_LD2450_TARGET_XY
|
||||
// Publishes a target's position as "x,y" text, so both values always come from the same radar frame
|
||||
std::array<text_sensor::TextSensor *, MAX_TARGETS> target_xy_text_sensors_{};
|
||||
#endif
|
||||
|
||||
#ifdef LD2450_POLYGON_ZONE_COUNT
|
||||
StaticVector<PolygonZone *, LD2450_POLYGON_ZONE_COUNT> polygon_zones_;
|
||||
|
||||
@@ -40,6 +40,8 @@ void PolygonZone::control(const std::string &value) {
|
||||
ESP_LOGW(TAG, "'%s': could not save the polygon; keeping the previous one", LOG_STR_ARG(this->get_log_name()));
|
||||
} else {
|
||||
this->polygon_ = polygon;
|
||||
// A user-made edit is written to flash now, as wall-powered sensors lose power without warning
|
||||
global_preferences->sync();
|
||||
// A target seen inside the previous polygon must not keep the new one on
|
||||
this->last_inside_ms_ = 0;
|
||||
if (this->polygon_.empty())
|
||||
|
||||
@@ -18,6 +18,10 @@ from . import CONF_LD2450_ID, FILTER_SOURCE_FILES, LD2450Component # noqa: F401
|
||||
|
||||
DEPENDENCIES = ["ld2450"]
|
||||
|
||||
CONF_XY = "xy"
|
||||
|
||||
ICON_CROSSHAIRS_GPS = "mdi:crosshairs-gps"
|
||||
|
||||
MAX_TARGETS = 3
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
@@ -43,6 +47,11 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend(
|
||||
entity_category=ENTITY_CATEGORY_NONE,
|
||||
icon=ICON_SIGN_DIRECTION,
|
||||
),
|
||||
cv.Optional(CONF_XY): text_sensor.text_sensor_schema(
|
||||
entity_category=ENTITY_CATEGORY_NONE,
|
||||
icon=ICON_CROSSHAIRS_GPS,
|
||||
filters=text_sensor.timeout_throttle_filters("NA"),
|
||||
),
|
||||
}
|
||||
)
|
||||
for n in range(MAX_TARGETS)
|
||||
@@ -56,8 +65,12 @@ async def to_code(config: ConfigType) -> None:
|
||||
await text_sensors(CONF_VERSION, hub.set_version_text_sensor)
|
||||
await text_sensors(CONF_MAC_ADDRESS, hub.set_mac_text_sensor)
|
||||
for n in range(MAX_TARGETS):
|
||||
if (direction_conf := config.get(f"target_{n + 1}")) and (
|
||||
direction_config := direction_conf.get(CONF_DIRECTION)
|
||||
):
|
||||
if not (target_conf := config.get(f"target_{n + 1}")):
|
||||
continue
|
||||
if direction_config := target_conf.get(CONF_DIRECTION):
|
||||
sens = await text_sensor.new_text_sensor(direction_config)
|
||||
cg.add(hub.set_direction_text_sensor(n, sens))
|
||||
if xy_config := target_conf.get(CONF_XY):
|
||||
cg.add_define("USE_LD2450_TARGET_XY")
|
||||
sens = await text_sensor.new_text_sensor(xy_config)
|
||||
cg.add(hub.set_target_xy_text_sensor(n, sens))
|
||||
@@ -41,7 +41,15 @@ template<typename T, size_t N> optional<size_t> find_index(const T (&arr)[N], T
|
||||
|
||||
// Formats the MAC address into the buffer, or "unknown" when it is not valid
|
||||
inline const char *format_mac_str(const uint8_t *mac_address, std::span<char, MAC_ADDRESS_PRETTY_BUFFER_SIZE> buffer) {
|
||||
if (mac_address_is_valid(mac_address)) {
|
||||
// mac_address_is_valid() rejects the multicast bit, which radar Bluetooth MACs may have set; only all-00 and all-FF
|
||||
// mean no address
|
||||
uint8_t any_set = 0x00;
|
||||
uint8_t all_set = 0xFF;
|
||||
for (size_t i = 0; i < MAC_ADDRESS_SIZE; i++) {
|
||||
any_set |= mac_address[i];
|
||||
all_set &= mac_address[i];
|
||||
}
|
||||
if (any_set != 0x00 && all_set != 0xFF) {
|
||||
format_mac_addr_upper(mac_address, buffer.data());
|
||||
} else {
|
||||
ESPHOME_strncpy_P(buffer.data(), ESPHOME_PSTR("unknown"), buffer.size());
|
||||
@@ -49,6 +57,21 @@ inline const char *format_mac_str(const uint8_t *mac_address, std::span<char, MA
|
||||
return buffer.data();
|
||||
}
|
||||
|
||||
// MAC address the module uses when Bluetooth is disabled
|
||||
static constexpr uint8_t NO_MAC[MAC_ADDRESS_SIZE] PROGMEM = {0x08, 0x05, 0x04, 0x03, 0x02, 0x01};
|
||||
|
||||
// Stores the radar's reported MAC, or clears it when the radar reports Bluetooth off; returns whether Bluetooth is on
|
||||
inline bool store_mac(const uint8_t *reported, uint8_t *mac_address) {
|
||||
uint8_t no_mac[MAC_ADDRESS_SIZE];
|
||||
progmem_memcpy(no_mac, NO_MAC, MAC_ADDRESS_SIZE);
|
||||
if (std::memcmp(reported, no_mac, MAC_ADDRESS_SIZE) == 0) {
|
||||
std::memset(mac_address, 0, MAC_ADDRESS_SIZE);
|
||||
return false;
|
||||
}
|
||||
std::memcpy(mac_address, reported, MAC_ADDRESS_SIZE);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Helper function to format firmware version with stack allocation
|
||||
// Buffer must be exactly 20 bytes (format: "x.xxXXXXXX" fits in 11 + null terminator, 20 for safety)
|
||||
inline void format_version_str(const uint8_t *version, std::span<char, 20> buffer) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "prometheus_handler.h"
|
||||
#include <cinttypes>
|
||||
#if defined(USE_NETWORK) && !defined(USE_HOST)
|
||||
#include "esphome/core/application.h"
|
||||
|
||||
@@ -100,6 +101,24 @@ void PrometheusHandler::handleRequest(AsyncWebServerRequest *req) {
|
||||
this->valve_row_(stream, obj, area, node, friendly_name);
|
||||
#endif
|
||||
|
||||
#ifdef USE_DATETIME_DATE
|
||||
this->date_type_(stream);
|
||||
for (auto *obj : App.get_dates())
|
||||
this->date_row_(stream, obj, area, node, friendly_name);
|
||||
#endif
|
||||
|
||||
#ifdef USE_DATETIME_TIME
|
||||
this->time_type_(stream);
|
||||
for (auto *obj : App.get_times())
|
||||
this->time_row_(stream, obj, area, node, friendly_name);
|
||||
#endif
|
||||
|
||||
#ifdef USE_DATETIME_DATETIME
|
||||
this->datetime_type_(stream);
|
||||
for (auto *obj : App.get_datetimes())
|
||||
this->datetime_row_(stream, obj, area, node, friendly_name);
|
||||
#endif
|
||||
|
||||
#ifdef USE_CLIMATE
|
||||
this->climate_type_(stream);
|
||||
for (auto *obj : App.get_climates())
|
||||
@@ -1096,6 +1115,78 @@ void PrometheusHandler::climate_row_(AsyncResponseStream *stream, climate::Clima
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(USE_DATETIME_DATE) || defined(USE_DATETIME_DATETIME) || defined(USE_DATETIME_TIME)
|
||||
void PrometheusHandler::date_time_rows_(AsyncResponseStream *stream, ProgmemStr failed_metric, ProgmemStr value_metric,
|
||||
EntityBase *obj, int64_t value_ms, std::string &area, std::string &node,
|
||||
std::string &friendly_name) {
|
||||
if (obj->is_internal() && !this->include_internal_)
|
||||
return;
|
||||
this->print_metric_labels_(stream, failed_metric, obj, area, node, friendly_name);
|
||||
if (!obj->has_state()) {
|
||||
stream->print(ESPHOME_F("\"} 1\n"));
|
||||
return;
|
||||
}
|
||||
stream->print(ESPHOME_F("\"} 0\n"));
|
||||
this->print_metric_labels_(stream, value_metric, obj, area, node, friendly_name);
|
||||
// Date and datetime are ms since the epoch (what Grafana expects), time is ms since midnight
|
||||
char buf[32];
|
||||
snprintf(buf, sizeof(buf), "\"} %" PRId64 "\n", value_ms);
|
||||
stream->print(buf);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_DATETIME_DATE
|
||||
void PrometheusHandler::date_type_(AsyncResponseStream *stream) {
|
||||
stream->print(ESPHOME_F("#TYPE esphome_date_value gauge\n"));
|
||||
stream->print(ESPHOME_F("#TYPE esphome_date_failed gauge\n"));
|
||||
}
|
||||
void PrometheusHandler::date_row_(AsyncResponseStream *stream, datetime::DateEntity *obj, std::string &area,
|
||||
std::string &node, std::string &friendly_name) {
|
||||
int64_t value_ms = 0;
|
||||
if (obj->has_state()) {
|
||||
// DateEntity::state_as_esptime() leaves the time fields unset, so build midnight here
|
||||
ESPTime t{};
|
||||
t.year = obj->year;
|
||||
t.month = obj->month;
|
||||
t.day_of_month = obj->day;
|
||||
t.recalc_timestamp_local();
|
||||
value_ms = static_cast<int64_t>(t.timestamp) * 1000;
|
||||
}
|
||||
this->date_time_rows_(stream, ESPHOME_F("esphome_date_failed"), ESPHOME_F("esphome_date_value"), obj, value_ms, area,
|
||||
node, friendly_name);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_DATETIME_TIME
|
||||
void PrometheusHandler::time_type_(AsyncResponseStream *stream) {
|
||||
stream->print(ESPHOME_F("#TYPE esphome_time_value gauge\n"));
|
||||
stream->print(ESPHOME_F("#TYPE esphome_time_failed gauge\n"));
|
||||
}
|
||||
void PrometheusHandler::time_row_(AsyncResponseStream *stream, datetime::TimeEntity *obj, std::string &area,
|
||||
std::string &node, std::string &friendly_name) {
|
||||
int64_t value_ms = 0;
|
||||
if (obj->has_state())
|
||||
value_ms = (static_cast<int64_t>(obj->hour) * 3600 + obj->minute * 60 + obj->second) * 1000;
|
||||
this->date_time_rows_(stream, ESPHOME_F("esphome_time_failed"), ESPHOME_F("esphome_time_value"), obj, value_ms, area,
|
||||
node, friendly_name);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_DATETIME_DATETIME
|
||||
void PrometheusHandler::datetime_type_(AsyncResponseStream *stream) {
|
||||
stream->print(ESPHOME_F("#TYPE esphome_datetime_value gauge\n"));
|
||||
stream->print(ESPHOME_F("#TYPE esphome_datetime_failed gauge\n"));
|
||||
}
|
||||
void PrometheusHandler::datetime_row_(AsyncResponseStream *stream, datetime::DateTimeEntity *obj, std::string &area,
|
||||
std::string &node, std::string &friendly_name) {
|
||||
int64_t value_ms = 0;
|
||||
if (obj->has_state())
|
||||
value_ms = static_cast<int64_t>(obj->state_as_esptime().timestamp) * 1000;
|
||||
this->date_time_rows_(stream, ESPHOME_F("esphome_datetime_failed"), ESPHOME_F("esphome_datetime_value"), obj,
|
||||
value_ms, area, node, friendly_name);
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace esphome::prometheus
|
||||
|
||||
#endif
|
||||
@@ -193,6 +193,36 @@ class PrometheusHandler final : public AsyncWebHandler, public Component {
|
||||
std::string &friendly_name);
|
||||
#endif
|
||||
|
||||
#if defined(USE_DATETIME_DATE) || defined(USE_DATETIME_DATETIME) || defined(USE_DATETIME_TIME)
|
||||
/// Print the failed row and, when the entity has a state, the value row in milliseconds
|
||||
void date_time_rows_(AsyncResponseStream *stream, ProgmemStr failed_metric, ProgmemStr value_metric, EntityBase *obj,
|
||||
int64_t value_ms, std::string &area, std::string &node, std::string &friendly_name);
|
||||
#endif
|
||||
|
||||
#ifdef USE_DATETIME_DATE
|
||||
/// Return the type for prometheus
|
||||
void date_type_(AsyncResponseStream *stream);
|
||||
/// Return the date state as prometheus data point
|
||||
void date_row_(AsyncResponseStream *stream, datetime::DateEntity *obj, std::string &area, std::string &node,
|
||||
std::string &friendly_name);
|
||||
#endif
|
||||
|
||||
#ifdef USE_DATETIME_TIME
|
||||
/// Return the type for prometheus
|
||||
void time_type_(AsyncResponseStream *stream);
|
||||
/// Return the time state as prometheus data point
|
||||
void time_row_(AsyncResponseStream *stream, datetime::TimeEntity *obj, std::string &area, std::string &node,
|
||||
std::string &friendly_name);
|
||||
#endif
|
||||
|
||||
#ifdef USE_DATETIME_DATETIME
|
||||
/// Return the type for prometheus
|
||||
void datetime_type_(AsyncResponseStream *stream);
|
||||
/// Return the datetime state as prometheus data point
|
||||
void datetime_row_(AsyncResponseStream *stream, datetime::DateTimeEntity *obj, std::string &area, std::string &node,
|
||||
std::string &friendly_name);
|
||||
#endif
|
||||
|
||||
#ifdef USE_CLIMATE
|
||||
/// Return the type for prometheus
|
||||
void climate_type_(AsyncResponseStream *stream);
|
||||
|
||||
@@ -8,8 +8,6 @@
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <new>
|
||||
|
||||
#ifdef USE_ESP_IDF
|
||||
#include "esp_task_wdt.h"
|
||||
#endif
|
||||
@@ -96,21 +94,10 @@ static int draw_callback(JPEGDRAW *jpeg) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
JpegDecoder::JpegDecoder(RuntimeImage *image) : ImageDecoder(image, JPEG) {
|
||||
RAMAllocator<JPEGDEC> allocator;
|
||||
JPEGDEC *jpeg = allocator.allocate(1);
|
||||
if (jpeg == nullptr) {
|
||||
JpegDecoder::JpegDecoder(RuntimeImage *image)
|
||||
: ImageDecoder(image, JPEG), jpeg_(RAMAllocator<JPEGDEC>().make_unique()) {
|
||||
if (this->jpeg_ == nullptr) {
|
||||
ESP_LOGE(TAG, "Failed to allocate memory for the JPEG decoder");
|
||||
return;
|
||||
}
|
||||
this->jpeg_ = new (jpeg) JPEGDEC();
|
||||
}
|
||||
|
||||
JpegDecoder::~JpegDecoder() {
|
||||
if (this->jpeg_ != nullptr) {
|
||||
this->jpeg_->~JPEGDEC();
|
||||
RAMAllocator<JPEGDEC> allocator;
|
||||
allocator.deallocate(this->jpeg_, 1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@ class JpegDecoder : public ImageDecoder {
|
||||
* @param image The RuntimeImage to decode the stream into.
|
||||
*/
|
||||
JpegDecoder(RuntimeImage *image);
|
||||
~JpegDecoder() override;
|
||||
|
||||
int prepare(size_t expected_size) override;
|
||||
int HOT decode(uint8_t *buffer, size_t size) override;
|
||||
@@ -47,8 +46,8 @@ class JpegDecoder : public ImageDecoder {
|
||||
static int32_t source_seek(JPEGFILE *file, int32_t position);
|
||||
static void source_close(void *handle) {}
|
||||
|
||||
/// About 18 KB of decoder state, allocated like pngle in PngDecoder: PSRAM first, nullptr if out of memory
|
||||
JPEGDEC *jpeg_{nullptr};
|
||||
/// About 18 KB of decoder state: PSRAM first, nullptr if out of memory
|
||||
RAMUniquePtr<JPEGDEC> jpeg_;
|
||||
int scale_{1}; // JPEGDEC decodes at 1/scale_ of the image size
|
||||
DecodeSource *source_{nullptr}; // Set while a begin_source() decode is in progress
|
||||
bool source_failed_{false};
|
||||
|
||||
@@ -74,9 +74,16 @@ class TinyUSB final : public Component {
|
||||
.bLength = sizeof(tusb_desc_device_t),
|
||||
.bDescriptorType = TUSB_DESC_DEVICE,
|
||||
.bcdUSB = 0x0200,
|
||||
#if CFG_TUD_CDC || CFG_TUD_NCM || CFG_TUD_ECM_RNDIS
|
||||
// Interface association (IAD) only for classes that group interfaces, as esp_tinyusb's default descriptor does
|
||||
.bDeviceClass = TUSB_CLASS_MISC,
|
||||
.bDeviceSubClass = MISC_SUBCLASS_COMMON,
|
||||
.bDeviceProtocol = MISC_PROTOCOL_IAD,
|
||||
#else
|
||||
.bDeviceClass = TUSB_CLASS_UNSPECIFIED,
|
||||
.bDeviceSubClass = 0,
|
||||
.bDeviceProtocol = 0,
|
||||
#endif
|
||||
.bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE,
|
||||
.idVendor = 0x303A,
|
||||
.idProduct = 0x4001,
|
||||
|
||||
@@ -185,6 +185,7 @@
|
||||
#define MDNS_DYNAMIC_TXT_COUNT 2
|
||||
#define LD2420_LISTENER_COUNT 1
|
||||
#define LD2450_POLYGON_ZONE_COUNT 2
|
||||
#define USE_LD2450_TARGET_XY
|
||||
#define SEN6X_STATUS_SENSOR_COUNT 1
|
||||
#define UART_BINARY_SENSOR_COUNT 2
|
||||
#define MICRONOVA_LISTENER_COUNT 1
|
||||
|
||||
@@ -137,6 +137,7 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type
|
||||
delay = 1;
|
||||
}
|
||||
|
||||
uint32_t offset = 0;
|
||||
// Take lock early to protect scheduler_item_pool_head_ access
|
||||
{
|
||||
LockGuard guard{this->lock_};
|
||||
@@ -181,13 +182,8 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type
|
||||
if (type == SchedulerItem::INTERVAL) {
|
||||
item->interval = delay;
|
||||
// first execution happens immediately after a random smallish offset
|
||||
uint32_t offset = this->calculate_interval_offset_(delay);
|
||||
offset = this->calculate_interval_offset_(delay);
|
||||
item->set_next_execution(now_64 + offset);
|
||||
#ifdef ESPHOME_LOG_HAS_VERBOSE
|
||||
SchedulerNameLog name_log;
|
||||
ESP_LOGV(TAG, "Scheduler interval for %s is %" PRIu32 "ms, offset %" PRIu32 "ms",
|
||||
name_log.format(name_type, static_name, hash_or_id), delay, offset);
|
||||
#endif
|
||||
} else {
|
||||
item->interval = 0;
|
||||
item->set_next_execution(now_64 + delay);
|
||||
@@ -213,6 +209,14 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#ifdef ESPHOME_LOG_HAS_VERBOSE
|
||||
// Logged after releasing the lock: a logger on_message automation can call back into the scheduler.
|
||||
if (type == SchedulerItem::INTERVAL) {
|
||||
SchedulerNameLog name_log;
|
||||
ESP_LOGV(TAG, "Scheduler interval for %s is %" PRIu32 "ms, offset %" PRIu32 "ms",
|
||||
name_log.format(name_type, static_name, hash_or_id), delay, offset);
|
||||
}
|
||||
#endif
|
||||
// A background insertion may shorten a sleep whose deadline was already computed.
|
||||
if (!is_main_loop_thread()) [[unlikely]]
|
||||
wake_scheduler_threadsafe();
|
||||
|
||||
+4
-3
@@ -7,6 +7,7 @@ import ipaddress
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
import platform
|
||||
import re
|
||||
import stat
|
||||
import sys
|
||||
@@ -40,9 +41,9 @@ FALSY_BOOL_STRINGS = frozenset({"false", "no", "off", "disable"})
|
||||
TRUTHY_ENV_STRINGS = TRUTHY_BOOL_STRINGS | {"1"}
|
||||
FALSY_ENV_STRINGS = FALSY_BOOL_STRINGS | {"0"}
|
||||
|
||||
IS_MACOS = sys.platform == "darwin"
|
||||
IS_WINDOWS = sys.platform == "win32"
|
||||
IS_LINUX = sys.platform.startswith("linux")
|
||||
IS_MACOS = platform.system() == "Darwin"
|
||||
IS_WINDOWS = platform.system() == "Windows"
|
||||
IS_LINUX = platform.system() == "Linux"
|
||||
|
||||
# FNV-1 hash constants (must match C++ in esphome/core/helpers.h)
|
||||
FNV1_OFFSET_BASIS = 2166136261
|
||||
|
||||
@@ -16,7 +16,6 @@ from esphome.core import EsphomeError
|
||||
("test_bk7231t.yaml", "BK7231T.*BLE 4.2"),
|
||||
("test_bk7252.yaml", "BK7251.*BLE 4.2"),
|
||||
("test_bk7231q.yaml", "BK7231Q.*no BLE"),
|
||||
("test_bk7238.yaml", "BK7238.*bootloader"),
|
||||
],
|
||||
)
|
||||
def test_unsupported_family_rejected(
|
||||
@@ -33,9 +32,11 @@ def test_unsupported_family_rejected(
|
||||
assert "cannot compile" in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.parametrize("config_file", ["test_bk7231n.yaml", "test_bk7238.yaml"])
|
||||
def test_ble5_family_generates(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
config_file: str,
|
||||
) -> None:
|
||||
main_cpp = generate_main(component_config_path("test_bk7231n.yaml"))
|
||||
main_cpp = generate_main(component_config_path(config_file))
|
||||
assert "bk72xx_ble::BK72xxBLE" in main_cpp
|
||||
@@ -0,0 +1,2 @@
|
||||
packages:
|
||||
bk72xx_ble: !include common.yaml
|
||||
@@ -30,11 +30,18 @@ class MockUARTComponent : public uart::UARTComponent {
|
||||
class TestableLD2450 : public LD2450Component {
|
||||
public:
|
||||
using LD2450Component::buffer_data_;
|
||||
using LD2450Component::bluetooth_on_;
|
||||
using LD2450Component::buffer_pos_;
|
||||
using LD2450Component::get_timeout_status_;
|
||||
using LD2450Component::mac_address_;
|
||||
using LD2450Component::readline_;
|
||||
using LD2450Component::timeout_;
|
||||
using LD2450Component::version_;
|
||||
using LD2450Component::zone_config_;
|
||||
using LD2450Component::zone_type_;
|
||||
#ifdef USE_NUMBER
|
||||
using LD2450Component::pref_;
|
||||
#endif
|
||||
|
||||
void feed(const std::vector<uint8_t> &data) {
|
||||
for (uint8_t byte : data) {
|
||||
|
||||
@@ -131,12 +131,23 @@ text_sensor:
|
||||
target_1:
|
||||
direction:
|
||||
name: Target-1 Direction
|
||||
xy:
|
||||
name: Target-1 XY
|
||||
target_2:
|
||||
direction:
|
||||
name: Target-2 Direction
|
||||
xy:
|
||||
name: Target-2 XY
|
||||
filters:
|
||||
- throttle_with_priority:
|
||||
timeout: 200ms
|
||||
value: NA
|
||||
target_3:
|
||||
direction:
|
||||
name: Target-3 Direction
|
||||
xy:
|
||||
name: Target-3 XY
|
||||
filters: []
|
||||
|
||||
number:
|
||||
- platform: ld2450
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
#include "common.h"
|
||||
#include "esphome/components/ld24xx/ld24xx.h"
|
||||
|
||||
namespace esphome::ld2450::testing {
|
||||
|
||||
namespace {
|
||||
// MAC query ACK (command 0xA5): header, length 10, command, result ok, 6 MAC bytes, footer.
|
||||
std::vector<uint8_t> make_mac_ack(const uint8_t (&mac)[MAC_ADDRESS_SIZE]) {
|
||||
std::vector<uint8_t> frame(std::begin(ld24xx::CMD_FRAME_HEADER), std::end(ld24xx::CMD_FRAME_HEADER));
|
||||
frame.insert(frame.end(), {0x0A, 0x00, 0xA5, 0x01, 0x00, 0x00});
|
||||
frame.insert(frame.end(), mac, mac + MAC_ADDRESS_SIZE);
|
||||
frame.insert(frame.end(), std::begin(ld24xx::CMD_FRAME_FOOTER), std::end(ld24xx::CMD_FRAME_FOOTER));
|
||||
return frame;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
class LD2450MacAckTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override { this->ld2450_.set_uart_parent(&this->mock_uart_); }
|
||||
std::string mac_str_() const {
|
||||
char buffer[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||
return ld24xx::format_mac_str(this->ld2450_.mac_address_, buffer);
|
||||
}
|
||||
|
||||
MockUARTComponent mock_uart_;
|
||||
TestableLD2450 ld2450_;
|
||||
};
|
||||
|
||||
TEST_F(LD2450MacAckTest, BluetoothOffAckClearsStoredMac) {
|
||||
// Captured from radar firmware 2.04.23101915; the first byte has the multicast bit set
|
||||
static constexpr uint8_t BEDROOM_MAC[MAC_ADDRESS_SIZE] = {0x81, 0x2C, 0x62, 0xBE, 0xC9, 0x96};
|
||||
this->ld2450_.feed(make_mac_ack(BEDROOM_MAC));
|
||||
EXPECT_TRUE(this->ld2450_.bluetooth_on_);
|
||||
EXPECT_THAT(this->ld2450_.mac_address_, ::testing::ElementsAreArray(BEDROOM_MAC));
|
||||
ASSERT_EQ(this->mac_str_(), "81:2C:62:BE:C9:96");
|
||||
|
||||
this->ld2450_.feed(make_mac_ack(ld24xx::NO_MAC));
|
||||
EXPECT_EQ(this->ld2450_.buffer_pos_, 0);
|
||||
EXPECT_FALSE(this->ld2450_.bluetooth_on_);
|
||||
EXPECT_THAT(this->ld2450_.mac_address_, ::testing::Each(0));
|
||||
}
|
||||
|
||||
} // namespace esphome::ld2450::testing
|
||||
@@ -0,0 +1,157 @@
|
||||
#ifdef USE_HOST
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include "common.h"
|
||||
#include "esphome/components/host/preferences.h"
|
||||
#include "esphome/components/number/number.h"
|
||||
#include "esphome/core/application.h"
|
||||
|
||||
namespace esphome::ld2450::testing {
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace {
|
||||
TestableLD2450 *hub = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
// What the generated apply="set_presence_timeout()", publish="before" number does
|
||||
void apply_presence_timeout(number::Number *entity, float value) {
|
||||
entity->publish_state(value);
|
||||
hub->set_presence_timeout();
|
||||
}
|
||||
using PresenceTimeoutNumber = number::ApplyNumber<apply_presence_timeout>;
|
||||
|
||||
// The presence_timeout part of LD2450Component::setup(), without its radar restart and scheduled info read
|
||||
void boot(TestableLD2450 &radar, number::Number *entity) {
|
||||
radar.set_presence_timeout_number(entity);
|
||||
if (entity != nullptr) {
|
||||
radar.pref_ = entity->make_entity_preference<float>();
|
||||
radar.set_presence_timeout();
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
class LD2450PresenceTimeoutTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
this->temp_dir_ = fs::temp_directory_path() / "esphome_ld2450_presence_timeout_test";
|
||||
fs::remove_all(this->temp_dir_);
|
||||
fs::create_directories(this->temp_dir_);
|
||||
setenv("ESPHOME_PREFDIR", this->temp_dir_.c_str(), 1);
|
||||
App.pre_setup("test_ld2450_timeout", strlen("test_ld2450_timeout"), "", 0);
|
||||
// A fresh store per test: the shared one keeps the file path of whichever test set it up first, whose directory
|
||||
// may already be gone, and its in-memory values
|
||||
host::host_preferences = &this->prefs_;
|
||||
global_preferences = &this->prefs_;
|
||||
this->ld2450_.set_uart_parent(&this->uart_);
|
||||
hub = &this->ld2450_;
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
hub = nullptr;
|
||||
host::setup_preferences();
|
||||
unsetenv("ESPHOME_PREFDIR");
|
||||
fs::remove_all(this->temp_dir_);
|
||||
}
|
||||
|
||||
/// Save a presence_timeout the way a previous boot did, and flush it
|
||||
void save_timeout(float seconds) {
|
||||
PresenceTimeoutNumber saved;
|
||||
ESPPreferenceObject pref = saved.make_entity_preference<float>();
|
||||
ASSERT_TRUE(pref.save(&seconds));
|
||||
ASSERT_TRUE(global_preferences->sync());
|
||||
}
|
||||
|
||||
/// Whether a frame without a target, now ms after the last one, clears presence
|
||||
bool cleared_after(uint32_t last_target_ms, uint32_t now_ms) {
|
||||
LoopBlockingGuard clock{nullptr, nullptr, now_ms};
|
||||
return this->ld2450_.get_timeout_status_(last_target_ms);
|
||||
}
|
||||
|
||||
host::HostPreferences prefs_;
|
||||
MockUARTComponent uart_;
|
||||
TestableLD2450 ld2450_;
|
||||
PresenceTimeoutNumber timeout_number_;
|
||||
fs::path temp_dir_;
|
||||
};
|
||||
|
||||
TEST_F(LD2450PresenceTimeoutTest, NoNumberKeepsDefaultHold) {
|
||||
boot(this->ld2450_, nullptr);
|
||||
|
||||
EXPECT_EQ(this->ld2450_.timeout_, 5000u);
|
||||
EXPECT_FALSE(this->cleared_after(1000, 5999));
|
||||
EXPECT_TRUE(this->cleared_after(1000, 6000));
|
||||
}
|
||||
|
||||
TEST_F(LD2450PresenceTimeoutTest, SetupWithNothingSavedPublishesDefault) {
|
||||
boot(this->ld2450_, &this->timeout_number_);
|
||||
|
||||
ASSERT_TRUE(this->timeout_number_.has_state());
|
||||
EXPECT_EQ(this->timeout_number_.state, 5.0f);
|
||||
EXPECT_EQ(this->ld2450_.timeout_, 5000u);
|
||||
}
|
||||
|
||||
TEST_F(LD2450PresenceTimeoutTest, SetupWithSavedZeroPublishesZero) {
|
||||
this->save_timeout(0);
|
||||
boot(this->ld2450_, &this->timeout_number_);
|
||||
|
||||
ASSERT_TRUE(this->timeout_number_.has_state());
|
||||
EXPECT_EQ(this->timeout_number_.state, 0.0f);
|
||||
EXPECT_EQ(this->ld2450_.timeout_, 0u);
|
||||
}
|
||||
|
||||
TEST_F(LD2450PresenceTimeoutTest, SetupWithSavedValuePublishesIt) {
|
||||
this->save_timeout(12);
|
||||
boot(this->ld2450_, &this->timeout_number_);
|
||||
|
||||
EXPECT_EQ(this->timeout_number_.state, 12.0f);
|
||||
EXPECT_EQ(this->ld2450_.timeout_, 12000u);
|
||||
}
|
||||
|
||||
TEST_F(LD2450PresenceTimeoutTest, ControlZeroKeepsZeroAndHoldsNothing) {
|
||||
boot(this->ld2450_, &this->timeout_number_);
|
||||
ASSERT_EQ(this->timeout_number_.state, 5.0f);
|
||||
|
||||
auto call = this->timeout_number_.make_call();
|
||||
call.set_value(0);
|
||||
call.perform();
|
||||
|
||||
// The old code restored the saved 5 s as soon as the state read 0
|
||||
EXPECT_EQ(this->timeout_number_.state, 0.0f);
|
||||
EXPECT_EQ(this->ld2450_.timeout_, 0u);
|
||||
EXPECT_TRUE(this->cleared_after(1000, 1000));
|
||||
EXPECT_TRUE(this->cleared_after(1000, 1001));
|
||||
}
|
||||
|
||||
TEST_F(LD2450PresenceTimeoutTest, ControlZeroIsSavedForTheNextBoot) {
|
||||
boot(this->ld2450_, &this->timeout_number_);
|
||||
|
||||
auto call = this->timeout_number_.make_call();
|
||||
call.set_value(0);
|
||||
call.perform();
|
||||
ASSERT_TRUE(global_preferences->sync());
|
||||
|
||||
TestableLD2450 rebooted;
|
||||
PresenceTimeoutNumber rebooted_number;
|
||||
rebooted.set_uart_parent(&this->uart_);
|
||||
hub = &rebooted;
|
||||
boot(rebooted, &rebooted_number);
|
||||
|
||||
ASSERT_TRUE(rebooted_number.has_state());
|
||||
EXPECT_EQ(rebooted_number.state, 0.0f);
|
||||
EXPECT_EQ(rebooted.timeout_, 0u);
|
||||
}
|
||||
|
||||
TEST_F(LD2450PresenceTimeoutTest, ControlNonZeroSetsHold) {
|
||||
boot(this->ld2450_, &this->timeout_number_);
|
||||
|
||||
auto call = this->timeout_number_.make_call();
|
||||
call.set_value(3);
|
||||
call.perform();
|
||||
|
||||
EXPECT_EQ(this->ld2450_.timeout_, 3000u);
|
||||
EXPECT_FALSE(this->cleared_after(1000, 3999));
|
||||
EXPECT_TRUE(this->cleared_after(1000, 4000));
|
||||
}
|
||||
|
||||
} // namespace esphome::ld2450::testing
|
||||
#endif // USE_HOST
|
||||
@@ -0,0 +1,55 @@
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include "esphome/components/ld24xx/ld24xx.h"
|
||||
|
||||
namespace esphome::ld24xx::testing {
|
||||
|
||||
static std::string format_mac(const uint8_t (&mac)[MAC_ADDRESS_SIZE]) {
|
||||
char buffer[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||
return format_mac_str(mac, buffer);
|
||||
}
|
||||
|
||||
TEST(LD24xxFormatMac, MulticastBitMacIsShownAsIs) {
|
||||
// First byte 0x81 has the multicast bit set, and the radar reports it as its Bluetooth MAC.
|
||||
const uint8_t mac[MAC_ADDRESS_SIZE] = {0x81, 0x2C, 0x62, 0xBE, 0xC9, 0x96};
|
||||
EXPECT_EQ(format_mac(mac), "81:2C:62:BE:C9:96");
|
||||
}
|
||||
|
||||
TEST(LD24xxFormatMac, UnicastMacIsShownAsIs) {
|
||||
const uint8_t mac[MAC_ADDRESS_SIZE] = {0x80, 0x2C, 0x62, 0xBE, 0xC9, 0x96};
|
||||
EXPECT_EQ(format_mac(mac), "80:2C:62:BE:C9:96");
|
||||
}
|
||||
|
||||
TEST(LD24xxFormatMac, AllZeroMacIsUnknown) {
|
||||
const uint8_t mac[MAC_ADDRESS_SIZE] = {0, 0, 0, 0, 0, 0};
|
||||
EXPECT_EQ(format_mac(mac), "unknown");
|
||||
}
|
||||
|
||||
TEST(LD24xxFormatMac, AllFfMacIsUnknown) {
|
||||
const uint8_t mac[MAC_ADDRESS_SIZE] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
|
||||
EXPECT_EQ(format_mac(mac), "unknown");
|
||||
}
|
||||
|
||||
TEST(LD24xxFormatMac, MacWithSomeZeroOrFfBytesIsShown) {
|
||||
const uint8_t mac[MAC_ADDRESS_SIZE] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x01};
|
||||
EXPECT_EQ(format_mac(mac), "00:00:00:00:00:01");
|
||||
const uint8_t ff_mac[MAC_ADDRESS_SIZE] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE};
|
||||
EXPECT_EQ(format_mac(ff_mac), "FF:FF:FF:FF:FF:FE");
|
||||
}
|
||||
|
||||
TEST(LD24xxStoreMac, ReportedMacIsStored) {
|
||||
const uint8_t reported[MAC_ADDRESS_SIZE] = {0x81, 0x2C, 0x62, 0xBE, 0xC9, 0x96};
|
||||
uint8_t mac[MAC_ADDRESS_SIZE] = {};
|
||||
EXPECT_TRUE(store_mac(reported, mac));
|
||||
EXPECT_THAT(mac, ::testing::ElementsAreArray(reported));
|
||||
}
|
||||
|
||||
TEST(LD24xxStoreMac, NoMacClearsStoredMac) {
|
||||
uint8_t mac[MAC_ADDRESS_SIZE] = {0x81, 0x2C, 0x62, 0xBE, 0xC9, 0x96};
|
||||
EXPECT_FALSE(store_mac(NO_MAC, mac));
|
||||
EXPECT_THAT(mac, ::testing::Each(0));
|
||||
}
|
||||
|
||||
} // namespace esphome::ld24xx::testing
|
||||
@@ -182,6 +182,29 @@ climate:
|
||||
name: LG Climate
|
||||
transmitter_id: xmitr
|
||||
|
||||
datetime:
|
||||
- platform: template
|
||||
name: Date
|
||||
id: test_date
|
||||
type: date
|
||||
initial_value: "2000-1-2"
|
||||
set_action:
|
||||
- logger.log: "set_value"
|
||||
- platform: template
|
||||
name: Time
|
||||
id: test_time
|
||||
type: time
|
||||
initial_value: "12:34:56am"
|
||||
set_action:
|
||||
- logger.log: "set_value"
|
||||
- platform: template
|
||||
name: DateTime
|
||||
id: test_datetime
|
||||
type: datetime
|
||||
initial_value: "2000-1-2 12:34:56"
|
||||
set_action:
|
||||
- logger.log: "set_value"
|
||||
|
||||
prometheus:
|
||||
include_internal: true
|
||||
relabel:
|
||||
@@ -191,3 +214,12 @@ prometheus:
|
||||
template_text_sensor1:
|
||||
id: hello_text
|
||||
name: Text Substitution
|
||||
test_date:
|
||||
id: prometheus_date
|
||||
name: Prometheus Date
|
||||
test_time:
|
||||
id: prometheus_time
|
||||
name: Prometheus Time
|
||||
test_datetime:
|
||||
id: prometheus_datetime
|
||||
name: Prometheus DateTime
|
||||
@@ -3,21 +3,19 @@
|
||||
#include "esphome/components/runtime_image/jpeg_decoder.h"
|
||||
#include "esphome/components/runtime_image/runtime_image.h"
|
||||
|
||||
#include <utility>
|
||||
|
||||
#ifdef USE_RUNTIME_IMAGE_JPEG
|
||||
|
||||
namespace esphome::runtime_image::testing {
|
||||
|
||||
// A decoder whose JPEGDEC state is missing, as when its allocation fails. The state is handed back before the base
|
||||
// destructor runs, so the teardown stays with JpegDecoder.
|
||||
// A decoder whose JPEGDEC state is missing, as when its allocation fails. The state moves to state_, which frees it.
|
||||
class StatelessJpegDecoder : public JpegDecoder {
|
||||
public:
|
||||
explicit StatelessJpegDecoder(RuntimeImage *image) : JpegDecoder(image), state_(this->jpeg_) {
|
||||
this->jpeg_ = nullptr;
|
||||
}
|
||||
~StatelessJpegDecoder() override { this->jpeg_ = this->state_; }
|
||||
explicit StatelessJpegDecoder(RuntimeImage *image) : JpegDecoder(image), state_(std::move(this->jpeg_)) {}
|
||||
|
||||
protected:
|
||||
JPEGDEC *state_;
|
||||
RAMUniquePtr<JPEGDEC> state_;
|
||||
};
|
||||
|
||||
// The ~18 KB of JPEGDEC state lives in its own allocation, so it can come from PSRAM
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
esphome:
|
||||
name: sched-verbose-log
|
||||
|
||||
host:
|
||||
api:
|
||||
logger:
|
||||
level: VERBOSE
|
||||
on_message:
|
||||
# The scheduler logs each new interval at VERBOSE. This automation then
|
||||
# calls back into the scheduler (the delay is a scheduler timeout).
|
||||
level: VERBOSE
|
||||
then:
|
||||
- delay: 1ms
|
||||
|
||||
globals:
|
||||
- id: fire_count
|
||||
type: int
|
||||
initial_value: "0"
|
||||
|
||||
interval:
|
||||
- interval: 50ms
|
||||
then:
|
||||
- lambda: |-
|
||||
id(fire_count) += 1;
|
||||
if (id(fire_count) == 5) {
|
||||
ESP_LOGI("test", "INTERVAL_5_FIRES_REACHED");
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
esphome:
|
||||
name: uart-mock-ld2450-target-xy
|
||||
|
||||
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 ld2450's DEPENDENCIES = ["uart"]
|
||||
# The actual UART bus used is the uart_mock component below
|
||||
uart:
|
||||
baud_rate: 115200
|
||||
port: /dev/null
|
||||
|
||||
uart_mock:
|
||||
id: mock_uart
|
||||
baud_rate: 256000
|
||||
auto_start: false
|
||||
responses:
|
||||
# Generic ACK for any command, to unblock setup commands
|
||||
- expect_tx: [0x04, 0x03, 0x02, 0x01]
|
||||
inject_rx:
|
||||
[
|
||||
0xFD, 0xFC, 0xFB, 0xFA,
|
||||
0x04, 0x00,
|
||||
0xFF, 0x01,
|
||||
0x00, 0x00,
|
||||
0x04, 0x03, 0x02, 0x01,
|
||||
]
|
||||
|
||||
# One frame every 100 ms, as the radar sends them. Coordinates are sign-magnitude, with the high bit set for
|
||||
# positive values.
|
||||
injections:
|
||||
# Targets 1 and 2 move, one step per frame. Target 3 moves for 3 frames, then leaves while its
|
||||
# later positions are still held back by the throttle
|
||||
- delay: 100ms
|
||||
inject_rx:
|
||||
[
|
||||
0xAA, 0xFF, 0x03, 0x00,
|
||||
0x58, 0x02, 0xE8, 0x83, 0x00, 0x00, 0x64, 0x00,
|
||||
0xC8, 0x80, 0xD0, 0x87, 0x00, 0x00, 0x64, 0x00,
|
||||
0x2C, 0x01, 0xB8, 0x8B, 0x00, 0x00, 0x64, 0x00,
|
||||
0x55, 0xCC,
|
||||
]
|
||||
- delay: 100ms
|
||||
inject_rx:
|
||||
[
|
||||
0xAA, 0xFF, 0x03, 0x00,
|
||||
0xF4, 0x01, 0x1A, 0x84, 0x00, 0x00, 0x64, 0x00,
|
||||
0x18, 0x81, 0xEE, 0x87, 0x00, 0x00, 0x64, 0x00,
|
||||
0xFA, 0x00, 0xEA, 0x8B, 0x00, 0x00, 0x64, 0x00,
|
||||
0x55, 0xCC,
|
||||
]
|
||||
- delay: 100ms
|
||||
inject_rx:
|
||||
[
|
||||
0xAA, 0xFF, 0x03, 0x00,
|
||||
0x90, 0x01, 0x4C, 0x84, 0x00, 0x00, 0x64, 0x00,
|
||||
0x68, 0x81, 0x0C, 0x88, 0x00, 0x00, 0x64, 0x00,
|
||||
0xC8, 0x00, 0x1C, 0x8C, 0x00, 0x00, 0x64, 0x00,
|
||||
0x55, 0xCC,
|
||||
]
|
||||
- delay: 100ms
|
||||
inject_rx:
|
||||
[
|
||||
0xAA, 0xFF, 0x03, 0x00,
|
||||
0x2C, 0x01, 0x7E, 0x84, 0x00, 0x00, 0x64, 0x00,
|
||||
0xB8, 0x81, 0x2A, 0x88, 0x00, 0x00, 0x64, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x55, 0xCC,
|
||||
]
|
||||
- delay: 100ms
|
||||
inject_rx:
|
||||
[
|
||||
0xAA, 0xFF, 0x03, 0x00,
|
||||
0xC8, 0x00, 0xB0, 0x84, 0x00, 0x00, 0x64, 0x00,
|
||||
0x08, 0x82, 0x48, 0x88, 0x00, 0x00, 0x64, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x55, 0xCC,
|
||||
]
|
||||
- delay: 100ms
|
||||
inject_rx:
|
||||
[
|
||||
0xAA, 0xFF, 0x03, 0x00,
|
||||
0x64, 0x00, 0xE2, 0x84, 0x00, 0x00, 0x64, 0x00,
|
||||
0x58, 0x82, 0x66, 0x88, 0x00, 0x00, 0x64, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x55, 0xCC,
|
||||
]
|
||||
- delay: 100ms
|
||||
inject_rx:
|
||||
[
|
||||
0xAA, 0xFF, 0x03, 0x00,
|
||||
0x00, 0x00, 0x14, 0x85, 0x00, 0x00, 0x64, 0x00,
|
||||
0xA8, 0x82, 0x84, 0x88, 0x00, 0x00, 0x64, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x55, 0xCC,
|
||||
]
|
||||
- delay: 100ms
|
||||
inject_rx:
|
||||
[
|
||||
0xAA, 0xFF, 0x03, 0x00,
|
||||
0x64, 0x80, 0x46, 0x85, 0x00, 0x00, 0x64, 0x00,
|
||||
0xF8, 0x82, 0xA2, 0x88, 0x00, 0x00, 0x64, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x55, 0xCC,
|
||||
]
|
||||
- delay: 100ms
|
||||
inject_rx:
|
||||
[
|
||||
0xAA, 0xFF, 0x03, 0x00,
|
||||
0xC8, 0x80, 0x78, 0x85, 0x00, 0x00, 0x64, 0x00,
|
||||
0x48, 0x83, 0xC0, 0x88, 0x00, 0x00, 0x64, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x55, 0xCC,
|
||||
]
|
||||
- delay: 100ms
|
||||
inject_rx:
|
||||
[
|
||||
0xAA, 0xFF, 0x03, 0x00,
|
||||
0x2C, 0x81, 0xAA, 0x85, 0x00, 0x00, 0x64, 0x00,
|
||||
0x98, 0x83, 0xDE, 0x88, 0x00, 0x00, 0x64, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x55, 0xCC,
|
||||
]
|
||||
- delay: 100ms
|
||||
inject_rx:
|
||||
[
|
||||
0xAA, 0xFF, 0x03, 0x00,
|
||||
0x90, 0x81, 0xDC, 0x85, 0x00, 0x00, 0x64, 0x00,
|
||||
0xE8, 0x83, 0xFC, 0x88, 0x00, 0x00, 0x64, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x55, 0xCC,
|
||||
]
|
||||
- delay: 100ms
|
||||
inject_rx:
|
||||
[
|
||||
0xAA, 0xFF, 0x03, 0x00,
|
||||
0xF4, 0x81, 0x0E, 0x86, 0x00, 0x00, 0x64, 0x00,
|
||||
0x38, 0x84, 0x1A, 0x89, 0x00, 0x00, 0x64, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x55, 0xCC,
|
||||
]
|
||||
# Targets 1 and 2 stand still for 1.2 s (repeated frames are ignored), then leave
|
||||
- delay: 1300ms
|
||||
inject_rx:
|
||||
[
|
||||
0xAA, 0xFF, 0x03, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x55, 0xCC,
|
||||
]
|
||||
|
||||
ld2450:
|
||||
id: ld2450_dev
|
||||
uart_id: mock_uart
|
||||
|
||||
text_sensor:
|
||||
- platform: ld2450
|
||||
ld2450_id: ld2450_dev
|
||||
target_1:
|
||||
xy:
|
||||
name: "Target 1 XY"
|
||||
filters:
|
||||
- timeout:
|
||||
timeout: 500ms
|
||||
value: last
|
||||
- throttle_with_priority:
|
||||
timeout: 500ms
|
||||
value: NA
|
||||
target_2:
|
||||
xy:
|
||||
name: "Target 2 XY"
|
||||
filters: []
|
||||
target_3:
|
||||
xy:
|
||||
name: "Target 3 XY"
|
||||
filters:
|
||||
- timeout:
|
||||
timeout: 500ms
|
||||
value: last
|
||||
- throttle_with_priority:
|
||||
timeout: 500ms
|
||||
value: NA
|
||||
|
||||
button:
|
||||
- platform: template
|
||||
name: "Start Scenario"
|
||||
id: start_scenario_btn
|
||||
on_press:
|
||||
- lambda: 'id(mock_uart).start_scenario();'
|
||||
@@ -0,0 +1,34 @@
|
||||
"""Test that a logger on_message automation can use the scheduler from a scheduler log.
|
||||
|
||||
Regression test for a self-deadlock: Scheduler::set_timer_common_ logged a new
|
||||
interval at VERBOSE while holding the scheduler lock. The logger calls on_message
|
||||
listeners on the same thread, and an automation with a delay adds a scheduler
|
||||
timeout, which took the same non-recursive lock again. The first set_interval()
|
||||
in setup() hung the main loop.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from .log_utils import LineWaiter
|
||||
from .state_utils import assert_device_name
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_scheduler_verbose_log_on_message(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""The main loop keeps running when on_message reacts to a scheduler log."""
|
||||
waiter = LineWaiter()
|
||||
|
||||
async with (
|
||||
run_compiled(yaml_config, line_callback=waiter.callback),
|
||||
api_client_connected() as client,
|
||||
):
|
||||
await assert_device_name(client, "sched-verbose-log")
|
||||
await waiter.wait_for("Scheduler interval for", timeout=5.0)
|
||||
await waiter.wait_for("INTERVAL_5_FIRES_REACHED", timeout=5.0)
|
||||
@@ -0,0 +1,87 @@
|
||||
"""Integration test for the LD2450 target xy text sensor, with and without a throttle."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from itertools import groupby
|
||||
|
||||
from aioesphomeapi import ButtonInfo, EntityState, TextSensorInfo, TextSensorState
|
||||
import pytest
|
||||
|
||||
from .state_utils import require_entity, subscribe_after_initial_states
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
|
||||
# The positions of each target in the fixture, one per radar frame
|
||||
TARGET_1 = [f"{-600 + 100 * i},{1000 + 50 * i}" for i in range(12)]
|
||||
TARGET_2 = [f"{200 + 80 * i},{2000 + 30 * i}" for i in range(12)]
|
||||
# Target 3 moves for 3 frames (300 ms) and leaves, with a 500 ms throttle
|
||||
TARGET_3 = [f"{-300 + 50 * i},{3000 + 50 * i}" for i in range(3)]
|
||||
# Frames are 100 ms apart: "NA" must come well before the 500 ms throttle ends
|
||||
TARGET_3_LEAVES_WITHIN_S = 0.45
|
||||
|
||||
|
||||
def _distinct(states: list[str]) -> list[str]:
|
||||
"""Drop the repeat that the timeout filter sends after a pause."""
|
||||
return [state for state, _ in groupby(states)]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_uart_mock_ld2450_target_xy(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Test the target xy text sensor with and without a throttle."""
|
||||
async with (
|
||||
run_compiled(yaml_config),
|
||||
api_client_connected() as client,
|
||||
):
|
||||
entities, _ = await client.list_entities_services()
|
||||
target_1 = require_entity(entities, "target_1_xy", TextSensorInfo)
|
||||
target_2 = require_entity(entities, "target_2_xy", TextSensorInfo)
|
||||
target_3 = require_entity(entities, "target_3_xy", TextSensorInfo)
|
||||
history: dict[int, list[str]] = {
|
||||
target.key: [] for target in (target_1, target_2, target_3)
|
||||
}
|
||||
target_3_times: list[float] = []
|
||||
loop = asyncio.get_running_loop()
|
||||
both_left = loop.create_future()
|
||||
|
||||
def on_state(state: EntityState) -> None:
|
||||
if not isinstance(state, TextSensorState) or state.key not in history:
|
||||
return
|
||||
history[state.key].append(state.state)
|
||||
if state.key == target_3.key:
|
||||
target_3_times.append(loop.time())
|
||||
# Target 3 left long before, so only targets 1 and 2 end the scenario
|
||||
left = all(
|
||||
history[key] and history[key][-1] == "NA"
|
||||
for key in (target_1.key, target_2.key)
|
||||
)
|
||||
if left and not both_left.done():
|
||||
both_left.set_result(None)
|
||||
|
||||
await subscribe_after_initial_states(client, entities, on_state=on_state)
|
||||
|
||||
start_btn = require_entity(entities, "start_scenario", ButtonInfo)
|
||||
client.button_command(start_btn.key)
|
||||
await asyncio.wait_for(both_left, timeout=10.0)
|
||||
|
||||
# Throttled: the first position at once, then a few, in order, and the final one before the target leaves
|
||||
throttled = _distinct(history[target_1.key])
|
||||
assert throttled[-1] == "NA"
|
||||
positions = throttled[:-1]
|
||||
assert positions[0] == TARGET_1[0]
|
||||
assert positions[-1] == TARGET_1[-1], (
|
||||
"the final position was held back for good"
|
||||
)
|
||||
indexes = [TARGET_1.index(position) for position in positions]
|
||||
assert indexes == sorted(set(indexes)), f"out of order or repeated: {positions}"
|
||||
assert 3 <= len(positions) <= 6, f"expected a few updates, got {positions}"
|
||||
|
||||
# Not throttled: every change is published
|
||||
assert history[target_2.key] == [*TARGET_2, "NA"]
|
||||
|
||||
# "NA" bypasses the throttle and drops the held position: nothing follows it
|
||||
assert _distinct(history[target_3.key]) == [TARGET_3[0], "NA"]
|
||||
assert target_3_times[1] - target_3_times[0] < TARGET_3_LEAVES_WITHIN_S
|
||||
@@ -94,22 +94,6 @@ def _leaked_heavy_modules(module: str, extra: tuple[str, ...] = ()) -> str:
|
||||
return result.stdout.strip()
|
||||
|
||||
|
||||
def test_main_module_does_not_query_the_os_version() -> None:
|
||||
"""``import esphome.__main__`` must not call ``platform.uname()`` (or ``system()``, which uses it).
|
||||
|
||||
On Windows they query WMI and fall back to running ``ver`` (importing
|
||||
subprocess) when that fails, so the stdlib leak check below went flaky.
|
||||
"""
|
||||
check = (
|
||||
"import platform\n"
|
||||
"def fail(*args, **kwargs):\n"
|
||||
" raise AssertionError('platform queried at import time')\n"
|
||||
"platform.uname = fail\n"
|
||||
"import esphome.__main__"
|
||||
)
|
||||
subprocess.run([sys.executable, "-c", check], check=True)
|
||||
|
||||
|
||||
def test_main_module_does_not_import_heavy_modules() -> None:
|
||||
"""A bare ``import esphome.__main__`` must not drag in validation/codegen.
|
||||
|
||||
|
||||
Reference in new issue
Block a user