mirror of
https://github.com/esphome/esphome.git
synced 2026-10-03 17:55:57 +00:00
Merge branch 'dev' into remove_posix_tz_parser
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
"""Tests for the binary sensor component."""
|
||||
|
||||
from tests.component_tests.helpers import INTERNAL_BIT, extract_packed_value
|
||||
|
||||
|
||||
def test_binary_sensor_is_setup(generate_main):
|
||||
"""
|
||||
@@ -29,7 +31,7 @@ def test_binary_sensor_sets_mandatory_fields(generate_main):
|
||||
)
|
||||
|
||||
# Then
|
||||
assert 'bs_1->set_name("test bs1",' in main_cpp
|
||||
assert 'bs_1->configure_entity_("test bs1",' in main_cpp
|
||||
assert "bs_1->set_pin(" in main_cpp
|
||||
|
||||
|
||||
@@ -44,9 +46,9 @@ def test_binary_sensor_config_value_internal_set(generate_main):
|
||||
"tests/component_tests/binary_sensor/test_binary_sensor.yaml"
|
||||
)
|
||||
|
||||
# Then
|
||||
assert "bs_1->set_internal(true);" in main_cpp
|
||||
assert "bs_2->set_internal(false);" in main_cpp
|
||||
# Then: bs_1 has internal: true, bs_2 has internal: false
|
||||
assert extract_packed_value(main_cpp, "bs_1") & INTERNAL_BIT != 0
|
||||
assert extract_packed_value(main_cpp, "bs_2") & INTERNAL_BIT == 0
|
||||
|
||||
|
||||
def test_binary_sensor_config_value_use_raw_set(generate_main):
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Tests for the button component"""
|
||||
|
||||
from tests.component_tests.helpers import INTERNAL_BIT, extract_packed_value
|
||||
|
||||
|
||||
def test_button_is_setup(generate_main):
|
||||
"""
|
||||
@@ -26,7 +28,7 @@ def test_button_sets_mandatory_fields(generate_main):
|
||||
main_cpp = generate_main("tests/component_tests/button/test_button.yaml")
|
||||
|
||||
# Then
|
||||
assert 'wol_1->set_name("wol_test_1",' in main_cpp
|
||||
assert 'wol_1->configure_entity_("wol_test_1",' in main_cpp
|
||||
assert "wol_2->set_macaddr(18, 52, 86, 120, 144, 171);" in main_cpp
|
||||
|
||||
|
||||
@@ -39,6 +41,6 @@ def test_button_config_value_internal_set(generate_main):
|
||||
# When
|
||||
main_cpp = generate_main("tests/component_tests/button/test_button.yaml")
|
||||
|
||||
# Then
|
||||
assert "wol_1->set_internal(true);" in main_cpp
|
||||
assert "wol_2->set_internal(false);" in main_cpp
|
||||
# Then: wol_1 has internal: true, wol_2 has internal: false
|
||||
assert extract_packed_value(main_cpp, "wol_1") & INTERNAL_BIT != 0
|
||||
assert extract_packed_value(main_cpp, "wol_2") & INTERNAL_BIT == 0
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
"""Shared helpers for component tests."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
INTERNAL_BIT = 1 << 24
|
||||
|
||||
|
||||
def extract_packed_value(main_cpp: str, var_name: str) -> int:
|
||||
"""Extract the third (packed) argument from a configure_entity_ call."""
|
||||
pattern = (
|
||||
rf"{re.escape(var_name)}->configure_entity_\("
|
||||
r'"(?:\\.|[^"\\])*"'
|
||||
r",\s*\w+,\s*(\d+)\)"
|
||||
)
|
||||
match = re.search(pattern, main_cpp)
|
||||
assert match, f"configure_entity_ call not found for {var_name}"
|
||||
return int(match.group(1))
|
||||
@@ -0,0 +1,50 @@
|
||||
"""Tests for the logger component."""
|
||||
|
||||
import re
|
||||
|
||||
|
||||
def test_logger_pre_setup_before_other_components(generate_main):
|
||||
"""Logger::pre_setup() must be called before any other component is created.
|
||||
|
||||
Log functions call global_logger->log_vprintf_() without a null check,
|
||||
so global_logger must be set before anything can log.
|
||||
"""
|
||||
main_cpp = generate_main("tests/component_tests/logger/test_logger.yaml")
|
||||
|
||||
# Find the logger's pre_setup() call specifically
|
||||
logger_pre_setup = re.search(r"logger_logger->pre_setup\(\)", main_cpp)
|
||||
if logger_pre_setup is None:
|
||||
# Fall back to finding any logger-related pre_setup
|
||||
logger_pre_setup = re.search(r"logger\w*->pre_setup\(\)", main_cpp)
|
||||
assert logger_pre_setup is not None, (
|
||||
"Logger pre_setup() not found in generated code"
|
||||
)
|
||||
|
||||
# Find all "new " allocations (component creation)
|
||||
new_allocations = list(re.finditer(r"\bnew [\w:]+", main_cpp))
|
||||
assert len(new_allocations) > 0, "No component allocations found"
|
||||
|
||||
# Separate logger and non-logger allocations
|
||||
logger_allocs = [a for a in new_allocations if "logger" in a.group().lower()]
|
||||
non_logger_allocs = [
|
||||
a
|
||||
for a in new_allocations
|
||||
if "logger" not in a.group().lower()
|
||||
# Skip placement new for App
|
||||
and "(&App)" not in main_cpp[max(0, a.start() - 5) : a.start()]
|
||||
]
|
||||
|
||||
assert len(logger_allocs) > 0, (
|
||||
f"Logger allocation not found in: {[a.group() for a in new_allocations]}"
|
||||
)
|
||||
assert len(non_logger_allocs) > 0, (
|
||||
"No non-logger component allocations found — "
|
||||
"add a component to test_logger.yaml so the ordering check is meaningful"
|
||||
)
|
||||
|
||||
# All non-logger allocations must appear after logger pre_setup()
|
||||
for alloc in non_logger_allocs:
|
||||
assert alloc.start() > logger_pre_setup.start(), (
|
||||
f"Component allocation '{alloc.group()}' at position {alloc.start()} "
|
||||
f"appears before logger pre_setup() at position {logger_pre_setup.start()}"
|
||||
)
|
||||
@@ -0,0 +1,14 @@
|
||||
---
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp8266:
|
||||
board: d1_mini_lite
|
||||
|
||||
logger:
|
||||
level: DEBUG
|
||||
|
||||
# Need at least one non-logger component so the ordering test
|
||||
# can verify that logger pre_setup() comes before other allocations.
|
||||
preferences:
|
||||
flash_write_interval: 1min
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Tests for the sensor component."""
|
||||
|
||||
from tests.component_tests.helpers import extract_packed_value
|
||||
|
||||
|
||||
def test_sensor_device_class_set(generate_main):
|
||||
"""
|
||||
@@ -10,5 +12,6 @@ def test_sensor_device_class_set(generate_main):
|
||||
# When
|
||||
main_cpp = generate_main("tests/component_tests/sensor/test_sensor.yaml")
|
||||
|
||||
# Then
|
||||
assert 's_1->set_device_class("voltage");' in main_cpp
|
||||
# Then: device_class: voltage means packed value must be non-zero
|
||||
packed = extract_packed_value(main_cpp, "s_1")
|
||||
assert packed != 0
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
"""Tests for the binary sensor component."""
|
||||
"""Tests for the text component."""
|
||||
|
||||
from tests.component_tests.helpers import INTERNAL_BIT, extract_packed_value
|
||||
|
||||
|
||||
def test_text_is_setup(generate_main):
|
||||
@@ -25,7 +27,7 @@ def test_text_sets_mandatory_fields(generate_main):
|
||||
main_cpp = generate_main("tests/component_tests/text/test_text.yaml")
|
||||
|
||||
# Then
|
||||
assert 'it_1->set_name("test 1 text",' in main_cpp
|
||||
assert 'it_1->configure_entity_("test 1 text",' in main_cpp
|
||||
|
||||
|
||||
def test_text_config_value_internal_set(generate_main):
|
||||
@@ -37,9 +39,9 @@ def test_text_config_value_internal_set(generate_main):
|
||||
# When
|
||||
main_cpp = generate_main("tests/component_tests/text/test_text.yaml")
|
||||
|
||||
# Then
|
||||
assert "it_2->set_internal(false);" in main_cpp
|
||||
assert "it_3->set_internal(true);" in main_cpp
|
||||
# Then: it_2 has internal: false, it_3 has internal: true
|
||||
assert extract_packed_value(main_cpp, "it_2") & INTERNAL_BIT == 0
|
||||
assert extract_packed_value(main_cpp, "it_3") & INTERNAL_BIT != 0
|
||||
|
||||
|
||||
def test_text_config_value_mode_set(generate_main):
|
||||
@@ -66,5 +68,20 @@ def test_text_config_lamda_is_set(generate_main):
|
||||
main_cpp = generate_main("tests/component_tests/text/test_text.yaml")
|
||||
|
||||
# Then
|
||||
assert "it_4->set_template([]() -> esphome::optional<std::string> {" in main_cpp
|
||||
assert "it_4->set_template([]() -> std::optional<std::string> {" in main_cpp
|
||||
assert 'return std::string{"Hello"};' in main_cpp
|
||||
|
||||
|
||||
def test_esphome_optional_alias_works(generate_main):
|
||||
"""
|
||||
Test that esphome::optional alias compiles (backward compatibility)
|
||||
"""
|
||||
# Given
|
||||
|
||||
# When
|
||||
main_cpp = generate_main("tests/component_tests/text/test_text.yaml")
|
||||
|
||||
# Then
|
||||
# Codegen emits std::optional, but esphome::optional must also work
|
||||
# via the using alias in esphome/core/optional.h
|
||||
assert "std::optional<std::string>" in main_cpp
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Tests for the text sensor component."""
|
||||
|
||||
from tests.component_tests.helpers import INTERNAL_BIT, extract_packed_value
|
||||
|
||||
|
||||
def test_text_sensor_is_setup(generate_main):
|
||||
"""
|
||||
@@ -25,9 +27,9 @@ def test_text_sensor_sets_mandatory_fields(generate_main):
|
||||
main_cpp = generate_main("tests/component_tests/text_sensor/test_text_sensor.yaml")
|
||||
|
||||
# Then
|
||||
assert 'ts_1->set_name("Template Text Sensor 1",' in main_cpp
|
||||
assert 'ts_2->set_name("Template Text Sensor 2",' in main_cpp
|
||||
assert 'ts_3->set_name("Template Text Sensor 3",' in main_cpp
|
||||
assert 'ts_1->configure_entity_("Template Text Sensor 1",' in main_cpp
|
||||
assert 'ts_2->configure_entity_("Template Text Sensor 2",' in main_cpp
|
||||
assert 'ts_3->configure_entity_("Template Text Sensor 3",' in main_cpp
|
||||
|
||||
|
||||
def test_text_sensor_config_value_internal_set(generate_main):
|
||||
@@ -39,9 +41,9 @@ def test_text_sensor_config_value_internal_set(generate_main):
|
||||
# When
|
||||
main_cpp = generate_main("tests/component_tests/text_sensor/test_text_sensor.yaml")
|
||||
|
||||
# Then
|
||||
assert "ts_2->set_internal(true);" in main_cpp
|
||||
assert "ts_3->set_internal(false);" in main_cpp
|
||||
# Then: ts_2 has internal: true, ts_3 has internal: false
|
||||
assert extract_packed_value(main_cpp, "ts_2") & INTERNAL_BIT != 0
|
||||
assert extract_packed_value(main_cpp, "ts_3") & INTERNAL_BIT == 0
|
||||
|
||||
|
||||
def test_text_sensor_device_class_set(generate_main):
|
||||
@@ -53,6 +55,9 @@ def test_text_sensor_device_class_set(generate_main):
|
||||
# When
|
||||
main_cpp = generate_main("tests/component_tests/text_sensor/test_text_sensor.yaml")
|
||||
|
||||
# Then
|
||||
assert 'ts_2->set_device_class("timestamp");' in main_cpp
|
||||
assert 'ts_3->set_device_class("date");' in main_cpp
|
||||
# Then: ts_2 has device_class: timestamp, ts_3 has device_class: date
|
||||
# so their packed values must be non-zero
|
||||
packed_ts_2 = extract_packed_value(main_cpp, "ts_2")
|
||||
assert packed_ts_2 != 0
|
||||
packed_ts_3 = extract_packed_value(main_cpp, "ts_3")
|
||||
assert packed_ts_3 != 0
|
||||
|
||||
@@ -7,10 +7,23 @@
|
||||
testing binaries that combine many components. By convention, this unique namespace is `esphome::component::testing`
|
||||
(where "component" is the component under test), for example: `esphome::uart::testing`.
|
||||
|
||||
### Platform components
|
||||
|
||||
For components that expose to a platform component, create a folder under your component test folder with the platform component name, e.g. `binary_sensor` and
|
||||
include the relevant `.cpp` and `.h` test files there.
|
||||
|
||||
### Override component code generation for testing
|
||||
|
||||
When generating code for testing, ESPHome won't invoke the component's `to_code` function, since most components do not
|
||||
need to generate configuration code for testing.
|
||||
|
||||
If you do need to generate code to for example configure compilation flags or add libraries,
|
||||
add the component name to the `CPP_TESTING_CODEGEN_COMPONENTS` allowlist in `script/cpp_unit_test.py`.
|
||||
|
||||
## Running component unit tests
|
||||
|
||||
(from the repository root)
|
||||
|
||||
```bash
|
||||
./script/cpp_unit_test.py component1 component2 ...
|
||||
```
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<<: !include common.yaml
|
||||
|
||||
wifi:
|
||||
ssid: MySSID
|
||||
password: password1
|
||||
@@ -0,0 +1,9 @@
|
||||
audio_file:
|
||||
- id: test_audio
|
||||
file:
|
||||
type: local
|
||||
path: $component_dir/test.wav
|
||||
|
||||
media_source:
|
||||
- platform: audio_file
|
||||
id: audio_file_source
|
||||
@@ -0,0 +1 @@
|
||||
<<: !include common.yaml
|
||||
Binary file not shown.
@@ -0,0 +1,4 @@
|
||||
ble_nus:
|
||||
type: uart
|
||||
tx_buffer_size: 160
|
||||
rx_buffer_size: 160
|
||||
@@ -0,0 +1 @@
|
||||
<<: !include common.yaml
|
||||
@@ -0,0 +1,19 @@
|
||||
sensor:
|
||||
- platform: dew_point
|
||||
name: Dew Point
|
||||
temperature: template_temperature
|
||||
humidity: template_humidity
|
||||
- platform: template
|
||||
id: template_humidity
|
||||
lambda: |-
|
||||
if (millis() > 10000) {
|
||||
return 0.6;
|
||||
}
|
||||
return 0.0;
|
||||
- platform: template
|
||||
id: template_temperature
|
||||
lambda: |-
|
||||
if (millis() > 10000) {
|
||||
return 42.0;
|
||||
}
|
||||
return 0.0;
|
||||
@@ -0,0 +1 @@
|
||||
<<: !include common.yaml
|
||||
@@ -0,0 +1 @@
|
||||
<<: !include common.yaml
|
||||
@@ -0,0 +1 @@
|
||||
<<: !include common.yaml
|
||||
@@ -1,3 +1,6 @@
|
||||
esp8266:
|
||||
enable_full_printf: false
|
||||
|
||||
logger:
|
||||
level: VERBOSE
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
external_components:
|
||||
- source: github://esphome/esphome@dev
|
||||
- id: my_ext
|
||||
source: github://esphome/esphome@dev
|
||||
refresh: 1d
|
||||
components: [bh1750]
|
||||
- source: ../../../esphome/components
|
||||
- id: my_local
|
||||
source: ../../../esphome/components
|
||||
components: [sntp]
|
||||
|
||||
@@ -1 +1,5 @@
|
||||
# WARNING: Using !extend or !remove prevents automatic component grouping in CI, making builds slower.
|
||||
<<: !include common.yaml
|
||||
|
||||
external_components:
|
||||
- id: !remove my_local
|
||||
|
||||
@@ -27,3 +27,14 @@ globals:
|
||||
type: bool
|
||||
restore_value: false
|
||||
initial_value: "false"
|
||||
# Test restore_value with string "false" - should be converted to bool false
|
||||
- id: glob_no_restore_string_false
|
||||
type: int
|
||||
restore_value: "false"
|
||||
initial_value: "42"
|
||||
# Test restore_value with string "true" - should be converted to bool true
|
||||
- id: glob_restore_string_true
|
||||
type: int
|
||||
restore_value: "true"
|
||||
initial_value: "99"
|
||||
update_interval: 5s
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
substitutions:
|
||||
verify_ssl: "false"
|
||||
|
||||
<<: !include common.yaml
|
||||
|
||||
http_request:
|
||||
verify_ssl: false
|
||||
tls_buffer_size_rx: 16384
|
||||
tls_buffer_size_tx: 512
|
||||
|
||||
@@ -1,9 +1,19 @@
|
||||
esphome:
|
||||
on_boot:
|
||||
then:
|
||||
- sensor.integration.reset:
|
||||
id: integration_sensor
|
||||
- sensor.integration.set_value:
|
||||
id: integration_sensor
|
||||
value: 100.0
|
||||
|
||||
sensor:
|
||||
- platform: adc
|
||||
id: my_sensor
|
||||
pin: ${pin}
|
||||
attenuation: 12db
|
||||
- platform: integration
|
||||
id: integration_sensor
|
||||
sensor: my_sensor
|
||||
name: Integration Sensor
|
||||
time_unit: s
|
||||
|
||||
@@ -16,7 +16,7 @@ class MockUARTComponent : public uart::UARTComponent {
|
||||
MOCK_METHOD(bool, read_array, (uint8_t * data, size_t len), (override));
|
||||
MOCK_METHOD(bool, peek_byte, (uint8_t * data), (override));
|
||||
MOCK_METHOD(size_t, available, (), (override));
|
||||
MOCK_METHOD(void, flush, (), (override));
|
||||
MOCK_METHOD(uart::FlushResult, flush, (), (override));
|
||||
MOCK_METHOD(void, check_logger_conflict, (), (override));
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "esphome/components/logger/logger.h"
|
||||
|
||||
/*
|
||||
This special main.cpp replaces the default one.
|
||||
It will run all the Google Tests found in all compiled cpp files and then exit with the result
|
||||
@@ -18,6 +20,12 @@ void original_setup() {
|
||||
}
|
||||
|
||||
void setup() {
|
||||
// Log functions call global_logger->log_vprintf_() without a null check,
|
||||
// so we must set up a Logger before any test that triggers logging.
|
||||
static esphome::logger::Logger test_logger(0);
|
||||
test_logger.set_log_level(ESPHOME_LOG_LEVEL);
|
||||
test_logger.pre_setup();
|
||||
|
||||
::testing::InitGoogleTest();
|
||||
int exit_code = RUN_ALL_TESTS();
|
||||
exit(exit_code);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
modbus:
|
||||
id: mod_bus1
|
||||
flow_control_pin: ${flow_control_pin}
|
||||
send_wait_time: 500ms
|
||||
turnaround_time: 100ms
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
display:
|
||||
- id: !extend main_lcd
|
||||
tft_url: http://esphome.io/default35.tft
|
||||
tft_upload_http_timeout: 20s
|
||||
tft_upload_http_retries: 10
|
||||
@@ -0,0 +1,3 @@
|
||||
display:
|
||||
- id: !extend main_lcd
|
||||
tft_upload_watchdog_timeout: 30s
|
||||
@@ -1,7 +1,5 @@
|
||||
packages:
|
||||
uart: !include ../../test_build_components/common/uart/esp32-ard.yaml
|
||||
base: !include common.yaml
|
||||
|
||||
display:
|
||||
- id: !extend main_lcd
|
||||
tft_url: http://esphome.io/default35.tft
|
||||
tft_upload: !include common_tft_upload.yaml
|
||||
tft_upload_watchdog: !include common_tft_upload_watchdog.yaml
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
packages:
|
||||
uart: !include ../../test_build_components/common/uart/esp32-idf.yaml
|
||||
base: !include common.yaml
|
||||
|
||||
display:
|
||||
- id: !extend main_lcd
|
||||
tft_url: http://esphome.io/default35.tft
|
||||
tft_upload: !include common_tft_upload.yaml
|
||||
tft_upload_watchdog: !include common_tft_upload_watchdog.yaml
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
packages:
|
||||
uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml
|
||||
base: !include common.yaml
|
||||
|
||||
display:
|
||||
- id: !extend main_lcd
|
||||
tft_url: http://esphome.io/default35.tft
|
||||
tft_upload: !include common_tft_upload.yaml
|
||||
|
||||
@@ -40,6 +40,10 @@ online_image:
|
||||
url: https://samples-files.com/samples/images/bmp/480-360-sample.bmp
|
||||
format: BMP
|
||||
type: BINARY
|
||||
- id: online_rgb_bmp_8bit
|
||||
url: https://samples-files.com/samples/images/bmp/480-360-sample.bmp
|
||||
format: BMP
|
||||
type: RGB
|
||||
- id: online_jpeg_image
|
||||
url: http://www.faqs.org/images/library.jpg
|
||||
format: JPEG
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
esp32:
|
||||
board: esp32-c6-devkitc-1
|
||||
framework:
|
||||
type: esp-idf
|
||||
log_level: DEBUG
|
||||
|
||||
network:
|
||||
enable_ipv6: true
|
||||
|
||||
@@ -13,3 +19,4 @@ openthread:
|
||||
force_dataset: true
|
||||
use_address: open-thread-test.local
|
||||
poll_period: 20sec
|
||||
output_power: 1dBm
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
zephyr_ble_server:
|
||||
|
||||
ota:
|
||||
- platform: zephyr_mcumgr
|
||||
transport:
|
||||
ble: true
|
||||
hardware_uart: CDC
|
||||
on_begin:
|
||||
then:
|
||||
- logger.log: "OTA start"
|
||||
on_progress:
|
||||
then:
|
||||
- logger.log:
|
||||
format: "OTA progress %0.1f%%"
|
||||
args: ["x"]
|
||||
on_end:
|
||||
then:
|
||||
- logger.log: "OTA end"
|
||||
on_error:
|
||||
then:
|
||||
- logger.log:
|
||||
format: "OTA update error %d"
|
||||
args: ["x"]
|
||||
on_state_change:
|
||||
then:
|
||||
lambda: >-
|
||||
ESP_LOGD("ota", "State %d", state);
|
||||
@@ -0,0 +1,77 @@
|
||||
#include "../common.h"
|
||||
|
||||
namespace esphome::packet_transport::testing {
|
||||
|
||||
TEST(PacketTransportBinarySensorTest, AddBinarySensor) {
|
||||
TestablePacketTransport transport;
|
||||
binary_sensor::BinarySensor bs;
|
||||
transport.add_binary_sensor("motion", &bs);
|
||||
ASSERT_EQ(transport.binary_sensors_.size(), 1u);
|
||||
EXPECT_STREQ(transport.binary_sensors_[0].id, "motion");
|
||||
EXPECT_EQ(transport.binary_sensors_[0].sensor, &bs);
|
||||
}
|
||||
|
||||
TEST(PacketTransportBinarySensorTest, AddRemoteBinarySensor) {
|
||||
TestablePacketTransport transport;
|
||||
binary_sensor::BinarySensor bs;
|
||||
transport.add_remote_binary_sensor("host1", "remote_motion", &bs);
|
||||
EXPECT_TRUE(transport.providers_.contains("host1"));
|
||||
EXPECT_EQ(transport.remote_binary_sensors_["host1"]["remote_motion"], &bs);
|
||||
}
|
||||
|
||||
TEST(PacketTransportBinarySensorTest, UnencryptedBinarySensorRoundTrip) {
|
||||
TestablePacketTransport encoder;
|
||||
encoder.init_for_test("sender");
|
||||
binary_sensor::BinarySensor local_bs;
|
||||
local_bs.state = true;
|
||||
encoder.add_binary_sensor("motion", &local_bs);
|
||||
|
||||
encoder.send_data_(true);
|
||||
ASSERT_EQ(encoder.sent_packets.size(), 1u);
|
||||
|
||||
TestablePacketTransport decoder;
|
||||
decoder.init_for_test("receiver");
|
||||
binary_sensor::BinarySensor remote_bs;
|
||||
decoder.add_remote_binary_sensor("sender", "motion", &remote_bs);
|
||||
|
||||
auto &packet = encoder.sent_packets[0];
|
||||
decoder.process_({packet.data(), packet.size()});
|
||||
EXPECT_TRUE(remote_bs.state);
|
||||
}
|
||||
|
||||
TEST(PacketTransportBinarySensorTest, MultipleSensorsRoundTrip) {
|
||||
TestablePacketTransport encoder;
|
||||
encoder.init_for_test("sender");
|
||||
|
||||
sensor::Sensor s1, s2;
|
||||
s1.state = 10.0f;
|
||||
s2.state = 20.0f;
|
||||
encoder.add_sensor("s1", &s1);
|
||||
encoder.add_sensor("s2", &s2);
|
||||
|
||||
binary_sensor::BinarySensor bs1;
|
||||
bs1.state = true;
|
||||
encoder.add_binary_sensor("bs1", &bs1);
|
||||
|
||||
encoder.send_data_(true);
|
||||
ASSERT_EQ(encoder.sent_packets.size(), 1u);
|
||||
|
||||
TestablePacketTransport decoder;
|
||||
decoder.init_for_test("receiver");
|
||||
sensor::Sensor rs1, rs2;
|
||||
binary_sensor::BinarySensor rbs1;
|
||||
rs1.state = -999.0f;
|
||||
rs2.state = -999.0f;
|
||||
decoder.add_remote_sensor("sender", "s1", &rs1);
|
||||
decoder.add_remote_sensor("sender", "s2", &rs2);
|
||||
decoder.add_remote_binary_sensor("sender", "bs1", &rbs1);
|
||||
|
||||
auto &packet = encoder.sent_packets[0];
|
||||
decoder.process_({packet.data(), packet.size()});
|
||||
|
||||
EXPECT_FLOAT_EQ(rs1.state, 10.0f);
|
||||
EXPECT_FLOAT_EQ(rs2.state, 20.0f);
|
||||
EXPECT_TRUE(rbs1.state);
|
||||
}
|
||||
|
||||
} // namespace esphome::packet_transport::testing
|
||||
@@ -0,0 +1,98 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <cstdio>
|
||||
#include <vector>
|
||||
#include <gtest/gtest.h>
|
||||
#include "esphome/components/packet_transport/packet_transport.h"
|
||||
|
||||
namespace esphome::packet_transport::testing {
|
||||
|
||||
// Protocol constants mirrored from packet_transport.cpp for test packet construction.
|
||||
static constexpr uint16_t MAGIC_NUMBER = 0x4553;
|
||||
static constexpr uint16_t MAGIC_PING = 0x5048;
|
||||
|
||||
// Concrete testable implementation of PacketTransport.
|
||||
// Captures sent packets and exposes protected members for verification.
|
||||
//
|
||||
// Sensor round-trip tests require USE_SENSOR / USE_BINARY_SENSOR to be defined,
|
||||
// which happens when 'sensor' and 'binary_sensor' components are in the build.
|
||||
// Run with --all or include those components to enable the full test suite.
|
||||
class TestablePacketTransport : public PacketTransport {
|
||||
public:
|
||||
using PacketTransport::add_key_;
|
||||
using PacketTransport::data_;
|
||||
using PacketTransport::encryption_key_;
|
||||
using PacketTransport::flush_;
|
||||
using PacketTransport::header_;
|
||||
using PacketTransport::increment_code_;
|
||||
using PacketTransport::init_data_;
|
||||
using PacketTransport::is_encrypted_;
|
||||
using PacketTransport::is_provider_;
|
||||
using PacketTransport::name_;
|
||||
using PacketTransport::ping_key_;
|
||||
using PacketTransport::ping_keys_;
|
||||
using PacketTransport::ping_pong_enable_;
|
||||
using PacketTransport::ping_pong_recyle_time_;
|
||||
using PacketTransport::process_;
|
||||
using PacketTransport::providers_;
|
||||
using PacketTransport::rolling_code_;
|
||||
using PacketTransport::rolling_code_enable_;
|
||||
using PacketTransport::send_data_;
|
||||
using PacketTransport::updated_;
|
||||
#ifdef USE_SENSOR
|
||||
using PacketTransport::add_data_;
|
||||
using PacketTransport::remote_sensors_;
|
||||
using PacketTransport::sensors_;
|
||||
#endif
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
using PacketTransport::add_binary_data_;
|
||||
using PacketTransport::binary_sensors_;
|
||||
using PacketTransport::remote_binary_sensors_;
|
||||
#endif
|
||||
|
||||
// NOTE: std::vector is used here for test convenience. For production code,
|
||||
// consider using StaticVector or FixedVector from esphome/core/helpers.h instead.
|
||||
mutable std::vector<std::vector<uint8_t>> sent_packets;
|
||||
size_t max_packet_size{512};
|
||||
bool send_enabled{true};
|
||||
|
||||
void send_packet(const std::vector<uint8_t> &buf) const override { this->sent_packets.push_back(buf); }
|
||||
size_t get_max_packet_size() override { return this->max_packet_size; }
|
||||
bool should_send() override { return this->send_enabled; }
|
||||
|
||||
/// Build the packet header for testing without requiring App or global_preferences.
|
||||
void init_for_test(const char *name) {
|
||||
this->name_ = name;
|
||||
this->header_.clear();
|
||||
// MAGIC_NUMBER as uint16_t little-endian
|
||||
this->header_.push_back(MAGIC_NUMBER & 0xFF);
|
||||
this->header_.push_back((MAGIC_NUMBER >> 8) & 0xFF);
|
||||
// Length-prefixed hostname
|
||||
auto len = strlen(name);
|
||||
this->header_.push_back(static_cast<uint8_t>(len));
|
||||
for (size_t i = 0; i < len; i++)
|
||||
this->header_.push_back(name[i]);
|
||||
// Pad to 4-byte boundary
|
||||
while (this->header_.size() & 0x3)
|
||||
this->header_.push_back(0);
|
||||
}
|
||||
};
|
||||
|
||||
/// Build a MAGIC_PING packet for testing add_key_ / ping-pong flows.
|
||||
inline std::vector<uint8_t> build_ping_packet(const char *hostname, uint32_t key) {
|
||||
std::vector<uint8_t> packet;
|
||||
packet.push_back(MAGIC_PING & 0xFF);
|
||||
packet.push_back((MAGIC_PING >> 8) & 0xFF);
|
||||
auto len = strlen(hostname);
|
||||
packet.push_back(static_cast<uint8_t>(len));
|
||||
for (size_t i = 0; i < len; i++)
|
||||
packet.push_back(hostname[i]);
|
||||
packet.push_back(key & 0xFF);
|
||||
packet.push_back((key >> 8) & 0xFF);
|
||||
packet.push_back((key >> 16) & 0xFF);
|
||||
packet.push_back((key >> 24) & 0xFF);
|
||||
return packet;
|
||||
}
|
||||
|
||||
} // namespace esphome::packet_transport::testing
|
||||
@@ -0,0 +1,186 @@
|
||||
#include "common.h"
|
||||
|
||||
namespace esphome::packet_transport::testing {
|
||||
|
||||
// --- Configuration setter tests ---
|
||||
|
||||
TEST(PacketTransportTest, SetIsProvider) {
|
||||
TestablePacketTransport transport;
|
||||
transport.set_is_provider(true);
|
||||
EXPECT_TRUE(transport.is_provider_);
|
||||
}
|
||||
|
||||
TEST(PacketTransportTest, SetEncryptionKey) {
|
||||
TestablePacketTransport transport;
|
||||
std::vector<uint8_t> key(32, 0xAB);
|
||||
transport.set_encryption_key(key);
|
||||
EXPECT_EQ(transport.encryption_key_, key);
|
||||
EXPECT_TRUE(transport.is_encrypted_());
|
||||
}
|
||||
|
||||
TEST(PacketTransportTest, NoEncryptionByDefault) {
|
||||
TestablePacketTransport transport;
|
||||
EXPECT_FALSE(transport.is_encrypted_());
|
||||
}
|
||||
|
||||
TEST(PacketTransportTest, SetRollingCodeEnable) {
|
||||
TestablePacketTransport transport;
|
||||
transport.set_rolling_code_enable(true);
|
||||
EXPECT_TRUE(transport.rolling_code_enable_);
|
||||
}
|
||||
|
||||
TEST(PacketTransportTest, SetPingPongEnable) {
|
||||
TestablePacketTransport transport;
|
||||
transport.set_ping_pong_enable(true);
|
||||
EXPECT_TRUE(transport.ping_pong_enable_);
|
||||
}
|
||||
|
||||
TEST(PacketTransportTest, SetPingPongRecycleTime) {
|
||||
TestablePacketTransport transport;
|
||||
transport.set_ping_pong_recycle_time(600);
|
||||
EXPECT_EQ(transport.ping_pong_recyle_time_, 600u);
|
||||
}
|
||||
|
||||
// --- Provider management ---
|
||||
|
||||
TEST(PacketTransportTest, AddProvider) {
|
||||
TestablePacketTransport transport;
|
||||
transport.add_provider("host1");
|
||||
EXPECT_TRUE(transport.providers_.contains("host1"));
|
||||
EXPECT_EQ(transport.providers_.size(), 1u);
|
||||
}
|
||||
|
||||
TEST(PacketTransportTest, AddProviderDuplicate) {
|
||||
TestablePacketTransport transport;
|
||||
transport.add_provider("host1");
|
||||
transport.add_provider("host1");
|
||||
EXPECT_EQ(transport.providers_.size(), 1u);
|
||||
}
|
||||
|
||||
TEST(PacketTransportTest, SetProviderEncryption) {
|
||||
TestablePacketTransport transport;
|
||||
transport.add_provider("host1");
|
||||
std::vector<uint8_t> key(32, 0xCD);
|
||||
transport.set_provider_encryption("host1", key);
|
||||
EXPECT_EQ(transport.providers_["host1"].encryption_key, key);
|
||||
}
|
||||
|
||||
// --- Ping key tests ---
|
||||
|
||||
TEST(PacketTransportTest, PingKeyStoredWhenEncrypted) {
|
||||
TestablePacketTransport transport;
|
||||
transport.init_for_test("receiver");
|
||||
transport.set_encryption_key(std::vector<uint8_t>(32, 0xAA));
|
||||
|
||||
auto ping = build_ping_packet("requester", 0xDEADBEEF);
|
||||
transport.process_({ping.data(), ping.size()});
|
||||
|
||||
ASSERT_EQ(transport.ping_keys_.size(), 1u);
|
||||
EXPECT_EQ(transport.ping_keys_["requester"], 0xDEADBEEFu);
|
||||
}
|
||||
|
||||
TEST(PacketTransportTest, PingKeyIgnoredWhenNotEncrypted) {
|
||||
TestablePacketTransport transport;
|
||||
transport.init_for_test("receiver");
|
||||
// No encryption key — add_key_ should be a no-op
|
||||
|
||||
auto ping = build_ping_packet("requester", 0xDEADBEEF);
|
||||
transport.process_({ping.data(), ping.size()});
|
||||
|
||||
EXPECT_TRUE(transport.ping_keys_.empty());
|
||||
}
|
||||
|
||||
TEST(PacketTransportTest, PingKeyUpdatedOnRepeat) {
|
||||
TestablePacketTransport transport;
|
||||
transport.init_for_test("receiver");
|
||||
transport.set_encryption_key(std::vector<uint8_t>(32, 0xAA));
|
||||
|
||||
auto ping1 = build_ping_packet("host1", 0x1111);
|
||||
transport.process_({ping1.data(), ping1.size()});
|
||||
EXPECT_EQ(transport.ping_keys_["host1"], 0x1111u);
|
||||
|
||||
// Same host, new key value — should update in place
|
||||
auto ping2 = build_ping_packet("host1", 0x2222);
|
||||
transport.process_({ping2.data(), ping2.size()});
|
||||
EXPECT_EQ(transport.ping_keys_.size(), 1u);
|
||||
EXPECT_EQ(transport.ping_keys_["host1"], 0x2222u);
|
||||
}
|
||||
|
||||
TEST(PacketTransportTest, PingKeyMaxLimit) {
|
||||
TestablePacketTransport transport;
|
||||
transport.init_for_test("receiver");
|
||||
transport.set_encryption_key(std::vector<uint8_t>(32, 0xAA));
|
||||
|
||||
// Fill to MAX_PING_KEYS (4)
|
||||
for (int i = 0; i < 4; i++) {
|
||||
char name[16];
|
||||
snprintf(name, sizeof(name), "host%d", i);
|
||||
auto ping = build_ping_packet(name, 0x1000 + i);
|
||||
transport.process_({ping.data(), ping.size()});
|
||||
}
|
||||
EXPECT_EQ(transport.ping_keys_.size(), 4u);
|
||||
|
||||
// 5th key should be discarded
|
||||
auto ping = build_ping_packet("host4", 0x9999);
|
||||
transport.process_({ping.data(), ping.size()});
|
||||
EXPECT_EQ(transport.ping_keys_.size(), 4u);
|
||||
EXPECT_FALSE(transport.ping_keys_.contains("host4"));
|
||||
}
|
||||
|
||||
// --- Process error handling ---
|
||||
|
||||
TEST(PacketTransportTest, ProcessShortBuffer) {
|
||||
TestablePacketTransport transport;
|
||||
transport.init_for_test("receiver");
|
||||
uint8_t buf[] = {0x53};
|
||||
// Too short for a magic number - should return safely
|
||||
transport.process_({buf, 1});
|
||||
}
|
||||
|
||||
TEST(PacketTransportTest, ProcessBadMagic) {
|
||||
TestablePacketTransport transport;
|
||||
transport.init_for_test("receiver");
|
||||
uint8_t buf[] = {0xFF, 0xFF, 0x00, 0x00};
|
||||
// Wrong magic - should return safely
|
||||
transport.process_({buf, sizeof(buf)});
|
||||
}
|
||||
|
||||
TEST(PacketTransportTest, ProcessOwnHostname) {
|
||||
TestablePacketTransport transport;
|
||||
transport.init_for_test("myself");
|
||||
// Build a packet from "myself" using a separate encoder
|
||||
TestablePacketTransport fake_sender;
|
||||
fake_sender.init_for_test("myself");
|
||||
fake_sender.send_data_(true);
|
||||
ASSERT_EQ(fake_sender.sent_packets.size(), 1u);
|
||||
|
||||
auto &packet = fake_sender.sent_packets[0];
|
||||
// Should be silently ignored because hostname matches our own
|
||||
transport.process_({packet.data(), packet.size()});
|
||||
}
|
||||
|
||||
TEST(PacketTransportTest, ProcessUnknownHostname) {
|
||||
TestablePacketTransport transport;
|
||||
transport.init_for_test("receiver");
|
||||
// No providers registered - "unknown" will not be found
|
||||
TestablePacketTransport sender;
|
||||
sender.init_for_test("unknown");
|
||||
sender.send_data_(true);
|
||||
ASSERT_EQ(sender.sent_packets.size(), 1u);
|
||||
|
||||
auto &packet = sender.sent_packets[0];
|
||||
// Should return safely without crash
|
||||
transport.process_({packet.data(), packet.size()});
|
||||
}
|
||||
|
||||
// --- Send disabled ---
|
||||
|
||||
TEST(PacketTransportTest, NoSendWhenDisabled) {
|
||||
TestablePacketTransport transport;
|
||||
transport.init_for_test("sender");
|
||||
transport.send_enabled = false;
|
||||
transport.send_data_(true);
|
||||
EXPECT_TRUE(transport.sent_packets.empty());
|
||||
}
|
||||
|
||||
} // namespace esphome::packet_transport::testing
|
||||
@@ -0,0 +1,170 @@
|
||||
#include "../common.h"
|
||||
|
||||
namespace esphome::packet_transport::testing {
|
||||
|
||||
TEST(PacketTransportSensorTest, AddSensor) {
|
||||
TestablePacketTransport transport;
|
||||
sensor::Sensor s;
|
||||
transport.add_sensor("temp", &s);
|
||||
ASSERT_EQ(transport.sensors_.size(), 1u);
|
||||
EXPECT_STREQ(transport.sensors_[0].id, "temp");
|
||||
EXPECT_EQ(transport.sensors_[0].sensor, &s);
|
||||
EXPECT_TRUE(transport.sensors_[0].updated);
|
||||
}
|
||||
|
||||
TEST(PacketTransportSensorTest, AddRemoteSensor) {
|
||||
TestablePacketTransport transport;
|
||||
sensor::Sensor s;
|
||||
transport.add_remote_sensor("host1", "remote_temp", &s);
|
||||
EXPECT_TRUE(transport.providers_.contains("host1"));
|
||||
EXPECT_EQ(transport.remote_sensors_["host1"]["remote_temp"], &s);
|
||||
}
|
||||
|
||||
TEST(PacketTransportSensorTest, UnencryptedSensorRoundTrip) {
|
||||
// Encoder
|
||||
TestablePacketTransport encoder;
|
||||
encoder.init_for_test("sender");
|
||||
sensor::Sensor local_sensor;
|
||||
local_sensor.state = 42.5f;
|
||||
encoder.add_sensor("temp", &local_sensor);
|
||||
|
||||
encoder.send_data_(true);
|
||||
ASSERT_EQ(encoder.sent_packets.size(), 1u);
|
||||
|
||||
// Decoder
|
||||
TestablePacketTransport decoder;
|
||||
decoder.init_for_test("receiver");
|
||||
sensor::Sensor remote_sensor;
|
||||
remote_sensor.state = -999.0f; // sentinel
|
||||
decoder.add_remote_sensor("sender", "temp", &remote_sensor);
|
||||
|
||||
auto &packet = encoder.sent_packets[0];
|
||||
decoder.process_({packet.data(), packet.size()});
|
||||
EXPECT_FLOAT_EQ(remote_sensor.state, 42.5f);
|
||||
}
|
||||
|
||||
TEST(PacketTransportSensorTest, EncryptedSensorRoundTrip) {
|
||||
std::vector<uint8_t> key(32);
|
||||
for (int i = 0; i < 32; i++)
|
||||
key[i] = i;
|
||||
|
||||
TestablePacketTransport encoder;
|
||||
encoder.init_for_test("sender");
|
||||
encoder.set_encryption_key(key);
|
||||
sensor::Sensor local_sensor;
|
||||
local_sensor.state = 99.9f;
|
||||
encoder.add_sensor("temp", &local_sensor);
|
||||
|
||||
encoder.send_data_(true);
|
||||
ASSERT_EQ(encoder.sent_packets.size(), 1u);
|
||||
|
||||
TestablePacketTransport decoder;
|
||||
decoder.init_for_test("receiver");
|
||||
sensor::Sensor remote_sensor;
|
||||
remote_sensor.state = -999.0f;
|
||||
decoder.add_remote_sensor("sender", "temp", &remote_sensor);
|
||||
decoder.set_provider_encryption("sender", key);
|
||||
|
||||
auto &packet = encoder.sent_packets[0];
|
||||
decoder.process_({packet.data(), packet.size()});
|
||||
EXPECT_FLOAT_EQ(remote_sensor.state, 99.9f);
|
||||
}
|
||||
|
||||
TEST(PacketTransportSensorTest, SendDataOnlyUpdated) {
|
||||
TestablePacketTransport encoder;
|
||||
encoder.init_for_test("sender");
|
||||
|
||||
sensor::Sensor s1, s2;
|
||||
s1.state = 1.0f;
|
||||
s2.state = 2.0f;
|
||||
encoder.add_sensor("s1", &s1);
|
||||
encoder.add_sensor("s2", &s2);
|
||||
|
||||
// Mark s1 as not updated, only s2 as updated
|
||||
encoder.sensors_[0].updated = false;
|
||||
encoder.sensors_[1].updated = true;
|
||||
|
||||
encoder.send_data_(false);
|
||||
ASSERT_EQ(encoder.sent_packets.size(), 1u);
|
||||
|
||||
TestablePacketTransport decoder;
|
||||
decoder.init_for_test("receiver");
|
||||
sensor::Sensor rs1, rs2;
|
||||
rs1.state = -999.0f;
|
||||
rs2.state = -999.0f;
|
||||
decoder.add_remote_sensor("sender", "s1", &rs1);
|
||||
decoder.add_remote_sensor("sender", "s2", &rs2);
|
||||
|
||||
auto &packet = encoder.sent_packets[0];
|
||||
decoder.process_({packet.data(), packet.size()});
|
||||
|
||||
EXPECT_FLOAT_EQ(rs1.state, -999.0f); // not updated, not sent
|
||||
EXPECT_FLOAT_EQ(rs2.state, 2.0f); // updated, sent
|
||||
}
|
||||
|
||||
TEST(PacketTransportSensorTest, PingKeyIncludedInTransmittedPacket) {
|
||||
std::vector<uint8_t> key(32, 0xBB);
|
||||
|
||||
// Responder: encrypted, owns a sensor
|
||||
TestablePacketTransport responder;
|
||||
responder.init_for_test("responder");
|
||||
responder.set_encryption_key(key);
|
||||
sensor::Sensor local_sensor;
|
||||
local_sensor.state = 77.7f;
|
||||
responder.add_sensor("temp", &local_sensor);
|
||||
|
||||
// Requester sends a MAGIC_PING that the responder processes
|
||||
auto ping = build_ping_packet("requester", 0xDEADBEEF);
|
||||
responder.process_({ping.data(), ping.size()});
|
||||
ASSERT_EQ(responder.ping_keys_.size(), 1u);
|
||||
|
||||
// Responder sends sensor data — ping key should be embedded
|
||||
responder.send_data_(true);
|
||||
ASSERT_EQ(responder.sent_packets.size(), 1u);
|
||||
|
||||
// Requester: encrypted provider, ping-pong enabled, expects key 0xDEADBEEF
|
||||
TestablePacketTransport requester;
|
||||
requester.init_for_test("requester");
|
||||
requester.set_ping_pong_enable(true);
|
||||
requester.ping_key_ = 0xDEADBEEF;
|
||||
sensor::Sensor remote_sensor;
|
||||
remote_sensor.state = -999.0f;
|
||||
requester.add_remote_sensor("responder", "temp", &remote_sensor);
|
||||
requester.set_provider_encryption("responder", key);
|
||||
|
||||
// The requester decrypts the packet and finds its ping key echoed back,
|
||||
// which gates the sensor data — if the key is missing, data is blocked.
|
||||
auto &packet = responder.sent_packets[0];
|
||||
requester.process_({packet.data(), packet.size()});
|
||||
EXPECT_FLOAT_EQ(remote_sensor.state, 77.7f);
|
||||
}
|
||||
|
||||
TEST(PacketTransportSensorTest, MissingPingKeyBlocksSensorData) {
|
||||
std::vector<uint8_t> key(32, 0xBB);
|
||||
|
||||
// Responder sends data WITHOUT receiving any MAGIC_PING first — no ping keys
|
||||
TestablePacketTransport responder;
|
||||
responder.init_for_test("responder");
|
||||
responder.set_encryption_key(key);
|
||||
sensor::Sensor local_sensor;
|
||||
local_sensor.state = 77.7f;
|
||||
responder.add_sensor("temp", &local_sensor);
|
||||
responder.send_data_(true);
|
||||
ASSERT_EQ(responder.sent_packets.size(), 1u);
|
||||
|
||||
// Requester with ping-pong enabled expects a key that isn't in the packet
|
||||
TestablePacketTransport requester;
|
||||
requester.init_for_test("requester");
|
||||
requester.set_ping_pong_enable(true);
|
||||
requester.ping_key_ = 0xDEADBEEF;
|
||||
sensor::Sensor remote_sensor;
|
||||
remote_sensor.state = -999.0f;
|
||||
requester.add_remote_sensor("responder", "temp", &remote_sensor);
|
||||
requester.set_provider_encryption("responder", key);
|
||||
|
||||
auto &packet = responder.sent_packets[0];
|
||||
requester.process_({packet.data(), packet.size()});
|
||||
EXPECT_FLOAT_EQ(remote_sensor.state, -999.0f); // blocked — ping key not found
|
||||
}
|
||||
|
||||
} // namespace esphome::packet_transport::testing
|
||||
@@ -1,3 +1,6 @@
|
||||
rp2040:
|
||||
enable_full_printf: false
|
||||
|
||||
logger:
|
||||
level: VERBOSE
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
rp2040_ble:
|
||||
@@ -0,0 +1,2 @@
|
||||
rp2040_ble:
|
||||
enable_on_boot: false
|
||||
@@ -0,0 +1 @@
|
||||
<<: !include common.yaml
|
||||
@@ -0,0 +1,10 @@
|
||||
wifi:
|
||||
ssid: MySSID
|
||||
password: password1
|
||||
|
||||
api:
|
||||
|
||||
serial_proxy:
|
||||
- id: serial_proxy_1
|
||||
name: Test Serial Port
|
||||
port_type: RS232
|
||||
@@ -0,0 +1,8 @@
|
||||
substitutions:
|
||||
tx_pin: GPIO4
|
||||
rx_pin: GPIO5
|
||||
|
||||
packages:
|
||||
uart: !include ../../test_build_components/common/uart/esp32-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
@@ -0,0 +1,8 @@
|
||||
substitutions:
|
||||
tx_pin: GPIO0
|
||||
rx_pin: GPIO2
|
||||
|
||||
packages:
|
||||
uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
@@ -0,0 +1,8 @@
|
||||
substitutions:
|
||||
tx_pin: GPIO4
|
||||
rx_pin: GPIO5
|
||||
|
||||
packages:
|
||||
uart: !include ../../test_build_components/common/uart/rp2040-ard.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
@@ -0,0 +1,18 @@
|
||||
<<: !include common.yaml
|
||||
|
||||
media_player:
|
||||
- platform: speaker
|
||||
id: speaker_media_player_id
|
||||
announcement_pipeline:
|
||||
speaker: speaker_id
|
||||
buffer_size: 1000000
|
||||
volume_increment: 0.02
|
||||
volume_max: 0.95
|
||||
volume_min: 0.0
|
||||
task_stack_in_psram: true
|
||||
on_turn_on:
|
||||
then:
|
||||
- logger.log: "Turn On Media Player"
|
||||
on_turn_off:
|
||||
then:
|
||||
- logger.log: "Turn Off Media Player"
|
||||
@@ -0,0 +1,9 @@
|
||||
substitutions:
|
||||
scl_pin: GPIO16
|
||||
sda_pin: GPIO17
|
||||
i2s_bclk_pin: GPIO27
|
||||
i2s_lrclk_pin: GPIO26
|
||||
i2s_mclk_pin: GPIO25
|
||||
i2s_dout_pin: GPIO23
|
||||
|
||||
<<: !include common-media_player_off_on.yaml
|
||||
@@ -0,0 +1,43 @@
|
||||
i2s_audio:
|
||||
i2s_lrclk_pin: ${i2s_bclk_pin}
|
||||
i2s_bclk_pin: ${i2s_lrclk_pin}
|
||||
i2s_mclk_pin: ${i2s_mclk_pin}
|
||||
|
||||
speaker:
|
||||
- platform: i2s_audio
|
||||
id: speaker_id
|
||||
dac_type: external
|
||||
i2s_dout_pin: ${i2s_dout_pin}
|
||||
sample_rate: 48000
|
||||
num_channels: 2
|
||||
|
||||
audio_file:
|
||||
- id: test_audio
|
||||
file:
|
||||
type: local
|
||||
path: $component_dir/test.wav
|
||||
|
||||
media_source:
|
||||
- platform: audio_file
|
||||
id: audio_file_source
|
||||
|
||||
media_player:
|
||||
- platform: speaker_source
|
||||
id: media_player_id
|
||||
name: Media Player
|
||||
volume_increment: 0.02
|
||||
volume_initial: 0.75
|
||||
volume_max: 0.95
|
||||
volume_min: 0.0
|
||||
media_pipeline:
|
||||
speaker: speaker_id
|
||||
format: FLAC
|
||||
num_channels: 1
|
||||
sources:
|
||||
- audio_file_source
|
||||
on_mute:
|
||||
- media_player.pause:
|
||||
id: media_player_id
|
||||
on_unmute:
|
||||
- media_player.play:
|
||||
id: media_player_id
|
||||
@@ -0,0 +1,9 @@
|
||||
substitutions:
|
||||
scl_pin: GPIO16
|
||||
sda_pin: GPIO17
|
||||
i2s_bclk_pin: GPIO27
|
||||
i2s_lrclk_pin: GPIO26
|
||||
i2s_mclk_pin: GPIO25
|
||||
i2s_dout_pin: GPIO23
|
||||
|
||||
<<: !include common.yaml
|
||||
Binary file not shown.
@@ -28,9 +28,14 @@ esphome:
|
||||
# Test C++ API: set_template() with stateless lambda (no captures)
|
||||
# NOTE: set_template() is not intended to be a public API, but we test it to ensure it doesn't break.
|
||||
- lambda: |-
|
||||
id(template_sens).set_template([]() -> esphome::optional<float> {
|
||||
id(template_sens).set_template([]() -> std::optional<float> {
|
||||
return 123.0f;
|
||||
});
|
||||
# Test that esphome::optional alias still works for backward compatibility
|
||||
- lambda: |-
|
||||
id(template_sens).set_template([]() -> esphome::optional<float> {
|
||||
return 42.0f;
|
||||
});
|
||||
|
||||
- datetime.date.set:
|
||||
id: test_date
|
||||
|
||||
@@ -30,7 +30,7 @@ class MockUARTComponent : public UARTComponent {
|
||||
MOCK_METHOD(bool, read_array, (uint8_t * data, size_t len), (override));
|
||||
MOCK_METHOD(bool, peek_byte, (uint8_t * data), (override));
|
||||
MOCK_METHOD(size_t, available, (), (override));
|
||||
MOCK_METHOD(void, flush, (), (override));
|
||||
MOCK_METHOD(FlushResult, flush, (), (override));
|
||||
MOCK_METHOD(void, check_logger_conflict, (), (override));
|
||||
};
|
||||
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
esphome:
|
||||
on_boot:
|
||||
then:
|
||||
- uart.write: 'Hello World'
|
||||
- uart.write: [0x00, 0x20, 0x42]
|
||||
- uart.write:
|
||||
id: uart_id
|
||||
data: 'Hello World'
|
||||
- uart.write:
|
||||
id: uart_id
|
||||
data: [0x00, 0x20, 0x42]
|
||||
|
||||
uart:
|
||||
- id: uart_uart
|
||||
- id: uart_id
|
||||
tx_pin: 4
|
||||
rx_pin: 5
|
||||
flow_control_pin: 6
|
||||
@@ -16,3 +20,9 @@ uart:
|
||||
rx_timeout: 1
|
||||
parity: EVEN
|
||||
stop_bits: 2
|
||||
- id: uart_debug
|
||||
tx_pin: 18
|
||||
rx_pin: 19
|
||||
baud_rate: 115200
|
||||
debug:
|
||||
debug_prefix: "[UART1] "
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
esphome:
|
||||
on_boot:
|
||||
then:
|
||||
- uart.write: 'Hello World'
|
||||
- uart.write: [0x00, 0x20, 0x42]
|
||||
- uart.write: !lambda |-
|
||||
return {0xAA, 0xBB, 0xCC};
|
||||
- uart.write:
|
||||
id: uart_id
|
||||
data: 'Hello World'
|
||||
- uart.write:
|
||||
id: uart_id
|
||||
data: [0x00, 0x20, 0x42]
|
||||
- uart.write:
|
||||
id: uart_id
|
||||
data: !lambda |-
|
||||
return {0xAA, 0xBB, 0xCC};
|
||||
|
||||
uart:
|
||||
- id: uart_uart
|
||||
- id: uart_id
|
||||
tx_pin: 17
|
||||
rx_pin: 16
|
||||
flow_control_pin: 4
|
||||
@@ -18,32 +24,54 @@ uart:
|
||||
rx_timeout: 1
|
||||
parity: EVEN
|
||||
stop_bits: 2
|
||||
- id: uart_debug
|
||||
tx_pin: 21
|
||||
rx_pin: 22
|
||||
baud_rate: 115200
|
||||
debug:
|
||||
debug_prefix: "[UART1] "
|
||||
- id: uart_debug_custom
|
||||
tx_pin: 25
|
||||
rx_pin: 26
|
||||
baud_rate: 9600
|
||||
debug:
|
||||
debug_prefix: "[UART2] "
|
||||
after:
|
||||
delimiter: "\n"
|
||||
sequence:
|
||||
- lambda: UARTDebug::log_string(direction, bytes, debug_prefix);
|
||||
- id: uart_debug_no_prefix
|
||||
tx_pin: 32
|
||||
rx_pin: 33
|
||||
baud_rate: 9600
|
||||
debug:
|
||||
|
||||
packet_transport:
|
||||
- platform: uart
|
||||
uart_id: uart_id
|
||||
|
||||
switch:
|
||||
# Test uart switch with single state (array)
|
||||
- platform: uart
|
||||
name: "UART Switch Single Array"
|
||||
uart_id: uart_uart
|
||||
uart_id: uart_id
|
||||
data: [0x01, 0x02, 0x03]
|
||||
# Test uart switch with single state (string)
|
||||
- platform: uart
|
||||
name: "UART Switch Single String"
|
||||
uart_id: uart_uart
|
||||
uart_id: uart_id
|
||||
data: "ON"
|
||||
# Test uart switch with turn_on/turn_off (arrays)
|
||||
- platform: uart
|
||||
name: "UART Switch Dual Array"
|
||||
uart_id: uart_uart
|
||||
uart_id: uart_id
|
||||
data:
|
||||
turn_on: [0xA0, 0xA1, 0xA2]
|
||||
turn_off: [0xB0, 0xB1, 0xB2]
|
||||
# Test uart switch with turn_on/turn_off (strings)
|
||||
- platform: uart
|
||||
name: "UART Switch Dual String"
|
||||
uart_id: uart_uart
|
||||
uart_id: uart_id
|
||||
data:
|
||||
turn_on: "TURN_ON"
|
||||
turn_off: "TURN_OFF"
|
||||
@@ -61,24 +89,26 @@ button:
|
||||
# Test uart button with array data
|
||||
- platform: uart
|
||||
name: "UART Button Array"
|
||||
uart_id: uart_uart
|
||||
uart_id: uart_id
|
||||
data: [0xFF, 0xEE, 0xDD]
|
||||
# Test uart button with string data
|
||||
- platform: uart
|
||||
name: "UART Button String"
|
||||
uart_id: uart_uart
|
||||
uart_id: uart_id
|
||||
data: "BUTTON_PRESS"
|
||||
# Test uart button with lambda (function pointer)
|
||||
- platform: template
|
||||
name: "UART Lambda Test"
|
||||
on_press:
|
||||
- uart.write: !lambda |-
|
||||
std::string cmd = "VALUE=" + str_sprintf("%.0f", id(test_number).state) + "\r\n";
|
||||
return std::vector<uint8_t>(cmd.begin(), cmd.end());
|
||||
- uart.write:
|
||||
id: uart_id
|
||||
data: !lambda |-
|
||||
std::string cmd = "VALUE=" + str_sprintf("%.0f", id(test_number).state) + "\r\n";
|
||||
return std::vector<uint8_t>(cmd.begin(), cmd.end());
|
||||
|
||||
event:
|
||||
- platform: uart
|
||||
uart_id: uart_uart
|
||||
uart_id: uart_id
|
||||
name: "UART Event"
|
||||
event_types:
|
||||
- "string_event_A": "*A#"
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
esphome:
|
||||
on_boot:
|
||||
then:
|
||||
- uart.write: 'Hello World'
|
||||
- uart.write: [0x00, 0x20, 0x42]
|
||||
- uart.write:
|
||||
id: uart_id
|
||||
data: 'Hello World'
|
||||
- uart.write:
|
||||
id: uart_id
|
||||
data: [0x00, 0x20, 0x42]
|
||||
|
||||
uart:
|
||||
- id: uart_uart
|
||||
- id: uart_id
|
||||
tx_pin: 4
|
||||
rx_pin: 5
|
||||
baud_rate: 9600
|
||||
@@ -13,15 +17,21 @@ uart:
|
||||
rx_buffer_size: 512
|
||||
parity: EVEN
|
||||
stop_bits: 2
|
||||
- id: uart_debug
|
||||
tx_pin: 14
|
||||
rx_pin: 12
|
||||
baud_rate: 115200
|
||||
debug:
|
||||
debug_prefix: "[UART1] "
|
||||
|
||||
switch:
|
||||
- platform: uart
|
||||
name: "UART Switch Array"
|
||||
uart_id: uart_uart
|
||||
uart_id: uart_id
|
||||
data: [0x01, 0x02, 0x03]
|
||||
- platform: uart
|
||||
name: "UART Switch Dual"
|
||||
uart_id: uart_uart
|
||||
uart_id: uart_id
|
||||
data:
|
||||
turn_on: [0xA0, 0xA1]
|
||||
turn_off: [0xB0, 0xB1]
|
||||
@@ -29,12 +39,12 @@ switch:
|
||||
button:
|
||||
- platform: uart
|
||||
name: "UART Button"
|
||||
uart_id: uart_uart
|
||||
uart_id: uart_id
|
||||
data: [0xFF, 0xEE]
|
||||
|
||||
event:
|
||||
- platform: uart
|
||||
uart_id: uart_uart
|
||||
uart_id: uart_id
|
||||
name: "UART Event"
|
||||
event_types:
|
||||
- "string_event_A": "*A#"
|
||||
|
||||
@@ -5,7 +5,7 @@ esphome:
|
||||
- uart.write: [0x00, 0x20, 0x42]
|
||||
|
||||
uart:
|
||||
- id: uart_uart
|
||||
- id: uart_id
|
||||
port: "/dev/ttyS0"
|
||||
baud_rate: 9600
|
||||
data_bits: 8
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
esphome:
|
||||
on_boot:
|
||||
then:
|
||||
- uart.write: 'Hello World'
|
||||
- uart.write: [0x00, 0x20, 0x42]
|
||||
- uart.write:
|
||||
id: uart_id
|
||||
data: 'Hello World'
|
||||
- uart.write:
|
||||
id: uart_id
|
||||
data: [0x00, 0x20, 0x42]
|
||||
|
||||
uart:
|
||||
- id: uart_uart
|
||||
- id: uart_id
|
||||
tx_pin: 4
|
||||
rx_pin: 5
|
||||
baud_rate: 9600
|
||||
@@ -13,3 +17,9 @@ uart:
|
||||
rx_buffer_size: 512
|
||||
parity: EVEN
|
||||
stop_bits: 2
|
||||
- id: uart_debug
|
||||
tx_pin: 8
|
||||
rx_pin: 9
|
||||
baud_rate: 115200
|
||||
debug:
|
||||
debug_prefix: "[UART1] "
|
||||
|
||||
@@ -34,3 +34,11 @@ usb_uart:
|
||||
- id: channel_4_1
|
||||
debug: true
|
||||
dummy_receiver: true
|
||||
debug_prefix: "[ESP_JTAG] "
|
||||
- id: uart_5
|
||||
type: cp210x
|
||||
channels:
|
||||
- id: channel_5_1
|
||||
baud_rate: 9600
|
||||
debug: true
|
||||
debug_prefix: "[CP210X] "
|
||||
|
||||
@@ -12,7 +12,9 @@
|
||||
using namespace esphome;
|
||||
|
||||
void setup() {
|
||||
App.pre_setup("livingroom", "LivingRoom", false);
|
||||
static char name[] = "livingroom";
|
||||
static char friendly_name[] = "LivingRoom";
|
||||
App.pre_setup(name, sizeof(name) - 1, friendly_name, sizeof(friendly_name) - 1);
|
||||
auto *log = new logger::Logger(115200); // NOLINT
|
||||
log->pre_setup();
|
||||
log->set_uart_selection(logger::UART_SELECTION_UART0);
|
||||
|
||||
@@ -73,11 +73,6 @@ def shared_platformio_cache() -> Generator[Path]:
|
||||
test_cache_dir = Path.home() / ".esphome-integration-tests"
|
||||
cache_dir = test_cache_dir / "platformio"
|
||||
|
||||
# Create the temp directory that PlatformIO uses to avoid race conditions
|
||||
# This ensures it exists and won't be deleted by parallel processes
|
||||
platformio_tmp_dir = cache_dir / ".cache" / "tmp"
|
||||
platformio_tmp_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Use a lock file in the home directory to ensure only one process initializes the cache
|
||||
# This is needed when running with pytest-xdist
|
||||
# The lock file must be in a directory that already exists to avoid race conditions
|
||||
@@ -87,8 +82,9 @@ def shared_platformio_cache() -> Generator[Path]:
|
||||
with open(lock_file, "w") as lock_fd:
|
||||
fcntl.flock(lock_fd.fileno(), fcntl.LOCK_EX)
|
||||
|
||||
# Check if cache needs initialization while holding the lock
|
||||
if not cache_dir.exists() or not any(cache_dir.iterdir()):
|
||||
# Check if the native platform is installed (the actual indicator of a populated cache)
|
||||
native_platform = cache_dir / "platforms" / "native"
|
||||
if not native_platform.exists():
|
||||
# Create the test cache directory if it doesn't exist
|
||||
test_cache_dir.mkdir(exist_ok=True)
|
||||
|
||||
@@ -197,6 +193,7 @@ async def yaml_config(request: pytest.FixtureRequest, unused_tcp_port: int) -> s
|
||||
" platformio_options:\n"
|
||||
" build_flags:\n"
|
||||
' - "-DDEBUG" # Enable assert() statements\n'
|
||||
' - "-DESPHOME_DEBUG" # Enable ESPHOME_DEBUG_ASSERT checks\n'
|
||||
' - "-DESPHOME_DEBUG_API" # Enable API protocol asserts\n'
|
||||
' - "-g" # Add debug symbols',
|
||||
)
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import uart
|
||||
from esphome.components.const import CONF_DATA_BITS, CONF_PARITY, CONF_STOP_BITS
|
||||
from esphome.components.uart import (
|
||||
CONF_RX_FULL_THRESHOLD,
|
||||
CONF_RX_TIMEOUT,
|
||||
debug_to_code,
|
||||
maybe_empty_debug,
|
||||
validate_raw_data,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_BAUD_RATE,
|
||||
CONF_DATA,
|
||||
CONF_DEBUG,
|
||||
CONF_DELAY,
|
||||
CONF_ID,
|
||||
CONF_INTERVAL,
|
||||
CONF_RX_BUFFER_SIZE,
|
||||
CONF_TRIGGER_ID,
|
||||
)
|
||||
from esphome.core import ID
|
||||
|
||||
CODEOWNERS = ["@esphome/tests"]
|
||||
MULTI_CONF = True
|
||||
|
||||
uart_mock_ns = cg.esphome_ns.namespace("uart_mock")
|
||||
MockUartComponent = uart_mock_ns.class_(
|
||||
"MockUartComponent", uart.UARTComponent, cg.Component
|
||||
)
|
||||
MockUartInjectRXAction = uart_mock_ns.class_(
|
||||
"MockUartInjectRXAction", automation.Action
|
||||
)
|
||||
MockUartTXTrigger = uart_mock_ns.class_(
|
||||
"MockUartTXTrigger",
|
||||
automation.Trigger.template(cg.std_vector.template(cg.uint8)),
|
||||
)
|
||||
|
||||
CONF_INJECTIONS = "injections"
|
||||
CONF_RESPONSES = "responses"
|
||||
CONF_INJECT_RX = "inject_rx"
|
||||
CONF_EXPECT_TX = "expect_tx"
|
||||
CONF_PERIODIC_RX = "periodic_rx"
|
||||
CONF_ON_TX = "on_tx"
|
||||
CONF_AUTO_START = "auto_start"
|
||||
|
||||
UART_PARITY_OPTIONS = {
|
||||
"NONE": uart.UARTParityOptions.UART_CONFIG_PARITY_NONE,
|
||||
"EVEN": uart.UARTParityOptions.UART_CONFIG_PARITY_EVEN,
|
||||
"ODD": uart.UARTParityOptions.UART_CONFIG_PARITY_ODD,
|
||||
}
|
||||
|
||||
INJECTION_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_INJECT_RX): [cv.hex_uint8_t],
|
||||
cv.Optional(CONF_DELAY, default="0ms"): cv.positive_time_period_milliseconds,
|
||||
}
|
||||
)
|
||||
|
||||
CONFIG_INJECT_RX_SCHEMA = cv.maybe_simple_value(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(MockUartComponent),
|
||||
cv.Required("data"): cv.templatable(validate_raw_data),
|
||||
cv.Optional(CONF_DELAY): cv.positive_time_period_milliseconds,
|
||||
},
|
||||
key=CONF_DATA,
|
||||
)
|
||||
|
||||
|
||||
RESPONSE_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_EXPECT_TX): [cv.hex_uint8_t],
|
||||
cv.Required(CONF_INJECT_RX): [cv.hex_uint8_t],
|
||||
cv.Optional(CONF_DELAY, default="0ms"): cv.positive_time_period_milliseconds,
|
||||
}
|
||||
)
|
||||
|
||||
PERIODIC_RX_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_DATA): [cv.hex_uint8_t],
|
||||
cv.Required(CONF_INTERVAL): cv.positive_time_period_milliseconds,
|
||||
}
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(MockUartComponent),
|
||||
cv.Required(CONF_BAUD_RATE): cv.int_range(min=1),
|
||||
cv.Optional(CONF_RX_BUFFER_SIZE, default=256): cv.validate_bytes,
|
||||
cv.Optional(CONF_RX_FULL_THRESHOLD): cv.int_range(min=1, max=120),
|
||||
cv.Optional(CONF_RX_TIMEOUT, default=2): cv.int_range(min=0, max=92),
|
||||
cv.Optional(CONF_STOP_BITS, default=1): cv.one_of(1, 2, int=True),
|
||||
cv.Optional(CONF_DATA_BITS, default=8): cv.int_range(min=5, max=8),
|
||||
cv.Optional(CONF_PARITY, default="NONE"): cv.enum(
|
||||
UART_PARITY_OPTIONS, upper=True
|
||||
),
|
||||
cv.Optional(CONF_INJECTIONS, default=[]): cv.ensure_list(INJECTION_SCHEMA),
|
||||
cv.Optional(CONF_RESPONSES, default=[]): cv.ensure_list(RESPONSE_SCHEMA),
|
||||
cv.Optional(CONF_PERIODIC_RX, default=[]): cv.ensure_list(PERIODIC_RX_SCHEMA),
|
||||
cv.Optional(CONF_AUTO_START, default=True): cv.boolean,
|
||||
cv.Optional(CONF_ON_TX): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(MockUartTXTrigger),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_DEBUG): maybe_empty_debug,
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"uart_mock.inject_rx", MockUartInjectRXAction, CONFIG_INJECT_RX_SCHEMA
|
||||
)
|
||||
async def inject_rx_to_code(config, action_id, template_arg, args):
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
data = config[CONF_DATA]
|
||||
if isinstance(data, bytes):
|
||||
data = list(data)
|
||||
|
||||
if cg.is_template(data):
|
||||
templ = await cg.templatable(data, args, cg.std_vector.template(cg.uint8))
|
||||
cg.add(var.set_data_template(templ))
|
||||
else:
|
||||
# Generate static array in flash to avoid RAM copy
|
||||
arr_id = ID(f"{action_id}_data", is_declaration=True, type=cg.uint8)
|
||||
arr = cg.static_const_array(arr_id, cg.ArrayInitializer(*data))
|
||||
cg.add(var.set_data_static(arr, len(data)))
|
||||
if CONF_DELAY in config:
|
||||
cg.add(var.set_delay(config[CONF_DELAY]))
|
||||
return var
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
|
||||
cg.add(var.set_baud_rate(config[CONF_BAUD_RATE]))
|
||||
cg.add(var.set_rx_buffer_size(config[CONF_RX_BUFFER_SIZE]))
|
||||
if CONF_RX_FULL_THRESHOLD in config:
|
||||
cg.add(var.set_rx_full_threshold(config[CONF_RX_FULL_THRESHOLD]))
|
||||
cg.add(var.set_rx_timeout(config[CONF_RX_TIMEOUT]))
|
||||
cg.add(var.set_stop_bits(config[CONF_STOP_BITS]))
|
||||
cg.add(var.set_data_bits(config[CONF_DATA_BITS]))
|
||||
cg.add(var.set_parity(config[CONF_PARITY]))
|
||||
|
||||
if not config[CONF_AUTO_START]:
|
||||
cg.add(var.set_auto_start(False))
|
||||
|
||||
for injection in config[CONF_INJECTIONS]:
|
||||
rx_data = injection[CONF_INJECT_RX]
|
||||
delay_ms = injection[CONF_DELAY]
|
||||
cg.add(var.add_injection(rx_data, delay_ms))
|
||||
|
||||
for response in config[CONF_RESPONSES]:
|
||||
tx_data = response[CONF_EXPECT_TX]
|
||||
rx_data = response[CONF_INJECT_RX]
|
||||
delay_ms = response[CONF_DELAY]
|
||||
cg.add(var.add_response(tx_data, rx_data, delay_ms))
|
||||
|
||||
for periodic in config[CONF_PERIODIC_RX]:
|
||||
data = periodic[CONF_DATA]
|
||||
interval = periodic[CONF_INTERVAL]
|
||||
cg.add(var.add_periodic_rx(data, interval))
|
||||
|
||||
for conf in config.get(CONF_ON_TX, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(
|
||||
trigger, [(cg.std_vector.template(cg.uint8), "data")], conf
|
||||
)
|
||||
|
||||
if CONF_DEBUG in config:
|
||||
await debug_to_code(config[CONF_DEBUG], var)
|
||||
@@ -0,0 +1,63 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "uart_mock.h"
|
||||
|
||||
namespace esphome::uart_mock {
|
||||
|
||||
// This pattern is similar to UARTWriteAction but calls inject_rx instead of write_array, and is parented to VirtualUART
|
||||
// instead of UARTComponent
|
||||
template<typename... Ts> class MockUartInjectRXAction : public Action<Ts...>, public Parented<MockUartComponent> {
|
||||
public:
|
||||
void set_data_template(std::vector<uint8_t> (*func)(Ts...)) {
|
||||
// Stateless lambdas (generated by ESPHome) implicitly convert to function pointers
|
||||
this->code_.func = func;
|
||||
this->len_ = -1; // Sentinel value indicates template mode
|
||||
}
|
||||
|
||||
// Store pointer to static data in flash (no RAM copy)
|
||||
void set_data_static(const uint8_t *data, size_t len) {
|
||||
this->code_.data = data;
|
||||
this->len_ = len; // Length >= 0 indicates static mode
|
||||
}
|
||||
|
||||
void set_delay(uint32_t delay_ms) { this->delay_ms_ = delay_ms; }
|
||||
|
||||
void play(const Ts &...x) override {
|
||||
if (this->len_ >= 0) {
|
||||
// Static mode: use pointer and length
|
||||
if (this->delay_ms_ > 0) {
|
||||
std::vector<uint8_t> data(this->code_.data, this->code_.data + this->len_);
|
||||
this->parent_->inject_to_rx_buffer_delayed(data, this->delay_ms_);
|
||||
} else {
|
||||
this->parent_->inject_to_rx_buffer(this->code_.data, static_cast<size_t>(this->len_));
|
||||
}
|
||||
} else {
|
||||
// Template mode: call function
|
||||
auto val = this->code_.func(x...);
|
||||
if (this->delay_ms_ > 0) {
|
||||
this->parent_->inject_to_rx_buffer_delayed(val, this->delay_ms_);
|
||||
} else {
|
||||
this->parent_->inject_to_rx_buffer(val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
uint32_t delay_ms_{0};
|
||||
ssize_t len_{-1}; // -1 = template mode, >=0 = static mode with length
|
||||
union Code {
|
||||
std::vector<uint8_t> (*func)(Ts...); // Function pointer (stateless lambdas)
|
||||
const uint8_t *data; // Pointer to static data in flash
|
||||
} code_;
|
||||
};
|
||||
|
||||
class MockUartTXTrigger : public Trigger<std::vector<uint8_t>> {
|
||||
public:
|
||||
explicit MockUartTXTrigger(MockUartComponent *parent) {
|
||||
parent->set_tx_hook([this](std::vector<uint8_t> data) { this->trigger(data); });
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace esphome::uart_mock
|
||||
@@ -0,0 +1,232 @@
|
||||
// Host-only test component — do not copy to production code.
|
||||
// See uart_mock.h for details.
|
||||
|
||||
#include "uart_mock.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::uart_mock {
|
||||
|
||||
static const char *const TAG = "uart_mock";
|
||||
|
||||
void MockUartComponent::setup() {
|
||||
ESP_LOGI(TAG, "Mock UART initialized with %zu injections, %zu responses, %zu periodic", this->injections_.size(),
|
||||
this->responses_.size(), this->periodic_rx_.size());
|
||||
}
|
||||
|
||||
void MockUartComponent::loop() {
|
||||
if (!this->loop_started_) {
|
||||
this->loop_started_ = true;
|
||||
if (this->auto_start_) {
|
||||
this->start_scenario();
|
||||
} else {
|
||||
ESP_LOGD(TAG, "Scenario waiting for manual start");
|
||||
}
|
||||
}
|
||||
|
||||
if (!this->scenario_active_) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t now = App.get_loop_component_start_time();
|
||||
|
||||
// Process at most ONE timed injection per loop iteration.
|
||||
// This ensures each injection is in a separate loop cycle, giving the consuming
|
||||
// component (e.g., LD2410) a chance to process each batch independently.
|
||||
if (this->injection_index_ < this->injections_.size()) {
|
||||
auto &injection = this->injections_[this->injection_index_];
|
||||
uint32_t total_delay = this->cumulative_delay_ms_ + injection.delay_ms;
|
||||
if (now - this->scenario_start_ms_ >= total_delay) {
|
||||
ESP_LOGD(TAG, "Injecting %zu RX bytes (injection %u)", injection.rx_data.size(), this->injection_index_);
|
||||
this->inject_to_rx_buffer(injection.rx_data);
|
||||
this->cumulative_delay_ms_ += injection.delay_ms;
|
||||
this->injection_index_++;
|
||||
}
|
||||
}
|
||||
|
||||
// Process periodic RX
|
||||
for (auto &periodic : this->periodic_rx_) {
|
||||
if (now - periodic.last_inject_ms >= periodic.interval_ms) {
|
||||
this->inject_to_rx_buffer(periodic.data);
|
||||
periodic.last_inject_ms = now;
|
||||
}
|
||||
}
|
||||
|
||||
// Process staged RX - deliver bytes whose delay has elapsed
|
||||
uint32_t now_ms = millis();
|
||||
while (!this->staged_rx_.empty() && (static_cast<int32_t>(now_ms - this->staged_rx_.front().available_at_ms) >= 0)) {
|
||||
auto &staged = this->staged_rx_.front();
|
||||
ESP_LOGD(TAG, "Delivering %zu staged RX bytes", staged.data.size());
|
||||
this->inject_to_rx_buffer(staged.data);
|
||||
this->staged_rx_.pop_front();
|
||||
}
|
||||
|
||||
// Process delayed responses
|
||||
for (auto &response : this->responses_) {
|
||||
if (response.delay_ms > 0 && response.last_match_ms > 0 && now - response.last_match_ms >= response.delay_ms) {
|
||||
ESP_LOGD(TAG, "Injecting %zu RX bytes for delayed response", response.inject_rx.size());
|
||||
this->inject_to_rx_buffer(response.inject_rx);
|
||||
response.last_match_ms = 0; // Reset to prevent repeated injection
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MockUartComponent::start_scenario() {
|
||||
uint32_t now = App.get_loop_component_start_time();
|
||||
this->scenario_active_ = true;
|
||||
this->scenario_start_ms_ = now;
|
||||
this->cumulative_delay_ms_ = 0;
|
||||
this->injection_index_ = 0;
|
||||
this->tx_buffer_.clear();
|
||||
for (auto &periodic : this->periodic_rx_) {
|
||||
periodic.last_inject_ms = now;
|
||||
}
|
||||
ESP_LOGD(TAG, "Scenario started at %u ms", now);
|
||||
}
|
||||
|
||||
void MockUartComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"Mock UART Component:\n"
|
||||
" Baud Rate: %u\n"
|
||||
" Injections: %zu\n"
|
||||
" Responses: %zu\n"
|
||||
" Periodic RX: %zu",
|
||||
this->baud_rate_, this->injections_.size(), this->responses_.size(), this->periodic_rx_.size());
|
||||
}
|
||||
|
||||
void MockUartComponent::write_array(const uint8_t *data, size_t len) {
|
||||
this->tx_count_ += len;
|
||||
this->tx_buffer_.insert(this->tx_buffer_.end(), data, data + len);
|
||||
|
||||
// Log all TX data so tests can verify what the component sends
|
||||
if (len > 0 && len <= 64) {
|
||||
char hex_buf[format_hex_pretty_size(64)];
|
||||
ESP_LOGD(TAG, "TX %zu bytes: %s", len, format_hex_pretty_to(hex_buf, sizeof(hex_buf), data, len));
|
||||
} else if (len > 64) {
|
||||
ESP_LOGD(TAG, "TX %zu bytes (too large to log)", len);
|
||||
}
|
||||
|
||||
#ifdef USE_UART_DEBUGGER
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
this->debug_callback_.call(uart::UART_DIRECTION_TX, data[i]);
|
||||
}
|
||||
#endif
|
||||
|
||||
// Responses are always active - they are request-response pairs triggered by
|
||||
// component TX, not timed injections. No race condition with test subscription.
|
||||
this->try_match_response_();
|
||||
|
||||
// This directly calls a tx_hook (lambda) as an alternative to the simpler match_response mechanism.
|
||||
if (this->tx_hook_) {
|
||||
std::vector<uint8_t> buf(data, data + len);
|
||||
this->tx_hook_(buf);
|
||||
}
|
||||
}
|
||||
|
||||
bool MockUartComponent::peek_byte(uint8_t *data) {
|
||||
if (this->rx_buffer_.empty()) {
|
||||
return false;
|
||||
}
|
||||
*data = this->rx_buffer_.front();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MockUartComponent::read_array(uint8_t *data, size_t len) {
|
||||
if (this->rx_buffer_.size() < len) {
|
||||
return false;
|
||||
}
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
data[i] = this->rx_buffer_.front();
|
||||
this->rx_buffer_.pop_front();
|
||||
}
|
||||
this->rx_count_ += len;
|
||||
|
||||
#ifdef USE_UART_DEBUGGER
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
this->debug_callback_.call(uart::UART_DIRECTION_RX, data[i]);
|
||||
}
|
||||
#endif
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
size_t MockUartComponent::available() { return this->rx_buffer_.size(); }
|
||||
|
||||
uart::FlushResult MockUartComponent::flush() {
|
||||
// Nothing to flush in mock
|
||||
return uart::FlushResult::ASSUMED_SUCCESS;
|
||||
}
|
||||
|
||||
void MockUartComponent::set_rx_full_threshold(size_t rx_full_threshold) {
|
||||
this->rx_full_threshold_ = rx_full_threshold;
|
||||
}
|
||||
|
||||
void MockUartComponent::set_rx_timeout(size_t rx_timeout) { this->rx_timeout_ = rx_timeout; }
|
||||
|
||||
void MockUartComponent::add_injection(const std::vector<uint8_t> &rx_data, uint32_t delay_ms) {
|
||||
this->injections_.push_back({rx_data, delay_ms});
|
||||
}
|
||||
|
||||
void MockUartComponent::add_response(const std::vector<uint8_t> &expect_tx, const std::vector<uint8_t> &inject_rx,
|
||||
uint32_t delay_ms) {
|
||||
this->responses_.push_back({expect_tx, inject_rx, delay_ms, 0});
|
||||
}
|
||||
|
||||
void MockUartComponent::add_periodic_rx(const std::vector<uint8_t> &data, uint32_t interval_ms) {
|
||||
this->periodic_rx_.push_back({data, interval_ms, 0});
|
||||
}
|
||||
|
||||
void MockUartComponent::try_match_response_() {
|
||||
for (auto &response : this->responses_) {
|
||||
if (this->tx_buffer_.size() < response.expect_tx.size()) {
|
||||
continue;
|
||||
}
|
||||
// Check if tx_buffer_ ends with expect_tx
|
||||
size_t offset = this->tx_buffer_.size() - response.expect_tx.size();
|
||||
if (std::equal(response.expect_tx.begin(), response.expect_tx.end(), this->tx_buffer_.begin() + offset)) {
|
||||
ESP_LOGD(TAG, "TX match found, injecting %zu RX bytes", response.inject_rx.size());
|
||||
if (response.delay_ms > 0) {
|
||||
ESP_LOGD(TAG, "Delaying response by %u ms", response.delay_ms);
|
||||
// Schedule the response injection as a future injection
|
||||
response.last_match_ms = App.get_loop_component_start_time();
|
||||
} else {
|
||||
this->inject_to_rx_buffer(response.inject_rx);
|
||||
}
|
||||
this->tx_buffer_.clear();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MockUartComponent::inject_to_rx_buffer(const uint8_t *data, size_t len) {
|
||||
std::vector<uint8_t> vec(data, data + len);
|
||||
this->inject_to_rx_buffer(vec);
|
||||
}
|
||||
|
||||
void MockUartComponent::inject_to_rx_buffer(const std::vector<uint8_t> &data) {
|
||||
// Log injected RX data so tests can see what's being fed to the component
|
||||
if (!data.empty() && data.size() <= 64) {
|
||||
char hex_buf[format_hex_pretty_size(64)];
|
||||
ESP_LOGD(TAG, "RX inject %zu bytes: %s", data.size(),
|
||||
format_hex_pretty_to(hex_buf, sizeof(hex_buf), data.data(), data.size()));
|
||||
} else if (data.size() > 64) {
|
||||
ESP_LOGD(TAG, "RX inject %zu bytes (too large to log inline)", data.size());
|
||||
}
|
||||
for (uint8_t byte : data) {
|
||||
this->rx_buffer_.push_back(byte);
|
||||
}
|
||||
}
|
||||
|
||||
void MockUartComponent::inject_to_rx_buffer_delayed(const std::vector<uint8_t> &data, uint32_t delay_ms) {
|
||||
if (!data.empty() && data.size() <= 64) {
|
||||
char hex_buf[format_hex_pretty_size(64)];
|
||||
ESP_LOGD(TAG, "Staging %zu RX bytes with %ums delay: %s", data.size(), delay_ms,
|
||||
format_hex_pretty_to(hex_buf, sizeof(hex_buf), data.data(), data.size()));
|
||||
} else if (data.size() > 64) {
|
||||
ESP_LOGD(TAG, "Staging %zu RX bytes with %ums delay (too large to log inline)", data.size(), delay_ms);
|
||||
}
|
||||
this->staged_rx_.push_back({data, millis() + delay_ms});
|
||||
}
|
||||
|
||||
} // namespace esphome::uart_mock
|
||||
@@ -0,0 +1,103 @@
|
||||
#pragma once
|
||||
|
||||
// ============================================================================
|
||||
// HOST-ONLY TEST COMPONENT — DO NOT COPY TO PRODUCTION CODE
|
||||
//
|
||||
// This component runs exclusively on the host platform for integration testing.
|
||||
// It intentionally uses std::vector, std::deque, and dynamic allocation which
|
||||
// would be inappropriate for production embedded components. Do not use this
|
||||
// code as a reference for writing ESPHome components targeting real hardware.
|
||||
// ============================================================================
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/uart/uart_component.h"
|
||||
#include <deque>
|
||||
#include <vector>
|
||||
|
||||
namespace esphome::uart_mock {
|
||||
|
||||
class MockUartComponent : public uart::UARTComponent, public Component {
|
||||
public:
|
||||
void setup() override;
|
||||
void loop() override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override { return setup_priority::BUS; }
|
||||
|
||||
// UARTComponent interface
|
||||
void write_array(const uint8_t *data, size_t len) override;
|
||||
bool peek_byte(uint8_t *data) override;
|
||||
bool read_array(uint8_t *data, size_t len) override;
|
||||
size_t available() override;
|
||||
uart::FlushResult flush() override;
|
||||
void set_rx_full_threshold(size_t rx_full_threshold) override;
|
||||
void set_rx_timeout(size_t rx_timeout) override;
|
||||
|
||||
// Scenario configuration - called from generated code
|
||||
void add_injection(const std::vector<uint8_t> &rx_data, uint32_t delay_ms);
|
||||
void add_response(const std::vector<uint8_t> &expect_tx, const std::vector<uint8_t> &inject_rx,
|
||||
uint32_t delay_ms = 0);
|
||||
void add_periodic_rx(const std::vector<uint8_t> &data, uint32_t interval_ms);
|
||||
|
||||
void start_scenario();
|
||||
void set_auto_start(bool auto_start) { this->auto_start_ = auto_start; }
|
||||
void set_tx_hook(std::function<void(const std::vector<uint8_t> &)> &&cb) { this->tx_hook_ = std::move(cb); }
|
||||
void inject_to_rx_buffer(const std::vector<uint8_t> &data);
|
||||
void inject_to_rx_buffer(const uint8_t *data, size_t len);
|
||||
// Stage bytes for delayed delivery - simulates transport-level latency (e.g., USB packets)
|
||||
void inject_to_rx_buffer_delayed(const std::vector<uint8_t> &data, uint32_t delay_ms);
|
||||
|
||||
protected:
|
||||
void check_logger_conflict() override {}
|
||||
void try_match_response_();
|
||||
|
||||
// Timed injections
|
||||
struct Injection {
|
||||
std::vector<uint8_t> rx_data;
|
||||
uint32_t delay_ms;
|
||||
};
|
||||
std::vector<Injection> injections_;
|
||||
uint32_t injection_index_{0};
|
||||
uint32_t scenario_start_ms_{0};
|
||||
uint32_t cumulative_delay_ms_{0};
|
||||
bool loop_started_{false};
|
||||
bool auto_start_{true};
|
||||
bool scenario_active_{false};
|
||||
|
||||
// TX-triggered responses
|
||||
struct Response {
|
||||
std::vector<uint8_t> expect_tx;
|
||||
std::vector<uint8_t> inject_rx;
|
||||
uint32_t delay_ms;
|
||||
uint32_t last_match_ms{0};
|
||||
};
|
||||
std::vector<Response> responses_;
|
||||
std::vector<uint8_t> tx_buffer_;
|
||||
|
||||
// RX buffer
|
||||
std::deque<uint8_t> rx_buffer_;
|
||||
|
||||
// Periodic RX
|
||||
struct PeriodicRx {
|
||||
std::vector<uint8_t> data;
|
||||
uint32_t interval_ms;
|
||||
uint32_t last_inject_ms{0};
|
||||
};
|
||||
std::vector<PeriodicRx> periodic_rx_;
|
||||
|
||||
// Staged RX - bytes that are pending delivery after a delay
|
||||
// Simulates transport-level latency (e.g., USB packet delivery)
|
||||
struct StagedRx {
|
||||
std::vector<uint8_t> data;
|
||||
uint32_t available_at_ms; // millis() time when bytes become available
|
||||
};
|
||||
std::deque<StagedRx> staged_rx_;
|
||||
|
||||
// Observability
|
||||
uint32_t tx_count_{0};
|
||||
uint32_t rx_count_{0};
|
||||
|
||||
// Direct TX hook for tests that want to bypass the response-matching logic
|
||||
std::function<void(const std::vector<uint8_t> &)> tx_hook_;
|
||||
};
|
||||
|
||||
} // namespace esphome::uart_mock
|
||||
@@ -0,0 +1,61 @@
|
||||
esphome:
|
||||
name: micros-to-millis-test
|
||||
platformio_options:
|
||||
build_flags:
|
||||
- "-DDEBUG"
|
||||
on_boot:
|
||||
- lambda: |-
|
||||
using esphome::micros_to_millis;
|
||||
const char *TAG = "MTM";
|
||||
int pass = 0, fail = 0;
|
||||
|
||||
auto check = [&](const char *name, uint64_t us) {
|
||||
uint32_t got = micros_to_millis(us);
|
||||
uint32_t want = (uint32_t)(us / 1000ULL);
|
||||
if (got == want) { pass++; }
|
||||
else { ESP_LOGE(TAG, "%s FAILED: got=%u want=%u", name, got, want); fail++; }
|
||||
};
|
||||
|
||||
// Basic values
|
||||
check("zero", 0);
|
||||
check("below_1ms", 999);
|
||||
check("exactly_1ms", 1000);
|
||||
check("above_1ms", 1001);
|
||||
|
||||
// Shift boundary (1000 = 8 * 125, exercises the >>3 shift)
|
||||
check("shift_7999", 7999);
|
||||
check("shift_8000", 8000);
|
||||
check("shift_8001", 8001);
|
||||
|
||||
// 32-bit boundary
|
||||
check("u32max_minus1", 0xFFFFFFFEULL);
|
||||
check("u32max", 0xFFFFFFFFULL);
|
||||
check("u32max_plus1", 0x100000000ULL);
|
||||
|
||||
// Realistic uptimes
|
||||
check("30_days", 2592000000000ULL);
|
||||
check("1_year", 31536000000000ULL);
|
||||
|
||||
// Carry path: construct x = us>>3 with specific hi/lo that trigger adj overflow
|
||||
{ uint64_t x = (603ULL << 32) | 0xFFFFFFFFU; check("carry_603", x << 3); }
|
||||
{ uint64_t x = (5000ULL << 32) | 0xFFFFFFFFU; check("carry_5000", x << 3); }
|
||||
|
||||
// Carry boundary: exact transition where adj overflows (hi=1000, R=46)
|
||||
{
|
||||
uint32_t hi = 1000;
|
||||
uint32_t thr = 0xFFFFFFFFU - hi * 46U;
|
||||
uint64_t h = (uint64_t)hi << 32;
|
||||
check("carry_before", (h | (thr - 1)) << 3);
|
||||
check("carry_at", (h | thr) << 3);
|
||||
check("carry_after", (h | (thr + 1)) << 3);
|
||||
}
|
||||
|
||||
// Mod-8 variations (exercises the >>3 truncation)
|
||||
for (int i = 0; i < 8; i++) { check("mod8", 2592000000000ULL + i); }
|
||||
|
||||
if (fail == 0) { ESP_LOGI(TAG, "ALL_PASSED %d tests", pass); }
|
||||
else { ESP_LOGE(TAG, "%d FAILED out of %d", fail, pass + fail); }
|
||||
|
||||
host:
|
||||
api:
|
||||
logger:
|
||||
@@ -1,4 +1,5 @@
|
||||
esphome:
|
||||
debug_scheduler: true # Enable scheduler leak detection
|
||||
name: scheduler-bulk-cleanup
|
||||
|
||||
external_components:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
esphome:
|
||||
debug_scheduler: true # Enable scheduler leak detection
|
||||
name: scheduler-defer-cancel
|
||||
|
||||
host:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
esphome:
|
||||
debug_scheduler: true # Enable scheduler leak detection
|
||||
name: scheduler-defer-cancel-regular
|
||||
|
||||
host:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
esphome:
|
||||
debug_scheduler: true # Enable scheduler leak detection
|
||||
name: scheduler-defer-fifo-simple
|
||||
|
||||
host:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
esphome:
|
||||
debug_scheduler: true # Enable scheduler leak detection
|
||||
name: scheduler-defer-stress-test
|
||||
|
||||
external_components:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
esphome:
|
||||
debug_scheduler: true # Enable scheduler leak detection
|
||||
name: scheduler-heap-stress-test
|
||||
|
||||
external_components:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
esphome:
|
||||
debug_scheduler: true # Enable scheduler leak detection
|
||||
name: scheduler-internal-id-test
|
||||
on_boot:
|
||||
priority: -100
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
esphome:
|
||||
debug_scheduler: true # Enable scheduler leak detection
|
||||
name: scheduler-null-name
|
||||
|
||||
host:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
esphome:
|
||||
debug_scheduler: true # Enable scheduler leak detection
|
||||
name: scheduler-numeric-id-test
|
||||
on_boot:
|
||||
priority: -100
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
esphome:
|
||||
debug_scheduler: true # Enable scheduler leak detection
|
||||
name: sched-rapid-cancel-test
|
||||
|
||||
external_components:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
esphome:
|
||||
debug_scheduler: true # Enable scheduler leak detection
|
||||
name: sched-recursive-timeout
|
||||
|
||||
external_components:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
esphome:
|
||||
debug_scheduler: true # Enable scheduler leak detection
|
||||
name: scheduler-removed-item-race
|
||||
|
||||
host:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
esphome:
|
||||
debug_scheduler: true # Enable scheduler leak detection
|
||||
name: scheduler-retry-test
|
||||
on_boot:
|
||||
priority: -100
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
esphome:
|
||||
debug_scheduler: true # Enable scheduler leak detection
|
||||
name: sched-simul-callbacks-test
|
||||
|
||||
external_components:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
esphome:
|
||||
debug_scheduler: true # Enable scheduler leak detection
|
||||
name: scheduler-string-lifetime-test
|
||||
|
||||
external_components:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
esphome:
|
||||
debug_scheduler: true # Enable scheduler leak detection
|
||||
name: sched-string-name-stress
|
||||
|
||||
external_components:
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
esphome:
|
||||
name: uart-mock-ld2410-test
|
||||
|
||||
host:
|
||||
api:
|
||||
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
|
||||
|
||||
uart_mock:
|
||||
id: mock_uart
|
||||
baud_rate: 256000
|
||||
auto_start: false
|
||||
injections:
|
||||
# Phase 1 (t=100ms): Valid LD2410 normal mode data frame - happy path
|
||||
# The buffer is clean at this point, so this frame should parse correctly.
|
||||
# Moving target: 100cm, energy 50
|
||||
# Still target: 120cm, energy 25
|
||||
# Detection distance: 300cm
|
||||
# Target state: 0x03 (moving + still)
|
||||
#
|
||||
# Note: LD2410's two_byte_to_int() uses signed char, so low bytes must be
|
||||
# <=127 to produce correct values. Values >127 wrap negative.
|
||||
#
|
||||
# Frame layout (24 bytes):
|
||||
# [0-3] F4 F3 F2 F1 = data frame header
|
||||
# [4-5] 0D 00 = length 13
|
||||
# [6] 02 = data type (normal)
|
||||
# [7] AA = data header marker
|
||||
# [8] 03 = target states (moving+still)
|
||||
# [9-10] 64 00 = moving distance 100 (0x0064)
|
||||
# [11] 32 = moving energy 50
|
||||
# [12-13] 78 00 = still distance 120 (0x0078)
|
||||
# [14] 19 = still energy 25
|
||||
# [15-16] 2C 01 = detection distance 300 (0x012C)
|
||||
# [17] 00 = padding
|
||||
# [18] 55 = data footer marker
|
||||
# [19] 00 = CRC/check
|
||||
# [20-23] F8 F7 F6 F5 = data frame footer
|
||||
- delay: 100ms
|
||||
inject_rx:
|
||||
[
|
||||
0xF4, 0xF3, 0xF2, 0xF1,
|
||||
0x0D, 0x00,
|
||||
0x02, 0xAA,
|
||||
0x03,
|
||||
0x64, 0x00,
|
||||
0x32,
|
||||
0x78, 0x00,
|
||||
0x19,
|
||||
0x2C, 0x01,
|
||||
0x00,
|
||||
0x55, 0x00,
|
||||
0xF8, 0xF7, 0xF6, 0xF5,
|
||||
]
|
||||
|
||||
# Phase 2 (t=200ms): Garbage bytes - corrupt the buffer
|
||||
# These random bytes will accumulate in the LD2410's internal buffer.
|
||||
# No footer will be found, so the buffer just grows.
|
||||
- delay: 100ms
|
||||
inject_rx: [0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x11, 0x22]
|
||||
|
||||
# Phase 3 (t=300ms): Truncated frame (header + partial data, no footer)
|
||||
# More bytes accumulating in the buffer without a footer match.
|
||||
- delay: 100ms
|
||||
inject_rx: [0xF4, 0xF3, 0xF2, 0xF1, 0x0D, 0x00, 0x02, 0xAA]
|
||||
|
||||
# Phase 4 (t=500ms): Overflow - inject 85 bytes of 0xFF (MAX_LINE_LENGTH=50)
|
||||
# Buffer has 15 bytes from phases 2+3.
|
||||
# Overflow math: need 35 bytes to trigger first overflow (pos 15->49),
|
||||
# then 50 more to trigger second overflow (pos 0->49). Total = 85 bytes.
|
||||
# After two overflows, buffer_pos_ = 0 with a completely clean buffer.
|
||||
# The LD2410 logs "Max command length exceeded" at each overflow.
|
||||
- delay: 200ms
|
||||
inject_rx:
|
||||
[
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
]
|
||||
|
||||
# Phase 5 (t=600ms): Valid frame after overflow - recovery test
|
||||
# Buffer was reset by overflow. This valid frame should parse correctly.
|
||||
# Moving target: 50cm, energy 100
|
||||
# Still target: 75cm, energy 80
|
||||
# Detection distance: 127cm
|
||||
- delay: 100ms
|
||||
inject_rx:
|
||||
[
|
||||
0xF4, 0xF3, 0xF2, 0xF1,
|
||||
0x0D, 0x00,
|
||||
0x02, 0xAA,
|
||||
0x03,
|
||||
0x32, 0x00,
|
||||
0x64,
|
||||
0x4B, 0x00,
|
||||
0x50,
|
||||
0x7F, 0x00,
|
||||
0x00,
|
||||
0x55, 0x00,
|
||||
0xF8, 0xF7, 0xF6, 0xF5,
|
||||
]
|
||||
|
||||
ld2410:
|
||||
id: ld2410_dev
|
||||
uart_id: mock_uart
|
||||
|
||||
sensor:
|
||||
- platform: ld2410
|
||||
ld2410_id: ld2410_dev
|
||||
moving_distance:
|
||||
name: "Moving Distance"
|
||||
still_distance:
|
||||
name: "Still Distance"
|
||||
moving_energy:
|
||||
name: "Moving Energy"
|
||||
still_energy:
|
||||
name: "Still Energy"
|
||||
detection_distance:
|
||||
name: "Detection Distance"
|
||||
|
||||
binary_sensor:
|
||||
- platform: ld2410
|
||||
ld2410_id: ld2410_dev
|
||||
has_target:
|
||||
name: "Has Target"
|
||||
has_moving_target:
|
||||
name: "Has Moving Target"
|
||||
has_still_target:
|
||||
name: "Has Still Target"
|
||||
|
||||
button:
|
||||
- platform: template
|
||||
name: "Start Scenario"
|
||||
id: start_scenario_btn
|
||||
on_press:
|
||||
- lambda: 'id(mock_uart).start_scenario();'
|
||||
@@ -0,0 +1,164 @@
|
||||
esphome:
|
||||
name: uart-mock-ld2410-eng-test
|
||||
|
||||
host:
|
||||
api:
|
||||
logger:
|
||||
level: VERBOSE
|
||||
|
||||
external_components:
|
||||
- source:
|
||||
type: local
|
||||
path: EXTERNAL_COMPONENT_PATH
|
||||
|
||||
# Dummy uart entry to satisfy ld2410's DEPENDENCIES = ["uart"]
|
||||
uart:
|
||||
baud_rate: 115200
|
||||
port: /dev/null
|
||||
|
||||
uart_mock:
|
||||
id: mock_uart
|
||||
baud_rate: 256000
|
||||
auto_start: false
|
||||
injections:
|
||||
# Phase 1 (t=100ms): Valid LD2410 engineering mode data frame
|
||||
# Captured from a real Screek Human Presence Sensor 1U with LD2410 firmware 2.4.x
|
||||
#
|
||||
# Engineering mode frame layout (45 bytes):
|
||||
# [0-3] F4 F3 F2 F1 = data frame header
|
||||
# [4-5] 23 00 = length 35
|
||||
# [6] 01 = data type (engineering mode)
|
||||
# [7] AA = data header marker
|
||||
# [8] 03 = target states (moving+still)
|
||||
# [9-10] 1E 00 = moving distance 30 (0x001E)
|
||||
# [11] 64 = moving energy 100
|
||||
# [12-13] 1E 00 = still distance 30 (0x001E)
|
||||
# [14] 64 = still energy 100
|
||||
# [15-16] 00 00 = detection distance 0
|
||||
# [17] 08 = max moving distance gate
|
||||
# [18] 08 = max still distance gate
|
||||
# [19-27] gate moving energies (gates 0-8)
|
||||
# [28-36] gate still energies (gates 0-8)
|
||||
# [37] 57 = light sensor value 87
|
||||
# [38] 01 = out pin presence (HIGH)
|
||||
# [39] 55 = data footer marker
|
||||
# [40] 00 = check
|
||||
# [41-44] F8 F7 F6 F5 = data frame footer
|
||||
- delay: 100ms
|
||||
inject_rx:
|
||||
[
|
||||
0xF4, 0xF3, 0xF2, 0xF1,
|
||||
0x23, 0x00,
|
||||
0x01, 0xAA,
|
||||
0x03,
|
||||
0x1E, 0x00,
|
||||
0x64,
|
||||
0x1E, 0x00,
|
||||
0x64,
|
||||
0x00, 0x00,
|
||||
0x08, 0x08,
|
||||
0x64, 0x41, 0x06, 0x0E, 0x2B, 0x16, 0x03, 0x03, 0x07,
|
||||
0x00, 0x00, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64,
|
||||
0x57, 0x01,
|
||||
0x55, 0x00,
|
||||
0xF8, 0xF7, 0xF6, 0xF5,
|
||||
]
|
||||
|
||||
# Phase 2 (t=200ms): Second engineering mode frame with different values
|
||||
# Real capture: moving at 73cm, still at 30cm, detection at 33cm
|
||||
- delay: 100ms
|
||||
inject_rx:
|
||||
[
|
||||
0xF4, 0xF3, 0xF2, 0xF1,
|
||||
0x23, 0x00,
|
||||
0x01, 0xAA,
|
||||
0x03,
|
||||
0x49, 0x00,
|
||||
0x64,
|
||||
0x1E, 0x00,
|
||||
0x64,
|
||||
0x21, 0x00,
|
||||
0x08, 0x08,
|
||||
0x11, 0x64, 0x05, 0x29, 0x39, 0x10, 0x03, 0x11, 0x0E,
|
||||
0x00, 0x00, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64,
|
||||
0x57, 0x01,
|
||||
0x55, 0x00,
|
||||
0xF8, 0xF7, 0xF6, 0xF5,
|
||||
]
|
||||
|
||||
# Phase 3 (t=300ms): Frame with still target at 291cm (multi-byte distance)
|
||||
# This tests the two_byte_to_int function with high byte > 0
|
||||
# Note: low byte 0x2F < 0x80 so it avoids the signed char bug
|
||||
- delay: 100ms
|
||||
inject_rx:
|
||||
[
|
||||
0xF4, 0xF3, 0xF2, 0xF1,
|
||||
0x23, 0x00,
|
||||
0x01, 0xAA,
|
||||
0x03,
|
||||
0x2F, 0x00,
|
||||
0x36,
|
||||
0x23, 0x01,
|
||||
0x64,
|
||||
0x21, 0x00,
|
||||
0x08, 0x08,
|
||||
0x2F, 0x36, 0x09, 0x0D, 0x15, 0x0B, 0x06, 0x06, 0x08,
|
||||
0x00, 0x00, 0x64, 0x64, 0x64, 0x64, 0x64, 0x5A, 0x3D,
|
||||
0x57, 0x01,
|
||||
0x55, 0x00,
|
||||
0xF8, 0xF7, 0xF6, 0xF5,
|
||||
]
|
||||
|
||||
ld2410:
|
||||
id: ld2410_dev
|
||||
uart_id: mock_uart
|
||||
|
||||
sensor:
|
||||
- platform: ld2410
|
||||
ld2410_id: ld2410_dev
|
||||
moving_distance:
|
||||
name: "Moving Distance"
|
||||
still_distance:
|
||||
name: "Still Distance"
|
||||
moving_energy:
|
||||
name: "Moving Energy"
|
||||
still_energy:
|
||||
name: "Still Energy"
|
||||
detection_distance:
|
||||
name: "Detection Distance"
|
||||
light:
|
||||
name: "Light"
|
||||
g0:
|
||||
move_energy:
|
||||
name: "Gate 0 Move Energy"
|
||||
still_energy:
|
||||
name: "Gate 0 Still Energy"
|
||||
g1:
|
||||
move_energy:
|
||||
name: "Gate 1 Move Energy"
|
||||
still_energy:
|
||||
name: "Gate 1 Still Energy"
|
||||
g2:
|
||||
move_energy:
|
||||
name: "Gate 2 Move Energy"
|
||||
still_energy:
|
||||
name: "Gate 2 Still Energy"
|
||||
|
||||
binary_sensor:
|
||||
- platform: ld2410
|
||||
ld2410_id: ld2410_dev
|
||||
has_target:
|
||||
name: "Has Target"
|
||||
has_moving_target:
|
||||
name: "Has Moving Target"
|
||||
has_still_target:
|
||||
name: "Has Still Target"
|
||||
out_pin_presence_status:
|
||||
name: "Out Pin Presence"
|
||||
|
||||
button:
|
||||
- platform: template
|
||||
name: "Start Scenario"
|
||||
id: start_scenario_btn
|
||||
on_press:
|
||||
- lambda: 'id(mock_uart).start_scenario();'
|
||||
@@ -0,0 +1,179 @@
|
||||
esphome:
|
||||
name: uart-mock-ld2412-test
|
||||
|
||||
host:
|
||||
api:
|
||||
logger:
|
||||
level: VERBOSE
|
||||
|
||||
external_components:
|
||||
- source:
|
||||
type: local
|
||||
path: EXTERNAL_COMPONENT_PATH
|
||||
|
||||
# Dummy uart entry to satisfy ld2412'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
|
||||
injections:
|
||||
# Phase 1 (t=100ms): Valid LD2412 normal mode data frame - happy path
|
||||
# The buffer is clean at this point, so this frame should parse correctly.
|
||||
# Moving target: 100cm, energy 50
|
||||
# Still target: 120cm, energy 25
|
||||
# Target state: 0x03 (moving + still)
|
||||
# detection_distance = 100 (LD2412 computes from moving target when MOVE_BITMASK set)
|
||||
#
|
||||
# Frame layout (24 bytes):
|
||||
# [0-3] F4 F3 F2 F1 = data frame header
|
||||
# [4-5] 0D 00 = length 13
|
||||
# [6] 02 = data type (normal)
|
||||
# [7] AA = data header marker
|
||||
# [8] 03 = target states (moving+still)
|
||||
# [9-10] 64 00 = moving distance 100 (0x0064)
|
||||
# [11] 32 = moving energy 50
|
||||
# [12-13] 78 00 = still distance 120 (0x0078)
|
||||
# [14] 19 = still energy 25
|
||||
# [15-16] 64 00 = detect distance bytes (ignored by LD2412 code)
|
||||
# [17] 00 = padding
|
||||
# [18] 55 = data footer marker
|
||||
# [19] 00 = CRC/check
|
||||
# [20-23] F8 F7 F6 F5 = data frame footer
|
||||
- delay: 100ms
|
||||
inject_rx:
|
||||
[
|
||||
0xF4, 0xF3, 0xF2, 0xF1,
|
||||
0x0D, 0x00,
|
||||
0x02, 0xAA,
|
||||
0x03,
|
||||
0x64, 0x00,
|
||||
0x32,
|
||||
0x78, 0x00,
|
||||
0x19,
|
||||
0x64, 0x00,
|
||||
0x00,
|
||||
0x55, 0x00,
|
||||
0xF8, 0xF7, 0xF6, 0xF5,
|
||||
]
|
||||
|
||||
# Phase 2 (t=300ms): Garbage bytes
|
||||
# LD2412's parser rejects bytes that don't match the frame header at
|
||||
# position 0 (must start with F4 or FD), so buffer stays empty.
|
||||
- delay: 200ms
|
||||
inject_rx: [0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x11, 0x22]
|
||||
|
||||
# Phase 3 (t=400ms): Truncated frame (header + partial data, no footer)
|
||||
# Starts with valid data frame header so parser accepts it.
|
||||
# After this, buffer_pos_ = 8.
|
||||
- delay: 100ms
|
||||
inject_rx: [0xF4, 0xF3, 0xF2, 0xF1, 0x0D, 0x00, 0x02, 0xAA]
|
||||
|
||||
# Phase 4 (t=600ms): Overflow - inject 60 bytes of 0xFF (MAX_LINE_LENGTH=54)
|
||||
# Buffer has 8 bytes from phase 3 (garbage in phase 2 was rejected).
|
||||
# Overflow math: buffer_pos_ starts at 8, overflow triggers when
|
||||
# buffer_pos_ reaches 53 (MAX_LINE_LENGTH - 1). Need 45 more bytes to
|
||||
# fill positions 8-52, then byte 46 triggers overflow. After overflow,
|
||||
# buffer_pos_ = 0 and remaining 0xFF bytes are rejected (don't match header).
|
||||
- delay: 200ms
|
||||
inject_rx:
|
||||
[
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
]
|
||||
|
||||
# Phase 5 (t=700ms): Valid frame after overflow - recovery test
|
||||
# Buffer was reset by overflow. This valid frame should parse correctly.
|
||||
# Moving target: 50cm, energy 100
|
||||
# Still target: 75cm, energy 80
|
||||
# detection_distance = 50 (moving target distance, since MOVE_BITMASK set)
|
||||
- delay: 100ms
|
||||
inject_rx:
|
||||
[
|
||||
0xF4, 0xF3, 0xF2, 0xF1,
|
||||
0x0D, 0x00,
|
||||
0x02, 0xAA,
|
||||
0x03,
|
||||
0x32, 0x00,
|
||||
0x64,
|
||||
0x4B, 0x00,
|
||||
0x50,
|
||||
0x32, 0x00,
|
||||
0x00,
|
||||
0x55, 0x00,
|
||||
0xF8, 0xF7, 0xF6, 0xF5,
|
||||
]
|
||||
|
||||
ld2412:
|
||||
id: ld2412_dev
|
||||
uart_id: mock_uart
|
||||
|
||||
sensor:
|
||||
- platform: ld2412
|
||||
ld2412_id: ld2412_dev
|
||||
moving_distance:
|
||||
name: "Moving Distance"
|
||||
filters:
|
||||
- timeout:
|
||||
timeout: 50ms
|
||||
value: last
|
||||
- throttle_with_priority: 50ms
|
||||
still_distance:
|
||||
name: "Still Distance"
|
||||
filters:
|
||||
- timeout:
|
||||
timeout: 50ms
|
||||
value: last
|
||||
- throttle_with_priority: 50ms
|
||||
moving_energy:
|
||||
name: "Moving Energy"
|
||||
filters:
|
||||
- timeout:
|
||||
timeout: 50ms
|
||||
value: last
|
||||
- throttle_with_priority: 50ms
|
||||
still_energy:
|
||||
name: "Still Energy"
|
||||
filters:
|
||||
- timeout:
|
||||
timeout: 50ms
|
||||
value: last
|
||||
- throttle_with_priority: 50ms
|
||||
detection_distance:
|
||||
name: "Detection Distance"
|
||||
filters:
|
||||
- timeout:
|
||||
timeout: 50ms
|
||||
value: last
|
||||
- throttle_with_priority: 50ms
|
||||
|
||||
binary_sensor:
|
||||
- platform: ld2412
|
||||
ld2412_id: ld2412_dev
|
||||
has_target:
|
||||
name: "Has Target"
|
||||
filters:
|
||||
- settle: 50ms
|
||||
has_moving_target:
|
||||
name: "Has Moving Target"
|
||||
filters:
|
||||
- settle: 50ms
|
||||
has_still_target:
|
||||
name: "Has Still Target"
|
||||
filters:
|
||||
- settle: 50ms
|
||||
|
||||
button:
|
||||
- platform: template
|
||||
name: "Start Scenario"
|
||||
id: start_scenario_btn
|
||||
on_press:
|
||||
- lambda: 'id(mock_uart).start_scenario();'
|
||||
@@ -0,0 +1,182 @@
|
||||
esphome:
|
||||
name: uart-mock-ld2412-eng-test
|
||||
|
||||
host:
|
||||
api:
|
||||
logger:
|
||||
level: VERBOSE
|
||||
|
||||
external_components:
|
||||
- source:
|
||||
type: local
|
||||
path: EXTERNAL_COMPONENT_PATH
|
||||
|
||||
# Dummy uart entry to satisfy ld2412's DEPENDENCIES = ["uart"]
|
||||
uart:
|
||||
baud_rate: 115200
|
||||
port: /dev/null
|
||||
|
||||
uart_mock:
|
||||
id: mock_uart
|
||||
baud_rate: 256000
|
||||
auto_start: false
|
||||
injections:
|
||||
# Phase 1 (t=100ms): Valid LD2412 engineering mode data frame
|
||||
#
|
||||
# Engineering mode frame layout (52 bytes):
|
||||
# [0-3] F4 F3 F2 F1 = data frame header
|
||||
# [4-5] 2A 00 = length 42
|
||||
# [6] 01 = data type (engineering mode)
|
||||
# [7] AA = data header marker
|
||||
# [8] 03 = target states (moving+still)
|
||||
# [9-10] 1E 00 = moving distance 30 (0x001E)
|
||||
# [11] 64 = moving energy 100
|
||||
# [12-13] 1E 00 = still distance 30 (0x001E)
|
||||
# [14] 64 = still energy 100
|
||||
# [15-16] 00 00 = detection distance bytes (ignored)
|
||||
# [17-30] gate moving energies (14 gates)
|
||||
# [31-44] gate still energies (14 gates)
|
||||
# [45] 57 = light sensor value 87
|
||||
# [46] 55 = data footer marker
|
||||
# [47] 00 = check
|
||||
# [48-51] F8 F7 F6 F5 = data frame footer
|
||||
- delay: 100ms
|
||||
inject_rx:
|
||||
[
|
||||
0xF4, 0xF3, 0xF2, 0xF1,
|
||||
0x2A, 0x00,
|
||||
0x01, 0xAA,
|
||||
0x03,
|
||||
0x1E, 0x00,
|
||||
0x64,
|
||||
0x1E, 0x00,
|
||||
0x64,
|
||||
0x00, 0x00,
|
||||
0x64, 0x41, 0x06, 0x0E, 0x2B, 0x16, 0x03, 0x03, 0x07, 0x05, 0x09, 0x08, 0x07, 0x06,
|
||||
0x00, 0x00, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x50, 0x40, 0x30, 0x20, 0x10,
|
||||
0x57,
|
||||
0x55, 0x00,
|
||||
0xF8, 0xF7, 0xF6, 0xF5,
|
||||
]
|
||||
|
||||
# Phase 2 (t=200ms): Second engineering mode frame with different values
|
||||
# Moving at 73cm, still at 30cm
|
||||
- delay: 100ms
|
||||
inject_rx:
|
||||
[
|
||||
0xF4, 0xF3, 0xF2, 0xF1,
|
||||
0x2A, 0x00,
|
||||
0x01, 0xAA,
|
||||
0x03,
|
||||
0x49, 0x00,
|
||||
0x64,
|
||||
0x1E, 0x00,
|
||||
0x64,
|
||||
0x21, 0x00,
|
||||
0x11, 0x64, 0x05, 0x29, 0x39, 0x10, 0x03, 0x11, 0x0E, 0x08, 0x06, 0x04, 0x03, 0x02,
|
||||
0x00, 0x00, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x50, 0x40, 0x30, 0x20, 0x10,
|
||||
0x57,
|
||||
0x55, 0x00,
|
||||
0xF8, 0xF7, 0xF6, 0xF5,
|
||||
]
|
||||
|
||||
# Phase 3 (t=300ms): Frame with still target at 291cm (multi-byte distance)
|
||||
# This tests encode_uint16 with high byte > 0
|
||||
# Target state: 0x02 (still only) -> detection_distance = still distance = 291
|
||||
- delay: 100ms
|
||||
inject_rx:
|
||||
[
|
||||
0xF4, 0xF3, 0xF2, 0xF1,
|
||||
0x2A, 0x00,
|
||||
0x01, 0xAA,
|
||||
0x02,
|
||||
0x2F, 0x00,
|
||||
0x36,
|
||||
0x23, 0x01,
|
||||
0x64,
|
||||
0x21, 0x00,
|
||||
0x2F, 0x36, 0x09, 0x0D, 0x15, 0x0B, 0x06, 0x06, 0x08, 0x09, 0x08, 0x07, 0x06, 0x05,
|
||||
0x00, 0x00, 0x64, 0x64, 0x64, 0x64, 0x64, 0x5A, 0x3D, 0x30, 0x20, 0x10, 0x08, 0x04,
|
||||
0x57,
|
||||
0x55, 0x00,
|
||||
0xF8, 0xF7, 0xF6, 0xF5,
|
||||
]
|
||||
|
||||
# Common filter definitions
|
||||
.sensor_filters: &sensor_filters
|
||||
filters:
|
||||
- timeout:
|
||||
timeout: 50ms
|
||||
value: last
|
||||
- throttle_with_priority: 50ms
|
||||
|
||||
.binary_filters: &binary_filters
|
||||
filters:
|
||||
- settle: 50ms
|
||||
|
||||
ld2412:
|
||||
id: ld2412_dev
|
||||
uart_id: mock_uart
|
||||
|
||||
sensor:
|
||||
- platform: ld2412
|
||||
ld2412_id: ld2412_dev
|
||||
moving_distance:
|
||||
name: "Moving Distance"
|
||||
<<: *sensor_filters
|
||||
still_distance:
|
||||
name: "Still Distance"
|
||||
<<: *sensor_filters
|
||||
moving_energy:
|
||||
name: "Moving Energy"
|
||||
<<: *sensor_filters
|
||||
still_energy:
|
||||
name: "Still Energy"
|
||||
<<: *sensor_filters
|
||||
detection_distance:
|
||||
name: "Detection Distance"
|
||||
<<: *sensor_filters
|
||||
light:
|
||||
name: "Light"
|
||||
<<: *sensor_filters
|
||||
gate_0:
|
||||
move_energy:
|
||||
name: "Gate 0 Move Energy"
|
||||
<<: *sensor_filters
|
||||
still_energy:
|
||||
name: "Gate 0 Still Energy"
|
||||
<<: *sensor_filters
|
||||
gate_1:
|
||||
move_energy:
|
||||
name: "Gate 1 Move Energy"
|
||||
<<: *sensor_filters
|
||||
still_energy:
|
||||
name: "Gate 1 Still Energy"
|
||||
<<: *sensor_filters
|
||||
gate_2:
|
||||
move_energy:
|
||||
name: "Gate 2 Move Energy"
|
||||
<<: *sensor_filters
|
||||
still_energy:
|
||||
name: "Gate 2 Still Energy"
|
||||
<<: *sensor_filters
|
||||
|
||||
binary_sensor:
|
||||
- platform: ld2412
|
||||
ld2412_id: ld2412_dev
|
||||
has_target:
|
||||
name: "Has Target"
|
||||
<<: *binary_filters
|
||||
has_moving_target:
|
||||
name: "Has Moving Target"
|
||||
<<: *binary_filters
|
||||
has_still_target:
|
||||
name: "Has Still Target"
|
||||
<<: *binary_filters
|
||||
|
||||
button:
|
||||
- platform: template
|
||||
name: "Start Scenario"
|
||||
id: start_scenario_btn
|
||||
on_press:
|
||||
- lambda: 'id(mock_uart).start_scenario();'
|
||||
@@ -0,0 +1,167 @@
|
||||
esphome:
|
||||
name: uart-mock-ld2412-eng-trunc
|
||||
|
||||
host:
|
||||
api:
|
||||
logger:
|
||||
level: VERBOSE
|
||||
|
||||
external_components:
|
||||
- source:
|
||||
type: local
|
||||
path: EXTERNAL_COMPONENT_PATH
|
||||
|
||||
# Dummy uart entry to satisfy ld2412's DEPENDENCIES = ["uart"]
|
||||
uart:
|
||||
baud_rate: 115200
|
||||
port: /dev/null
|
||||
|
||||
uart_mock:
|
||||
id: mock_uart
|
||||
baud_rate: 256000
|
||||
auto_start: false
|
||||
injections:
|
||||
# Phase 1 (t=100ms): Valid engineering mode frame (52 bytes, buffer_pos_=52)
|
||||
# Establishes baseline: gate_0_move=100, light=87
|
||||
- delay: 100ms
|
||||
inject_rx:
|
||||
[
|
||||
0xF4, 0xF3, 0xF2, 0xF1,
|
||||
0x2A, 0x00,
|
||||
0x01, 0xAA,
|
||||
0x03,
|
||||
0x1E, 0x00,
|
||||
0x64,
|
||||
0x1E, 0x00,
|
||||
0x64,
|
||||
0x00, 0x00,
|
||||
0x64, 0x41, 0x06, 0x0E, 0x2B, 0x16, 0x03, 0x03, 0x07, 0x05, 0x09, 0x08, 0x07, 0x06,
|
||||
0x00, 0x00, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x50, 0x40, 0x30, 0x20, 0x10,
|
||||
0x57,
|
||||
0x55, 0x00,
|
||||
0xF8, 0xF7, 0xF6, 0xF5,
|
||||
]
|
||||
|
||||
# Phase 2 (t=200ms): Truncated engineering mode frame (24 bytes, buffer_pos_=24)
|
||||
# This frame has data_type=0x01 (engineering) but only enough data for the
|
||||
# basic target fields, not the gate energies or light sensor.
|
||||
# buffer_pos_=24 passes the old check (>= 12) but fails the new check (< 46).
|
||||
# Without the fix, indices 17-45 would read stale buffer data from Phase 1.
|
||||
#
|
||||
# Layout (24 bytes):
|
||||
# [0-3] F4 F3 F2 F1 = data frame header
|
||||
# [4-5] 0E 00 = length 14
|
||||
# [6] 01 = data type (engineering mode)
|
||||
# [7] AA = data header marker
|
||||
# [8] 03 = target states (moving+still)
|
||||
# [9-10] 1E 00 = moving distance 30
|
||||
# [11] 50 = moving energy 80
|
||||
# [12-13] 1E 00 = still distance 30
|
||||
# [14] 50 = still energy 80
|
||||
# [15-16] FF FF = garbage detection distance bytes
|
||||
# [17] FF = padding (would be gate data in full frame)
|
||||
# [18] 55 = data footer marker (at buffer_pos_ - 6)
|
||||
# [19] 00 = check byte
|
||||
# [20-23] F8 F7 F6 F5 = data frame footer
|
||||
- delay: 100ms
|
||||
inject_rx:
|
||||
[
|
||||
0xF4, 0xF3, 0xF2, 0xF1,
|
||||
0x0E, 0x00,
|
||||
0x01, 0xAA,
|
||||
0x03,
|
||||
0x1E, 0x00,
|
||||
0x50,
|
||||
0x1E, 0x00,
|
||||
0x50,
|
||||
0xFF, 0xFF,
|
||||
0xFF,
|
||||
0x55, 0x00,
|
||||
0xF8, 0xF7, 0xF6, 0xF5,
|
||||
]
|
||||
|
||||
# Phase 3 (t=300ms): Valid recovery frame with different values
|
||||
# gate_0_move=50, light=42 — proves component recovered
|
||||
- delay: 100ms
|
||||
inject_rx:
|
||||
[
|
||||
0xF4, 0xF3, 0xF2, 0xF1,
|
||||
0x2A, 0x00,
|
||||
0x01, 0xAA,
|
||||
0x03,
|
||||
0x1E, 0x00,
|
||||
0x64,
|
||||
0x1E, 0x00,
|
||||
0x64,
|
||||
0x00, 0x00,
|
||||
0x32, 0x20, 0x06, 0x0E, 0x2B, 0x16, 0x03, 0x03, 0x07, 0x05, 0x09, 0x08, 0x07, 0x06,
|
||||
0x00, 0x00, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x28, 0x20, 0x18, 0x10, 0x08,
|
||||
0x2A,
|
||||
0x55, 0x00,
|
||||
0xF8, 0xF7, 0xF6, 0xF5,
|
||||
]
|
||||
|
||||
# Common filter definitions
|
||||
.sensor_filters: &sensor_filters
|
||||
filters:
|
||||
- timeout:
|
||||
timeout: 50ms
|
||||
value: last
|
||||
- throttle_with_priority: 50ms
|
||||
|
||||
.binary_filters: &binary_filters
|
||||
filters:
|
||||
- settle: 50ms
|
||||
|
||||
ld2412:
|
||||
id: ld2412_dev
|
||||
uart_id: mock_uart
|
||||
|
||||
sensor:
|
||||
- platform: ld2412
|
||||
ld2412_id: ld2412_dev
|
||||
moving_distance:
|
||||
name: "Moving Distance"
|
||||
<<: *sensor_filters
|
||||
still_distance:
|
||||
name: "Still Distance"
|
||||
<<: *sensor_filters
|
||||
moving_energy:
|
||||
name: "Moving Energy"
|
||||
<<: *sensor_filters
|
||||
still_energy:
|
||||
name: "Still Energy"
|
||||
<<: *sensor_filters
|
||||
detection_distance:
|
||||
name: "Detection Distance"
|
||||
<<: *sensor_filters
|
||||
light:
|
||||
name: "Light"
|
||||
<<: *sensor_filters
|
||||
gate_0:
|
||||
move_energy:
|
||||
name: "Gate 0 Move Energy"
|
||||
<<: *sensor_filters
|
||||
still_energy:
|
||||
name: "Gate 0 Still Energy"
|
||||
<<: *sensor_filters
|
||||
|
||||
binary_sensor:
|
||||
- platform: ld2412
|
||||
ld2412_id: ld2412_dev
|
||||
has_target:
|
||||
name: "Has Target"
|
||||
<<: *binary_filters
|
||||
has_moving_target:
|
||||
name: "Has Moving Target"
|
||||
<<: *binary_filters
|
||||
has_still_target:
|
||||
name: "Has Still Target"
|
||||
<<: *binary_filters
|
||||
|
||||
button:
|
||||
- platform: template
|
||||
name: "Start Scenario"
|
||||
id: start_scenario_btn
|
||||
on_press:
|
||||
- lambda: 'id(mock_uart).start_scenario();'
|
||||
@@ -0,0 +1,187 @@
|
||||
esphome:
|
||||
name: uart-mock-ld2420-test
|
||||
|
||||
host:
|
||||
api:
|
||||
logger:
|
||||
level: VERBOSE
|
||||
|
||||
external_components:
|
||||
- source:
|
||||
type: local
|
||||
path: EXTERNAL_COMPONENT_PATH
|
||||
|
||||
# Dummy uart entry to satisfy ld2420'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: 115200
|
||||
auto_start: false
|
||||
responses:
|
||||
# Version-specific response: match the complete firmware version command TX.
|
||||
# CMD_READ_VERSION (0x0000) TX = FD FC FB FA 02 00 00 00 04 03 02 01
|
||||
# Returns "v2.0.0" → get_firmware_int = 200 >= 154 → energy mode
|
||||
#
|
||||
# Response layout:
|
||||
# [0-3] FD FC FB FA = header
|
||||
# [4-5] 0C 00 = length 12
|
||||
# [6] 00 = cmd (CMD_READ_VERSION)
|
||||
# [7] 01 = status (ACK)
|
||||
# [8-9] 00 00 = error = 0
|
||||
# [10] 06 = ver_len = 6
|
||||
# [11] 00 = padding
|
||||
# [12-17] "v2.0.0" = version string
|
||||
# [18-21] 04 03 02 01 = footer
|
||||
- expect_tx:
|
||||
[0xFD, 0xFC, 0xFB, 0xFA, 0x02, 0x00, 0x00, 0x00, 0x04, 0x03, 0x02, 0x01]
|
||||
inject_rx:
|
||||
[
|
||||
0xFD, 0xFC, 0xFB, 0xFA,
|
||||
0x0C, 0x00,
|
||||
0x00, 0x01,
|
||||
0x00, 0x00,
|
||||
0x06, 0x00,
|
||||
0x76, 0x32, 0x2E, 0x30, 0x2E, 0x30,
|
||||
0x04, 0x03, 0x02, 0x01,
|
||||
]
|
||||
|
||||
# Catch-all response: match any command footer (04 03 02 01).
|
||||
# Returns a generic ACK with cmd=0xFF (CMD_ENABLE_CONF case in switch).
|
||||
# All commands get unblocked via cmd_reply_.ack = true.
|
||||
# Data fields stay zeroed (min_gate=0, max_gate=0, timeout=0, thresholds=0).
|
||||
#
|
||||
# Response layout:
|
||||
# [0-3] FD FC FB FA = header
|
||||
# [4-5] 04 00 = length 4
|
||||
# [6] FF = cmd (handled as CMD_ENABLE_CONF)
|
||||
# [7] 01 = status (ACK)
|
||||
# [8-9] 00 00 = error = 0
|
||||
# [10-13] 04 03 02 01 = footer
|
||||
- expect_tx: [0x04, 0x03, 0x02, 0x01]
|
||||
inject_rx:
|
||||
[
|
||||
0xFD, 0xFC, 0xFB, 0xFA,
|
||||
0x04, 0x00,
|
||||
0xFF, 0x01,
|
||||
0x00, 0x00,
|
||||
0x04, 0x03, 0x02, 0x01,
|
||||
]
|
||||
|
||||
injections:
|
||||
# Phase 1 (t=100ms): Valid LD2420 energy mode data frame - happy path
|
||||
# Buffer is clean (buffer_pos_=0). This frame should parse correctly.
|
||||
# Presence: 1 (target detected), Distance: 100cm, Gate energies: all 0
|
||||
#
|
||||
# Energy frame layout (45 bytes):
|
||||
# [0-3] F4 F3 F2 F1 = energy frame header
|
||||
# [4-5] 23 00 = length 35 (1+2+32)
|
||||
# [6] 01 = presence (1 = target)
|
||||
# [7-8] 64 00 = distance 100 (uint16_t LE)
|
||||
# [9-40] 00 00 x16 = 16 gate energies (uint16_t LE each)
|
||||
# [41-44] F8 F7 F6 F5 = energy frame footer
|
||||
- delay: 100ms
|
||||
inject_rx:
|
||||
[
|
||||
0xF4, 0xF3, 0xF2, 0xF1,
|
||||
0x23, 0x00,
|
||||
0x01,
|
||||
0x64, 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,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0xF8, 0xF7, 0xF6, 0xF5,
|
||||
]
|
||||
|
||||
# Phase 2 (t=300ms): Garbage bytes
|
||||
# LD2420's readline_ does NOT check frame headers at position 0 (unlike LD2412),
|
||||
# so these bytes accumulate in the buffer. buffer_pos_ goes from 0 to 7.
|
||||
- delay: 200ms
|
||||
inject_rx: [0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x11, 0x22]
|
||||
|
||||
# Phase 3 (t=400ms): Truncated energy frame WITH footer (13 bytes)
|
||||
# This tests PR #14458 bug #3: missing length validation in handle_energy_mode_.
|
||||
# The 7 garbage bytes from Phase 2 are still in the buffer (buffer_pos_=7).
|
||||
# These 13 bytes are appended at positions 7-19 (buffer_pos_=20).
|
||||
# Energy footer at positions 16-19 triggers handle_energy_mode_(buffer, 20).
|
||||
#
|
||||
# Pre-fix: handle_energy_mode_ reads 32 bytes of gate energy from buffer[9:40],
|
||||
# which is past the 20 actual bytes. Reads uninitialized data.
|
||||
# No "Energy frame too short" warning exists.
|
||||
# Post-fix: len=20 < 41 → logs "Energy frame too short: 20 bytes", returns early.
|
||||
#
|
||||
# Frame: header + length + presence + distance + footer (no gate data)
|
||||
- delay: 100ms
|
||||
inject_rx:
|
||||
[
|
||||
0xF4, 0xF3, 0xF2, 0xF1,
|
||||
0x23, 0x00,
|
||||
0x01,
|
||||
0x64, 0x00,
|
||||
0xF8, 0xF7, 0xF6, 0xF5,
|
||||
]
|
||||
|
||||
# Phase 4 (t=600ms): Overflow - inject 50 bytes of 0xFF (MAX_LINE_LENGTH=50)
|
||||
# After Phase 3, buffer_pos_=0 (reset after energy footer detection).
|
||||
# 49 bytes fill positions 0-48 (buffer_pos_=49), 50th byte triggers overflow.
|
||||
# Logs "Max command length exceeded; ignoring", buffer_pos_=0.
|
||||
- delay: 200ms
|
||||
inject_rx:
|
||||
[
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
]
|
||||
|
||||
# Phase 5 (t=1500ms): Valid frame after overflow - recovery test
|
||||
# Buffer was reset by overflow. This valid frame should parse correctly.
|
||||
# Presence: 1 (target), Distance: 50cm
|
||||
# Delay=900ms ensures >1000ms gap from Phase 1 for REFRESH_RATE_MS throttle.
|
||||
- delay: 900ms
|
||||
inject_rx:
|
||||
[
|
||||
0xF4, 0xF3, 0xF2, 0xF1,
|
||||
0x23, 0x00,
|
||||
0x01,
|
||||
0x32, 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,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0xF8, 0xF7, 0xF6, 0xF5,
|
||||
]
|
||||
|
||||
button:
|
||||
- platform: template
|
||||
name: "Start Scenario"
|
||||
on_press:
|
||||
- lambda: 'id(mock_uart).start_scenario();'
|
||||
|
||||
ld2420:
|
||||
id: ld2420_dev
|
||||
uart_id: mock_uart
|
||||
|
||||
sensor:
|
||||
- platform: ld2420
|
||||
ld2420_id: ld2420_dev
|
||||
moving_distance:
|
||||
name: "Moving Distance"
|
||||
filters:
|
||||
- timeout:
|
||||
timeout: 50ms
|
||||
value: last
|
||||
- throttle_with_priority: 50ms
|
||||
|
||||
binary_sensor:
|
||||
- platform: ld2420
|
||||
ld2420_id: ld2420_dev
|
||||
has_target:
|
||||
name: "Has Target"
|
||||
filters:
|
||||
- settle: 50ms
|
||||
@@ -0,0 +1,141 @@
|
||||
esphome:
|
||||
name: uart-mock-ld2420-simple-test
|
||||
|
||||
host:
|
||||
api:
|
||||
logger:
|
||||
level: VERBOSE
|
||||
|
||||
external_components:
|
||||
- source:
|
||||
type: local
|
||||
path: EXTERNAL_COMPONENT_PATH
|
||||
|
||||
# Dummy uart entry to satisfy ld2420's DEPENDENCIES = ["uart"]
|
||||
uart:
|
||||
baud_rate: 115200
|
||||
port: /dev/null
|
||||
|
||||
uart_mock:
|
||||
id: mock_uart
|
||||
baud_rate: 115200
|
||||
auto_start: false
|
||||
responses:
|
||||
# Catch-all response only (no version-specific response).
|
||||
# Without a version response, firmware_ver_ stays at default "v0.0.0".
|
||||
# get_firmware_int("v0.0.0") = 0 < 154 → simple mode (CMD_SYSTEM_MODE_SIMPLE).
|
||||
- expect_tx: [0x04, 0x03, 0x02, 0x01]
|
||||
inject_rx:
|
||||
[
|
||||
0xFD, 0xFC, 0xFB, 0xFA,
|
||||
0x04, 0x00,
|
||||
0xFF, 0x01,
|
||||
0x00, 0x00,
|
||||
0x04, 0x03, 0x02, 0x01,
|
||||
]
|
||||
|
||||
injections:
|
||||
# Phase 1 (t=100ms): Valid simple mode text frame - happy path
|
||||
# "ON Range 0100\r\n" → presence=true, distance=100
|
||||
# Simple mode frames end with \r\n (0x0D 0x0A), triggering handle_simple_mode_.
|
||||
- delay: 100ms
|
||||
inject_rx:
|
||||
[
|
||||
0x4F, 0x4E, 0x20, 0x52, 0x61, 0x6E, 0x67, 0x65, 0x20,
|
||||
0x30, 0x31, 0x30, 0x30,
|
||||
0x0D, 0x0A,
|
||||
]
|
||||
|
||||
# Phase 2 (t=300ms): Garbage bytes
|
||||
# LD2420's readline_ stores all bytes regardless of header. buffer_pos_ = 7.
|
||||
- delay: 200ms
|
||||
inject_rx: [0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x11, 0x22]
|
||||
|
||||
# Phase 3 (t=500ms): Overflow - inject 50 bytes of 0xFF (MAX_LINE_LENGTH=50)
|
||||
# buffer_pos_ starts at 7 (from Phase 2 garbage).
|
||||
# Positions 7-48 fill (42 bytes), byte 43 triggers overflow (buffer_pos_=49).
|
||||
# After overflow: buffer_pos_=0, remaining 7 bytes fill positions 0-6.
|
||||
# Final buffer_pos_ = 7.
|
||||
- delay: 200ms
|
||||
inject_rx:
|
||||
[
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
]
|
||||
|
||||
# Phase 4 (t=1400ms): Recovery after overflow
|
||||
# buffer_pos_ = 7 (from overflow remainder). These 15 bytes fill positions 7-21.
|
||||
# At position 21 (0x0A), \r\n detected → handle_simple_mode_(buffer, 22).
|
||||
# Parser skips 0xFF bytes at positions 0-6, finds "ON" at positions 7-8,
|
||||
# parses digits "0050" → distance=50.
|
||||
# Delay=900ms ensures >1000ms gap from Phase 1 for REFRESH_RATE_MS throttle.
|
||||
- delay: 900ms
|
||||
inject_rx:
|
||||
[
|
||||
0x4F, 0x4E, 0x20, 0x52, 0x61, 0x6E, 0x67, 0x65, 0x20,
|
||||
0x30, 0x30, 0x35, 0x30,
|
||||
0x0D, 0x0A,
|
||||
]
|
||||
|
||||
# Phase 5 (t=2500ms): 16-digit distance - tests PR #14458 bug #1
|
||||
# "ON Range 0000000000000000\r\n" has 16 digit characters.
|
||||
# handle_simple_mode_ outbuf is 16 bytes, can hold 15 digits (index 0-14).
|
||||
#
|
||||
# Pre-fix: At the 16th digit, index=15, (index < bufsize-1) is false.
|
||||
# The digit branch doesn't increment pos. The else branch is skipped.
|
||||
# pos stays at the 16th digit FOREVER → INFINITE LOOP.
|
||||
# The binary hangs, no more state updates, test times out.
|
||||
# Post-fix: pos always increments (moved outside digit branch).
|
||||
# 16th digit skipped, loop continues to \r\n. distance=0.
|
||||
- delay: 1100ms
|
||||
inject_rx:
|
||||
[
|
||||
0x4F, 0x4E, 0x20, 0x52, 0x61, 0x6E, 0x67, 0x65, 0x20,
|
||||
0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30,
|
||||
0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30,
|
||||
0x0D, 0x0A,
|
||||
]
|
||||
|
||||
# Phase 6 (t=3700ms): Post-bug-trigger recovery
|
||||
# If Phase 5 didn't hang, this frame should parse correctly.
|
||||
# "ON Range 0025\r\n" → distance=25
|
||||
# Delay=1200ms ensures >1000ms gap from Phase 5 for throttle.
|
||||
- delay: 1200ms
|
||||
inject_rx:
|
||||
[
|
||||
0x4F, 0x4E, 0x20, 0x52, 0x61, 0x6E, 0x67, 0x65, 0x20,
|
||||
0x30, 0x30, 0x32, 0x35,
|
||||
0x0D, 0x0A,
|
||||
]
|
||||
|
||||
button:
|
||||
- platform: template
|
||||
name: "Start Scenario"
|
||||
on_press:
|
||||
- lambda: 'id(mock_uart).start_scenario();'
|
||||
|
||||
ld2420:
|
||||
id: ld2420_dev
|
||||
uart_id: mock_uart
|
||||
|
||||
sensor:
|
||||
- platform: ld2420
|
||||
ld2420_id: ld2420_dev
|
||||
moving_distance:
|
||||
name: "Moving Distance"
|
||||
filters:
|
||||
- timeout:
|
||||
timeout: 50ms
|
||||
value: last
|
||||
- throttle_with_priority: 50ms
|
||||
|
||||
binary_sensor:
|
||||
- platform: ld2420
|
||||
ld2420_id: ld2420_dev
|
||||
has_target:
|
||||
name: "Has Target"
|
||||
filters:
|
||||
- settle: 50ms
|
||||
@@ -0,0 +1,221 @@
|
||||
esphome:
|
||||
name: uart-mock-ld2450-test
|
||||
|
||||
host:
|
||||
api:
|
||||
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:
|
||||
# Catch-all response: match any command footer (04 03 02 01).
|
||||
# Returns a generic ACK to unblock setup commands.
|
||||
#
|
||||
# Response layout:
|
||||
# [0-3] FD FC FB FA = header
|
||||
# [4-5] 04 00 = length 4
|
||||
# [6] FF = cmd (handled as CMD_ENABLE_CONF)
|
||||
# [7] 01 = status (ACK)
|
||||
# [8-9] 00 00 = error = 0
|
||||
# [10-13] 04 03 02 01 = footer
|
||||
- expect_tx: [0x04, 0x03, 0x02, 0x01]
|
||||
inject_rx:
|
||||
[
|
||||
0xFD, 0xFC, 0xFB, 0xFA,
|
||||
0x04, 0x00,
|
||||
0xFF, 0x01,
|
||||
0x00, 0x00,
|
||||
0x04, 0x03, 0x02, 0x01,
|
||||
]
|
||||
|
||||
injections:
|
||||
# Phase 1 (t=100ms): Valid LD2450 periodic data frame - happy path
|
||||
# The buffer is clean at this point, so this frame should parse correctly.
|
||||
#
|
||||
# Target 1: X=-500mm, Y=1000mm, Speed=-50mm/s (approaching), Res=320mm
|
||||
# X: magnitude=500 (0x01F4), negative → high=0x01, low=0xF4
|
||||
# Y: magnitude=1000 (0x03E8), positive → high=0x83, low=0xE8
|
||||
# Speed: raw=5, negative (approaching) → high=0x00, low=0x05, decoded=-50mm/s
|
||||
# Resolution: 320 → low=0x40, high=0x01
|
||||
# Distance: sqrt(500²+1000²) = sqrt(1250000) ≈ 1118mm
|
||||
#
|
||||
# Target 2: X=200mm, Y=500mm, Speed=0 (stationary), Res=100mm
|
||||
# X: magnitude=200 (0x00C8), positive → high=0x80, low=0xC8
|
||||
# Y: magnitude=500 (0x01F4), positive → high=0x81, low=0xF4
|
||||
# Speed: 0 → 0x00, 0x00
|
||||
# Resolution: 100 → low=0x64, high=0x00
|
||||
# Distance: sqrt(200²+500²) = sqrt(290000) ≈ 538mm
|
||||
#
|
||||
# Target 3: No target (all zeros)
|
||||
# Distance: 0 → sensors publish unknown/NaN
|
||||
#
|
||||
# Counts: target_count=2, moving_target_count=1, still_target_count=1
|
||||
#
|
||||
# Frame layout (30 bytes):
|
||||
# [0-3] AA FF 03 00 = periodic data header
|
||||
# [4-11] Target 1 (8 bytes): X_L X_H Y_L Y_H SPD_L SPD_H RES_L RES_H
|
||||
# [12-19] Target 2 (8 bytes)
|
||||
# [20-27] Target 3 (8 bytes)
|
||||
# [28-29] 55 CC = periodic data footer
|
||||
- delay: 100ms
|
||||
inject_rx:
|
||||
[
|
||||
0xAA, 0xFF, 0x03, 0x00,
|
||||
0xF4, 0x01, 0xE8, 0x83, 0x05, 0x00, 0x40, 0x01,
|
||||
0xC8, 0x80, 0xF4, 0x81, 0x00, 0x00, 0x64, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x55, 0xCC,
|
||||
]
|
||||
|
||||
# Phase 2 (t=300ms): Garbage bytes
|
||||
# LD2450's readline_ does NOT reject bytes at position 0 (unlike LD2412),
|
||||
# so these bytes accumulate in the buffer. buffer_pos_ goes from 0 to 7.
|
||||
- delay: 200ms
|
||||
inject_rx: [0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x11, 0x22]
|
||||
|
||||
# Phase 3 (t=400ms): Truncated frame (header + partial data, no footer)
|
||||
# More bytes accumulating in the buffer without a footer match.
|
||||
# After this, buffer_pos_ = 7 + 8 = 15.
|
||||
- delay: 100ms
|
||||
inject_rx: [0xAA, 0xFF, 0x03, 0x00, 0x01, 0x02, 0x03, 0x04]
|
||||
|
||||
# Phase 4 (t=600ms): Overflow - inject 75 bytes of 0xFF (MAX_LINE_LENGTH=45)
|
||||
# Buffer has 15 bytes from phases 2+3.
|
||||
# readline_() stores bytes while buffer_pos_ < 44. When buffer_pos_ == 44,
|
||||
# the next byte triggers overflow: logs warning, resets buffer_pos_ to 0,
|
||||
# and discards that byte.
|
||||
#
|
||||
# First overflow: 29 bytes fill positions 15-43 (buffer_pos_=44), byte 30
|
||||
# triggers overflow (discarded). Total consumed: 30 bytes.
|
||||
# Second overflow: 44 bytes fill positions 0-43 (buffer_pos_=44), byte 45
|
||||
# triggers overflow (discarded). Total consumed: 30+45 = 75 bytes.
|
||||
# After both overflows, buffer_pos_ = 0 (clean state for recovery frame).
|
||||
- delay: 200ms
|
||||
inject_rx:
|
||||
[
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
]
|
||||
|
||||
# Phase 5 (t=700ms): Valid frame after overflow - recovery test
|
||||
# Buffer was reset by overflow. This valid frame should parse correctly.
|
||||
#
|
||||
# Target 1: X=300mm, Y=400mm, Speed=30mm/s (moving away), Res=100mm
|
||||
# X: magnitude=300 (0x012C), positive → high=0x81, low=0x2C
|
||||
# Y: magnitude=400 (0x0190), positive → high=0x81, low=0x90
|
||||
# Speed: raw=3, positive (moving away) → high=0x80, low=0x03, decoded=30mm/s
|
||||
# Resolution: 100 → low=0x64, high=0x00
|
||||
# Distance: sqrt(300²+400²) = 500mm
|
||||
#
|
||||
# Target 2 & 3: No target (all zeros)
|
||||
# Counts: target_count=1, moving_target_count=1, still_target_count=0
|
||||
- delay: 100ms
|
||||
inject_rx:
|
||||
[
|
||||
0xAA, 0xFF, 0x03, 0x00,
|
||||
0x2C, 0x81, 0x90, 0x81, 0x03, 0x80, 0x64, 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
|
||||
|
||||
sensor:
|
||||
- platform: ld2450
|
||||
ld2450_id: ld2450_dev
|
||||
target_count:
|
||||
name: "Target Count"
|
||||
filters: &sensor_filters
|
||||
- timeout:
|
||||
timeout: 50ms
|
||||
value: last
|
||||
- throttle_with_priority: 50ms
|
||||
still_target_count:
|
||||
name: "Still Target Count"
|
||||
filters: *sensor_filters
|
||||
moving_target_count:
|
||||
name: "Moving Target Count"
|
||||
filters: *sensor_filters
|
||||
target_1:
|
||||
x:
|
||||
name: "Target 1 X"
|
||||
filters: *sensor_filters
|
||||
y:
|
||||
name: "Target 1 Y"
|
||||
filters: *sensor_filters
|
||||
speed:
|
||||
name: "Target 1 Speed"
|
||||
filters: *sensor_filters
|
||||
distance:
|
||||
name: "Target 1 Distance"
|
||||
filters: *sensor_filters
|
||||
resolution:
|
||||
name: "Target 1 Resolution"
|
||||
filters: *sensor_filters
|
||||
angle:
|
||||
name: "Target 1 Angle"
|
||||
filters: *sensor_filters
|
||||
target_2:
|
||||
x:
|
||||
name: "Target 2 X"
|
||||
filters: *sensor_filters
|
||||
y:
|
||||
name: "Target 2 Y"
|
||||
filters: *sensor_filters
|
||||
speed:
|
||||
name: "Target 2 Speed"
|
||||
filters: *sensor_filters
|
||||
distance:
|
||||
name: "Target 2 Distance"
|
||||
filters: *sensor_filters
|
||||
|
||||
binary_sensor:
|
||||
- platform: ld2450
|
||||
ld2450_id: ld2450_dev
|
||||
has_target:
|
||||
name: "Has Target"
|
||||
filters: &binary_sensor_filters
|
||||
- settle: 50ms
|
||||
has_moving_target:
|
||||
name: "Has Moving Target"
|
||||
filters: *binary_sensor_filters
|
||||
has_still_target:
|
||||
name: "Has Still Target"
|
||||
filters: *binary_sensor_filters
|
||||
|
||||
text_sensor:
|
||||
- platform: ld2450
|
||||
ld2450_id: ld2450_dev
|
||||
target_1:
|
||||
direction:
|
||||
name: "Target 1 Direction"
|
||||
|
||||
button:
|
||||
- platform: template
|
||||
name: "Start Scenario"
|
||||
id: start_scenario_btn
|
||||
on_press:
|
||||
- lambda: 'id(mock_uart).start_scenario();'
|
||||
@@ -0,0 +1,92 @@
|
||||
esphome:
|
||||
name: uart-mock-modbus-test
|
||||
|
||||
host:
|
||||
api:
|
||||
logger:
|
||||
level: VERBOSE
|
||||
|
||||
external_components:
|
||||
- source:
|
||||
type: local
|
||||
path: EXTERNAL_COMPONENT_PATH
|
||||
|
||||
# Dummy uart entry to satisfy modbus's DEPENDENCIES = ["uart"]
|
||||
# The actual UART bus used is the uart_mock component below
|
||||
uart:
|
||||
baud_rate: 115200
|
||||
port: /dev/null
|
||||
|
||||
uart_mock:
|
||||
- id: virtual_uart_dev
|
||||
baud_rate: 9600
|
||||
rx_full_threshold: 120
|
||||
rx_timeout: 2
|
||||
auto_start: false
|
||||
debug:
|
||||
responses:
|
||||
- expect_tx: [0x01, 0x03, 0x00, 0x03, 0x00, 0x01, 0x74, 0x0A] # Read holding register 3 on device 1 (basic_register)
|
||||
inject_rx: [0x01, 0x03, 0x02, 0x01, 0x03, 0xF9, 0xD5] # Return value 0x0103 (hex) = 259 (dec)
|
||||
- expect_tx: [0x01, 0x03, 0x00, 0x05, 0x00, 0x01, 0x94, 0x0B] # Read holding register 5 on device 1 (delayed_response)
|
||||
delay: 100ms # Shorter than modbus send_wait_time of 200ms, should succeed
|
||||
inject_rx: [0x01, 0x03, 0x02, 0x00, 0xFF, 0xF8, 0x04] # Return value 0x00FF (hex) = 255 (dec)
|
||||
- expect_tx: [0x02, 0x03, 0x00, 0x07, 0x00, 0x01, 0x35, 0xF8] # Read holding register 7 on device 2 (late_response)
|
||||
delay: 300ms # Longer than modbus send_wait_time of 200ms, should cause timeout
|
||||
inject_rx: [0x02, 0x03, 0x02, 0x00, 0xF0, 0xFC, 0x00] # Return value 0x00F0 (hex) = 240 (dec)
|
||||
- expect_tx: [0x03, 0x03, 0x00, 0x09, 0x00, 0x01, 0x55, 0xEA] # Read holding register 9 on device 3 (no_response)
|
||||
inject_rx: [] # No response, should cause timeout
|
||||
- expect_tx: [0x01, 0x03, 0x00, 0x0A, 0x00, 0x01, 0xA4, 0x08] # Read holding register A on device 1 (exception_response)
|
||||
inject_rx: [0x01, 0x83, 0x02, 0xC0, 0xF1] # Exception response with code 2 (illegal data address)
|
||||
|
||||
modbus:
|
||||
uart_id: virtual_uart_dev
|
||||
send_wait_time: 200ms
|
||||
turnaround_time: 10ms
|
||||
|
||||
modbus_controller:
|
||||
- address: 1
|
||||
id: modbus_controller_ok
|
||||
max_cmd_retries: 0
|
||||
update_interval: 1s
|
||||
- address: 2
|
||||
id: modbus_controller_slow
|
||||
max_cmd_retries: 0
|
||||
update_interval: 1s
|
||||
- address: 3
|
||||
id: modbus_controller_offline
|
||||
max_cmd_retries: 0
|
||||
update_interval: 1s
|
||||
|
||||
sensor:
|
||||
- platform: modbus_controller
|
||||
name: "basic_register"
|
||||
address: 0x03
|
||||
register_type: holding
|
||||
modbus_controller_id: modbus_controller_ok
|
||||
- platform: modbus_controller
|
||||
name: "delayed_response"
|
||||
address: 0x05
|
||||
register_type: holding
|
||||
modbus_controller_id: modbus_controller_ok
|
||||
- platform: modbus_controller
|
||||
name: "late_response"
|
||||
address: 0x07
|
||||
register_type: holding
|
||||
modbus_controller_id: modbus_controller_slow
|
||||
- platform: modbus_controller
|
||||
name: "no_response"
|
||||
address: 0x09
|
||||
register_type: holding
|
||||
modbus_controller_id: modbus_controller_offline
|
||||
- platform: modbus_controller
|
||||
name: "exception_response"
|
||||
address: 0x0A
|
||||
register_type: holding
|
||||
modbus_controller_id: modbus_controller_ok
|
||||
|
||||
button:
|
||||
- platform: template
|
||||
name: "Start Scenario"
|
||||
id: start_scenario_btn
|
||||
on_press:
|
||||
- lambda: 'id(virtual_uart_dev).start_scenario();'
|
||||
@@ -0,0 +1,64 @@
|
||||
esphome:
|
||||
name: uart-mock-modbus-no-thresh
|
||||
|
||||
host:
|
||||
api:
|
||||
logger:
|
||||
level: VERBOSE
|
||||
|
||||
external_components:
|
||||
- source:
|
||||
type: local
|
||||
path: EXTERNAL_COMPONENT_PATH
|
||||
|
||||
# Dummy uart entry to satisfy modbus's DEPENDENCIES = ["uart"]
|
||||
# The actual UART bus used is the uart_mock component below
|
||||
uart:
|
||||
baud_rate: 115200
|
||||
port: /dev/null
|
||||
|
||||
# Simulate a non-hardware UART (e.g., USB UART) by not setting rx_full_threshold.
|
||||
# This leaves it at the default sentinel value (0), triggering the 50ms fallback timeout.
|
||||
uart_mock:
|
||||
- id: virtual_uart_dev
|
||||
baud_rate: 9600
|
||||
auto_start: false
|
||||
debug:
|
||||
on_tx:
|
||||
- then:
|
||||
- if:
|
||||
condition: #Read 80 input registers on device 2, starting at address 0 (SDM meter request)
|
||||
lambda: "return data == std::vector<uint8_t>({0x02,0x04,0x00,0x00,0x00,0x50,0xF0,0x05});"
|
||||
then:
|
||||
- uart_mock.inject_rx: # First USB packet: SDM meter response part 1
|
||||
!lambda return {0x02,0x04,0xA0,0x43,0x73,0x19,0x9A,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,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,0x00,0x00,0x00,0x00,0x00,0x00,0x3F,0x80,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,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,0x00,0x00,0x00,0x00,0x00};
|
||||
- uart_mock.inject_rx: # Second USB packet: rest of response (staged with 40ms latency)
|
||||
delay: 40ms
|
||||
data: !lambda return{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
|
||||
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x42,0x6F,0xCC,0xCD,0x43,0x7C,0xB8,0x10,0x3D,0x38,0x51,0xEC,
|
||||
0x43,0x81,0x1B,0xE7,0x3B,0x03,0x12,0x6F,0x50,0x1B};
|
||||
|
||||
modbus:
|
||||
uart_id: virtual_uart_dev
|
||||
turnaround_time: 10ms
|
||||
|
||||
sensor:
|
||||
- platform: sdm_meter
|
||||
address: 2
|
||||
update_interval: 1s
|
||||
phase_a:
|
||||
voltage:
|
||||
name: sdm_voltage
|
||||
|
||||
button:
|
||||
- platform: template
|
||||
name: "Start Scenario"
|
||||
id: start_scenario_btn
|
||||
on_press:
|
||||
- lambda: 'id(virtual_uart_dev).start_scenario();'
|
||||
@@ -0,0 +1,64 @@
|
||||
esphome:
|
||||
name: uart-mock-modbus-test
|
||||
|
||||
host:
|
||||
api:
|
||||
logger:
|
||||
level: VERBOSE
|
||||
|
||||
external_components:
|
||||
- source:
|
||||
type: local
|
||||
path: EXTERNAL_COMPONENT_PATH
|
||||
|
||||
# Dummy uart entry to satisfy modbus's DEPENDENCIES = ["uart"]
|
||||
# The actual UART bus used is the uart_mock component below
|
||||
uart:
|
||||
baud_rate: 115200
|
||||
port: /dev/null
|
||||
|
||||
uart_mock:
|
||||
- id: virtual_uart_dev
|
||||
baud_rate: 9600
|
||||
rx_full_threshold: 120
|
||||
rx_timeout: 2
|
||||
auto_start: false
|
||||
debug:
|
||||
on_tx:
|
||||
- then:
|
||||
- if:
|
||||
condition: #Read 80 input registers on device 2, starting at address 0 (SDM meter request)
|
||||
lambda: "return data == std::vector<uint8_t>({0x02,0x04,0x00,0x00,0x00,0x50,0xF0,0x05});"
|
||||
then:
|
||||
- uart_mock.inject_rx: # Good response from SDM meter with CRC. Voltage is 243V
|
||||
!lambda return {0x02,0x04,0xA0,0x43,0x73,0x19,0x9A,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,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,0x00,0x00,0x00,0x00,0x00,0x00,0x3F,0x80,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,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,0x00,0x00,0x00,0x00,0x00};
|
||||
- delay: 120ms # Simulate some delay for UART buffer to fill before next part of response (1ms = 1 byte at 9600 baud)
|
||||
- uart_mock.inject_rx: # Rest of response (including CRC)
|
||||
!lambda return{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
|
||||
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x42,0x6F,0xCC,0xCD,0x43,0x7C,0xB8,0x10,0x3D,0x38,0x51,0xEC,
|
||||
0x43,0x81,0x1B,0xE7,0x3B,0x03,0x12,0x6F,0x50,0x1B};
|
||||
|
||||
modbus:
|
||||
uart_id: virtual_uart_dev
|
||||
turnaround_time: 10ms
|
||||
|
||||
sensor:
|
||||
- platform: sdm_meter
|
||||
address: 2
|
||||
update_interval: 1s
|
||||
phase_a:
|
||||
voltage:
|
||||
name: sdm_voltage
|
||||
|
||||
button:
|
||||
- platform: template
|
||||
name: "Start Scenario"
|
||||
id: start_scenario_btn
|
||||
on_press:
|
||||
- lambda: 'id(virtual_uart_dev).start_scenario();'
|
||||
@@ -3,10 +3,18 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Callable
|
||||
import logging
|
||||
from typing import TypeVar
|
||||
|
||||
from aioesphomeapi import ButtonInfo, EntityInfo, EntityState
|
||||
from aioesphomeapi import (
|
||||
BinarySensorState,
|
||||
ButtonInfo,
|
||||
EntityInfo,
|
||||
EntityState,
|
||||
SensorState,
|
||||
TextSensorState,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -234,3 +242,107 @@ class InitialStateHelper:
|
||||
asyncio.TimeoutError: If initial states aren't received within timeout
|
||||
"""
|
||||
await asyncio.wait_for(self._initial_states_received, timeout=timeout)
|
||||
|
||||
|
||||
class SensorStateCollector:
|
||||
"""Collects sensor, binary sensor, and text sensor state updates with wait helpers.
|
||||
|
||||
Usage:
|
||||
collector = SensorStateCollector(
|
||||
sensor_names=["moving_distance", "still_distance"],
|
||||
binary_sensor_names=["has_target"],
|
||||
text_sensor_names=["direction"],
|
||||
)
|
||||
# Use collector.on_state as the callback (or wrap it)
|
||||
client.subscribe_states(helper.on_state_wrapper(collector.on_state))
|
||||
|
||||
# Wait for all sensors to have at least one value
|
||||
await collector.wait_for_all(timeout=3.0)
|
||||
|
||||
# Access collected states
|
||||
assert collector.sensor_states["moving_distance"][0] == approx(100.0)
|
||||
assert collector.text_sensor_states["direction"][0] == "Approaching"
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
sensor_names: list[str],
|
||||
binary_sensor_names: list[str] | None = None,
|
||||
text_sensor_names: list[str] | None = None,
|
||||
entities: list[EntityInfo] | None = None,
|
||||
) -> None:
|
||||
self.sensor_states: dict[str, list[float]] = {name: [] for name in sensor_names}
|
||||
self.binary_states: dict[str, list[bool]] = {
|
||||
name: [] for name in (binary_sensor_names or [])
|
||||
}
|
||||
self.text_sensor_states: dict[str, list[str]] = {
|
||||
name: [] for name in (text_sensor_names or [])
|
||||
}
|
||||
self._key_to_sensor: dict[int, str] = {}
|
||||
self._waiters: list[tuple[Callable[[], bool], asyncio.Future[bool]]] = []
|
||||
|
||||
if entities is not None:
|
||||
self.build_key_mapping(entities)
|
||||
|
||||
def build_key_mapping(self, entities: list[EntityInfo]) -> None:
|
||||
"""Build key-to-name mapping from entities. Sorted by descending length."""
|
||||
all_names = (
|
||||
list(self.sensor_states.keys())
|
||||
+ list(self.binary_states.keys())
|
||||
+ list(self.text_sensor_states.keys())
|
||||
)
|
||||
all_names.sort(key=len, reverse=True)
|
||||
self._key_to_sensor = build_key_to_entity_mapping(entities, all_names)
|
||||
|
||||
def on_state(self, state: EntityState) -> None:
|
||||
"""Process a state update."""
|
||||
if isinstance(state, SensorState) and not state.missing_state:
|
||||
sensor_name = self._key_to_sensor.get(state.key)
|
||||
if sensor_name and sensor_name in self.sensor_states:
|
||||
self.sensor_states[sensor_name].append(state.state)
|
||||
self._check_waiters()
|
||||
elif isinstance(state, BinarySensorState):
|
||||
sensor_name = self._key_to_sensor.get(state.key)
|
||||
if sensor_name and sensor_name in self.binary_states:
|
||||
self.binary_states[sensor_name].append(state.state)
|
||||
self._check_waiters()
|
||||
elif isinstance(state, TextSensorState) and not state.missing_state:
|
||||
sensor_name = self._key_to_sensor.get(state.key)
|
||||
if sensor_name and sensor_name in self.text_sensor_states:
|
||||
self.text_sensor_states[sensor_name].append(state.state)
|
||||
self._check_waiters()
|
||||
|
||||
def _check_waiters(self) -> None:
|
||||
"""Check all pending waiters and resolve any whose condition is met."""
|
||||
for condition, future in self._waiters:
|
||||
if not future.done() and condition():
|
||||
future.set_result(True)
|
||||
|
||||
def _all_have_values(self) -> bool:
|
||||
"""Check if all sensor, binary sensor, and text sensor lists have at least one value."""
|
||||
return (
|
||||
all(len(v) >= 1 for v in self.sensor_states.values())
|
||||
and all(len(v) >= 1 for v in self.binary_states.values())
|
||||
and all(len(v) >= 1 for v in self.text_sensor_states.values())
|
||||
)
|
||||
|
||||
async def wait_for_all(self, timeout: float = 3.0) -> None:
|
||||
"""Wait until all sensors and binary sensors have at least one value."""
|
||||
if self._all_have_values():
|
||||
return
|
||||
future: asyncio.Future[bool] = asyncio.get_running_loop().create_future()
|
||||
self._waiters.append((self._all_have_values, future))
|
||||
await asyncio.wait_for(future, timeout=timeout)
|
||||
|
||||
def add_waiter(self, condition: Callable[[], bool]) -> asyncio.Future[bool]:
|
||||
"""Add a custom waiter that resolves when condition returns True.
|
||||
|
||||
Returns:
|
||||
A future that resolves when the condition is met.
|
||||
"""
|
||||
future: asyncio.Future[bool] = asyncio.get_running_loop().create_future()
|
||||
if condition():
|
||||
future.set_result(True)
|
||||
else:
|
||||
self._waiters.append((condition, future))
|
||||
return future
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
"""Integration test for micros_to_millis Euclidean decomposition."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import re
|
||||
|
||||
import pytest
|
||||
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_micros_to_millis(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Test that micros_to_millis matches reference uint64 division."""
|
||||
|
||||
all_passed = asyncio.Event()
|
||||
failures: list[str] = []
|
||||
|
||||
def on_log_line(line: str) -> None:
|
||||
clean_line = re.sub(r"\x1b\[[0-9;]*m", "", line)
|
||||
if "ALL_PASSED" in clean_line:
|
||||
all_passed.set()
|
||||
elif "FAILED" in clean_line and "[MTM" in clean_line:
|
||||
failures.append(clean_line)
|
||||
|
||||
async with (
|
||||
run_compiled(yaml_config, line_callback=on_log_line),
|
||||
api_client_connected() as client,
|
||||
):
|
||||
device_info = await client.device_info()
|
||||
assert device_info is not None
|
||||
assert device_info.name == "micros-to-millis-test"
|
||||
|
||||
try:
|
||||
await asyncio.wait_for(all_passed.wait(), timeout=2.0)
|
||||
except TimeoutError:
|
||||
if failures:
|
||||
pytest.fail(f"micros_to_millis failures: {failures}")
|
||||
pytest.fail("micros_to_millis test timed out")
|
||||
|
||||
assert not failures, f"micros_to_millis failures: {failures}"
|
||||
@@ -17,10 +17,10 @@ async def test_oversized_payload_plaintext(
|
||||
) -> None:
|
||||
"""Test that oversized payloads (>32768 bytes) from client cause disconnection without crashing."""
|
||||
process_exited = False
|
||||
helper_log_found = False
|
||||
helper_log_event = asyncio.Event()
|
||||
|
||||
def check_logs(line: str) -> None:
|
||||
nonlocal process_exited, helper_log_found
|
||||
nonlocal process_exited
|
||||
# Check for signs that the process exited/crashed
|
||||
if "Segmentation fault" in line or "core dumped" in line:
|
||||
process_exited = True
|
||||
@@ -30,7 +30,7 @@ async def test_oversized_payload_plaintext(
|
||||
and "Bad packet: message size" in line
|
||||
and "exceeds maximum" in line
|
||||
):
|
||||
helper_log_found = True
|
||||
helper_log_event.set()
|
||||
|
||||
async with run_compiled(yaml_config, line_callback=check_logs):
|
||||
async with api_client_connected_with_disconnect() as (client, disconnect_event):
|
||||
@@ -54,10 +54,13 @@ async def test_oversized_payload_plaintext(
|
||||
|
||||
# After disconnection, verify process didn't crash
|
||||
assert not process_exited, "ESPHome process should not crash"
|
||||
# Verify we saw the expected HELPER_LOG message
|
||||
assert helper_log_found, (
|
||||
"Expected to see HELPER_LOG about message size exceeding maximum"
|
||||
)
|
||||
# Wait for the expected log message (may arrive after disconnect event)
|
||||
try:
|
||||
await asyncio.wait_for(helper_log_event.wait(), timeout=2.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(
|
||||
"Expected to see HELPER_LOG about message size exceeding maximum"
|
||||
)
|
||||
|
||||
# Try to reconnect to verify the process is still running
|
||||
async with api_client_connected_with_disconnect() as (client2, _):
|
||||
@@ -77,10 +80,10 @@ async def test_oversized_protobuf_message_id_plaintext(
|
||||
This tests the message type limit - message IDs must fit in a uint16_t (0-65535).
|
||||
"""
|
||||
process_exited = False
|
||||
helper_log_found = False
|
||||
helper_log_event = asyncio.Event()
|
||||
|
||||
def check_logs(line: str) -> None:
|
||||
nonlocal process_exited, helper_log_found
|
||||
nonlocal process_exited
|
||||
# Check for signs that the process exited/crashed
|
||||
if "Segmentation fault" in line or "core dumped" in line:
|
||||
process_exited = True
|
||||
@@ -90,7 +93,7 @@ async def test_oversized_protobuf_message_id_plaintext(
|
||||
and "Bad packet: message type" in line
|
||||
and "exceeds maximum" in line
|
||||
):
|
||||
helper_log_found = True
|
||||
helper_log_event.set()
|
||||
|
||||
async with run_compiled(yaml_config, line_callback=check_logs):
|
||||
async with api_client_connected_with_disconnect() as (client, disconnect_event):
|
||||
@@ -114,10 +117,13 @@ async def test_oversized_protobuf_message_id_plaintext(
|
||||
|
||||
# After disconnection, verify process didn't crash
|
||||
assert not process_exited, "ESPHome process should not crash"
|
||||
# Verify we saw the expected HELPER_LOG message
|
||||
assert helper_log_found, (
|
||||
"Expected to see HELPER_LOG about message type exceeding maximum"
|
||||
)
|
||||
# Wait for the expected log message (may arrive after disconnect event)
|
||||
try:
|
||||
await asyncio.wait_for(helper_log_event.wait(), timeout=2.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(
|
||||
"Expected to see HELPER_LOG about message type exceeding maximum"
|
||||
)
|
||||
|
||||
# Try to reconnect to verify the process is still running
|
||||
async with api_client_connected_with_disconnect() as (client2, _):
|
||||
@@ -135,10 +141,10 @@ async def test_oversized_payload_noise(
|
||||
"""Test that oversized payloads from client cause disconnection without crashing with noise encryption."""
|
||||
noise_key = "N4Yle5YirwZhPiHHsdZLdOA73ndj/84veVaLhTvxCuU="
|
||||
process_exited = False
|
||||
helper_log_found = False
|
||||
helper_log_event = asyncio.Event()
|
||||
|
||||
def check_logs(line: str) -> None:
|
||||
nonlocal process_exited, helper_log_found
|
||||
nonlocal process_exited
|
||||
# Check for signs that the process exited/crashed
|
||||
if "Segmentation fault" in line or "core dumped" in line:
|
||||
process_exited = True
|
||||
@@ -149,7 +155,7 @@ async def test_oversized_payload_noise(
|
||||
and "Bad packet: message size" in line
|
||||
and "exceeds maximum" in line
|
||||
):
|
||||
helper_log_found = True
|
||||
helper_log_event.set()
|
||||
|
||||
async with run_compiled(yaml_config, line_callback=check_logs):
|
||||
async with api_client_connected_with_disconnect(noise_psk=noise_key) as (
|
||||
@@ -177,10 +183,13 @@ async def test_oversized_payload_noise(
|
||||
|
||||
# After disconnection, verify process didn't crash
|
||||
assert not process_exited, "ESPHome process should not crash"
|
||||
# Verify we saw the expected HELPER_LOG message
|
||||
assert helper_log_found, (
|
||||
"Expected to see HELPER_LOG about message size exceeding maximum"
|
||||
)
|
||||
# Wait for the expected log message (may arrive after disconnect event)
|
||||
try:
|
||||
await asyncio.wait_for(helper_log_event.wait(), timeout=2.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(
|
||||
"Expected to see HELPER_LOG about message size exceeding maximum"
|
||||
)
|
||||
|
||||
# Try to reconnect to verify the process is still running
|
||||
async with api_client_connected_with_disconnect(noise_psk=noise_key) as (
|
||||
@@ -274,10 +283,10 @@ async def test_noise_corrupt_encrypted_frame(
|
||||
"""
|
||||
noise_key = "N4Yle5YirwZhPiHHsdZLdOA73ndj/84veVaLhTvxCuU="
|
||||
process_exited = False
|
||||
cipherstate_failed = False
|
||||
cipherstate_event = asyncio.Event()
|
||||
|
||||
def check_logs(line: str) -> None:
|
||||
nonlocal process_exited, cipherstate_failed
|
||||
nonlocal process_exited
|
||||
# Check for signs that the process exited/crashed
|
||||
if "Segmentation fault" in line or "core dumped" in line:
|
||||
process_exited = True
|
||||
@@ -290,7 +299,7 @@ async def test_noise_corrupt_encrypted_frame(
|
||||
"[W][api.connection" in line
|
||||
and "Reading failed CIPHERSTATE_DECRYPT_FAILED" in line
|
||||
):
|
||||
cipherstate_failed = True
|
||||
cipherstate_event.set()
|
||||
|
||||
async with run_compiled(yaml_config, line_callback=check_logs):
|
||||
async with api_client_connected_with_disconnect(noise_psk=noise_key) as (
|
||||
@@ -326,10 +335,14 @@ async def test_noise_corrupt_encrypted_frame(
|
||||
assert not process_exited, (
|
||||
"ESPHome process should not crash on corrupt encrypted frames"
|
||||
)
|
||||
# Verify we saw the expected log message about decryption failure
|
||||
assert cipherstate_failed, (
|
||||
"Expected to see log about noise_cipherstate_decrypt failure or CIPHERSTATE_DECRYPT_FAILED"
|
||||
)
|
||||
# Wait for the expected log message (may arrive after disconnect event)
|
||||
try:
|
||||
await asyncio.wait_for(cipherstate_event.wait(), timeout=2.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(
|
||||
"Expected to see log about noise_cipherstate_decrypt failure"
|
||||
" or CIPHERSTATE_DECRYPT_FAILED"
|
||||
)
|
||||
|
||||
# Verify we can still reconnect after handling the corrupt frame
|
||||
async with api_client_connected_with_disconnect(noise_psk=noise_key) as (
|
||||
|
||||
@@ -0,0 +1,265 @@
|
||||
"""Integration test for LD2410 component with mock UART.
|
||||
|
||||
Tests:
|
||||
test_uart_mock_ld2410 (normal mode):
|
||||
1. Happy path - valid data frame publishes correct sensor values
|
||||
2. Garbage resilience - random bytes don't crash the component
|
||||
3. Truncated frame handling - partial frame doesn't corrupt state
|
||||
4. Buffer overflow recovery - overflow resets the parser
|
||||
5. Post-overflow parsing - next valid frame after overflow is parsed correctly
|
||||
6. TX logging - verifies LD2410 sends expected setup commands
|
||||
|
||||
test_uart_mock_ld2410_engineering (engineering mode):
|
||||
1. Engineering mode frames with per-gate energy data and light sensor
|
||||
2. Multi-byte still distance (291cm) using high byte > 0
|
||||
3. Out pin presence binary sensor
|
||||
4. Gate energy sensor values from real device captures
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
|
||||
from aioesphomeapi import ButtonInfo
|
||||
import pytest
|
||||
|
||||
from .state_utils import InitialStateHelper, SensorStateCollector, find_entity
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_uart_mock_ld2410(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Test LD2410 data parsing with happy path, garbage, overflow, and recovery."""
|
||||
# Replace external component path placeholder
|
||||
external_components_path = str(
|
||||
Path(__file__).parent / "fixtures" / "external_components"
|
||||
)
|
||||
yaml_config = yaml_config.replace(
|
||||
"EXTERNAL_COMPONENT_PATH", external_components_path
|
||||
)
|
||||
|
||||
loop = asyncio.get_running_loop()
|
||||
|
||||
# Track overflow warning in logs
|
||||
overflow_seen = loop.create_future()
|
||||
|
||||
# Track TX data logged by the mock for assertions
|
||||
tx_log_lines: list[str] = []
|
||||
|
||||
def line_callback(line: str) -> None:
|
||||
if "Max command length exceeded" in line and not overflow_seen.done():
|
||||
overflow_seen.set_result(True)
|
||||
# Capture all TX log lines from uart_mock
|
||||
if "uart_mock" in line and "TX " in line:
|
||||
tx_log_lines.append(line)
|
||||
|
||||
collector = SensorStateCollector(
|
||||
sensor_names=[
|
||||
"moving_distance",
|
||||
"still_distance",
|
||||
"moving_energy",
|
||||
"still_energy",
|
||||
"detection_distance",
|
||||
],
|
||||
binary_sensor_names=[
|
||||
"has_target",
|
||||
"has_moving_target",
|
||||
"has_still_target",
|
||||
],
|
||||
)
|
||||
|
||||
# Signal when we see recovery frame values
|
||||
recovery_received = collector.add_waiter(
|
||||
lambda: pytest.approx(50.0) in collector.sensor_states["moving_distance"]
|
||||
)
|
||||
|
||||
async with (
|
||||
run_compiled(yaml_config, line_callback=line_callback),
|
||||
api_client_connected() as client,
|
||||
):
|
||||
entities, _ = await client.list_entities_services()
|
||||
collector.build_key_mapping(entities)
|
||||
|
||||
# Set up initial state helper
|
||||
initial_state_helper = InitialStateHelper(entities)
|
||||
client.subscribe_states(
|
||||
initial_state_helper.on_state_wrapper(collector.on_state)
|
||||
)
|
||||
|
||||
try:
|
||||
await initial_state_helper.wait_for_initial_states()
|
||||
except TimeoutError:
|
||||
pytest.fail("Timeout waiting for initial states")
|
||||
|
||||
# Start the UART mock scenario now that we're subscribed
|
||||
start_btn = find_entity(entities, "start_scenario", ButtonInfo)
|
||||
assert start_btn is not None, "Start Scenario button not found"
|
||||
client.button_command(start_btn.key)
|
||||
|
||||
# Wait for Phase 1 - all sensors and binary sensors have at least one value
|
||||
try:
|
||||
await collector.wait_for_all(timeout=3.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(
|
||||
f"Timeout waiting for Phase 1 frame. Received:\n"
|
||||
f" sensor_states: {collector.sensor_states}\n"
|
||||
f" binary_states: {collector.binary_states}"
|
||||
)
|
||||
|
||||
# Phase 1 values: moving=100, still=120, energy=50/25, detect=300
|
||||
assert collector.sensor_states["moving_distance"][0] == pytest.approx(100.0)
|
||||
assert collector.sensor_states["still_distance"][0] == pytest.approx(120.0)
|
||||
assert collector.sensor_states["moving_energy"][0] == pytest.approx(50.0)
|
||||
assert collector.sensor_states["still_energy"][0] == pytest.approx(25.0)
|
||||
assert collector.sensor_states["detection_distance"][0] == pytest.approx(300.0)
|
||||
|
||||
# Wait for the recovery frame (Phase 5) to be parsed
|
||||
# This proves the component survived garbage + truncated + overflow
|
||||
try:
|
||||
await asyncio.wait_for(recovery_received, timeout=15.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(
|
||||
f"Timeout waiting for recovery frame. Received:\n"
|
||||
f" sensor_states: {collector.sensor_states}"
|
||||
)
|
||||
|
||||
# Verify overflow warning was logged
|
||||
assert overflow_seen.done(), (
|
||||
"Expected 'Max command length exceeded' warning in logs"
|
||||
)
|
||||
|
||||
# Verify LD2410 sent setup commands (TX logging)
|
||||
# LD2410 sends 7 commands during setup: FF (config on), A0 (version),
|
||||
# A5 (MAC), AB (distance res), AE (light), 61 (params), FE (config off)
|
||||
assert len(tx_log_lines) > 0, "Expected TX log lines from uart_mock"
|
||||
tx_data = " ".join(tx_log_lines)
|
||||
assert "FD:FC:FB:FA" in tx_data, (
|
||||
"Expected LD2410 command frame header FD:FC:FB:FA in TX log"
|
||||
)
|
||||
assert "04:03:02:01" in tx_data, (
|
||||
"Expected LD2410 command frame footer 04:03:02:01 in TX log"
|
||||
)
|
||||
|
||||
# Recovery frame: moving=50, still=75, energy=100/80, detect=127
|
||||
recovery_idx = next(
|
||||
i
|
||||
for i, v in enumerate(collector.sensor_states["moving_distance"])
|
||||
if v == pytest.approx(50.0)
|
||||
)
|
||||
assert collector.sensor_states["still_distance"][recovery_idx] == pytest.approx(
|
||||
75.0
|
||||
)
|
||||
assert collector.sensor_states["moving_energy"][recovery_idx] == pytest.approx(
|
||||
100.0
|
||||
)
|
||||
assert collector.sensor_states["still_energy"][recovery_idx] == pytest.approx(
|
||||
80.0
|
||||
)
|
||||
assert collector.sensor_states["detection_distance"][
|
||||
recovery_idx
|
||||
] == pytest.approx(127.0)
|
||||
|
||||
# Verify binary sensors detected targets (from Phase 1 frame)
|
||||
assert collector.binary_states["has_target"][0] is True
|
||||
assert collector.binary_states["has_moving_target"][0] is True
|
||||
assert collector.binary_states["has_still_target"][0] is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_uart_mock_ld2410_engineering(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Test LD2410 engineering mode with per-gate energy, light, and multi-byte distance."""
|
||||
external_components_path = str(
|
||||
Path(__file__).parent / "fixtures" / "external_components"
|
||||
)
|
||||
yaml_config = yaml_config.replace(
|
||||
"EXTERNAL_COMPONENT_PATH", external_components_path
|
||||
)
|
||||
|
||||
collector = SensorStateCollector(
|
||||
sensor_names=[
|
||||
"moving_distance",
|
||||
"still_distance",
|
||||
"moving_energy",
|
||||
"still_energy",
|
||||
"detection_distance",
|
||||
"light",
|
||||
"gate_0_move_energy",
|
||||
"gate_1_move_energy",
|
||||
"gate_2_move_energy",
|
||||
"gate_0_still_energy",
|
||||
"gate_1_still_energy",
|
||||
"gate_2_still_energy",
|
||||
],
|
||||
binary_sensor_names=[
|
||||
"has_target",
|
||||
"has_moving_target",
|
||||
"has_still_target",
|
||||
"out_pin_presence",
|
||||
],
|
||||
)
|
||||
|
||||
# Signal when we see Phase 3 frame values
|
||||
phase3_received = collector.add_waiter(
|
||||
lambda: pytest.approx(291.0) in collector.sensor_states["still_distance"]
|
||||
)
|
||||
|
||||
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)
|
||||
)
|
||||
|
||||
try:
|
||||
await initial_state_helper.wait_for_initial_states()
|
||||
except TimeoutError:
|
||||
pytest.fail("Timeout waiting for initial states")
|
||||
|
||||
# Start the UART mock scenario now that we're subscribed
|
||||
start_btn = find_entity(entities, "start_scenario", ButtonInfo)
|
||||
assert start_btn is not None, "Start Scenario button not found"
|
||||
client.button_command(start_btn.key)
|
||||
|
||||
# Wait for Phase 1 - all sensors and binary sensors have at least one value
|
||||
try:
|
||||
await collector.wait_for_all(timeout=3.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(
|
||||
f"Timeout waiting for Phase 1 frame. Received:\n"
|
||||
f" sensor_states: {collector.sensor_states}\n"
|
||||
f" binary_states: {collector.binary_states}"
|
||||
)
|
||||
|
||||
# Phase 1 values (engineering mode frame):
|
||||
# moving=30, energy=100, still=30, energy=100, detect=0
|
||||
assert collector.sensor_states["moving_distance"][0] == pytest.approx(30.0)
|
||||
assert collector.sensor_states["still_distance"][0] == pytest.approx(30.0)
|
||||
assert collector.sensor_states["gate_0_move_energy"][0] == pytest.approx(100.0)
|
||||
assert collector.sensor_states["gate_1_move_energy"][0] == pytest.approx(65.0)
|
||||
assert collector.sensor_states["light"][0] == pytest.approx(87.0)
|
||||
assert collector.binary_states["out_pin_presence"][0] is True
|
||||
|
||||
# Wait for Phase 3 frame (still_distance = 291cm, multi-byte)
|
||||
try:
|
||||
await asyncio.wait_for(phase3_received, timeout=15.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(
|
||||
f"Timeout waiting for Phase 3 frame. Received:\n"
|
||||
f" still_distance: {collector.sensor_states['still_distance']}"
|
||||
)
|
||||
|
||||
assert pytest.approx(291.0) in collector.sensor_states["still_distance"]
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user