mirror of
https://github.com/esphome/esphome.git
synced 2026-10-21 13:34:12 +00:00
[ld2410] Wait for complete query parameter acknowledgements (#21145)
This commit is contained in:
@@ -549,6 +549,10 @@ bool LD2410Component::handle_ack_data_() {
|
||||
break;
|
||||
|
||||
case CMD_QUERY: { // Query parameters response
|
||||
// Config values can contain the footer bytes, so wait for the whole 38 byte frame
|
||||
if (this->buffer_pos_ < 38) {
|
||||
return false;
|
||||
}
|
||||
if (this->buffer_data_[10] != HEADER)
|
||||
return true; // value head=0xAA
|
||||
#ifdef USE_NUMBER
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
esphome:
|
||||
name: uart-mock-ld2410-query-ack
|
||||
|
||||
host:
|
||||
api:
|
||||
batch_delay: 0ms # Disable batching to receive all state updates
|
||||
logger:
|
||||
level: VERBOSE
|
||||
|
||||
external_components:
|
||||
- source:
|
||||
type: local
|
||||
path: EXTERNAL_COMPONENT_PATH
|
||||
|
||||
# Dummy uart entry to satisfy ld2410's DEPENDENCIES = ["uart"]
|
||||
# The actual UART bus used is the uart_mock component below
|
||||
uart:
|
||||
baud_rate: 115200
|
||||
port: /dev/null
|
||||
|
||||
# Synthetic query parameters ACKs (command 0x61), built from the layout the component parses.
|
||||
# They are not device captures. Layout (38 bytes):
|
||||
# [0-3] header, [4-5] length 28, [6-7] 61 01, [8-9] 00 00, [10] AA, [11] gate count,
|
||||
# [12] max move gate, [13] max still gate, [14-22] move thresholds, [23-31] still thresholds,
|
||||
# [32-33] timeout (low, high), [34-37] footer
|
||||
uart_mock:
|
||||
id: mock_uart
|
||||
baud_rate: 256000
|
||||
auto_start: false
|
||||
injections:
|
||||
# Ordinary normal mode data frame, as in uart_mock_ld2410.yaml Phase 1
|
||||
# Moving 100cm energy 50, still 120cm energy 25, detection 300cm
|
||||
- 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,
|
||||
]
|
||||
|
||||
# Seed: a whole ACK, so every byte the next ACK is read from holds a known value
|
||||
# Max gates 8/8, move 50 50 40 30 20 15 15 15 15, still 0 0 40 40 40 40 15 15 15, timeout 5
|
||||
- delay: 100ms
|
||||
inject_rx:
|
||||
[
|
||||
0xFD, 0xFC, 0xFB, 0xFA, 0x1C, 0x00, 0x61, 0x01, 0x00, 0x00,
|
||||
0xAA, 0x08, 0x08, 0x08,
|
||||
0x32, 0x32, 0x28, 0x1E, 0x14, 0x0F, 0x0F, 0x0F, 0x0F,
|
||||
0x00, 0x00, 0x28, 0x28, 0x28, 0x28, 0x0F, 0x0F, 0x0F,
|
||||
0x05, 0x00,
|
||||
0x04, 0x03, 0x02, 0x01,
|
||||
]
|
||||
|
||||
# Move thresholds of gates 0 to 3 are the footer bytes 04 03 02 01
|
||||
# Max gates 6/5, move 4 3 2 1 60 45 46 47 48, still 20 to 28, timeout 300
|
||||
- delay: 100ms
|
||||
inject_rx:
|
||||
[
|
||||
0xFD, 0xFC, 0xFB, 0xFA, 0x1C, 0x00, 0x61, 0x01, 0x00, 0x00,
|
||||
0xAA, 0x08, 0x06, 0x05,
|
||||
0x04, 0x03, 0x02, 0x01, 0x3C, 0x2D, 0x2E, 0x2F, 0x30,
|
||||
0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C,
|
||||
0x2C, 0x01,
|
||||
0x04, 0x03, 0x02, 0x01,
|
||||
]
|
||||
|
||||
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"
|
||||
|
||||
number:
|
||||
- platform: ld2410
|
||||
ld2410_id: ld2410_dev
|
||||
timeout:
|
||||
name: "Timeout"
|
||||
max_move_distance_gate:
|
||||
name: "Max Move Distance Gate"
|
||||
max_still_distance_gate:
|
||||
name: "Max Still Distance Gate"
|
||||
g0:
|
||||
move_threshold:
|
||||
name: "G0 Move Threshold"
|
||||
still_threshold:
|
||||
name: "G0 Still Threshold"
|
||||
g4:
|
||||
move_threshold:
|
||||
name: "G4 Move Threshold"
|
||||
still_threshold:
|
||||
name: "G4 Still Threshold"
|
||||
g8:
|
||||
move_threshold:
|
||||
name: "G8 Move Threshold"
|
||||
still_threshold:
|
||||
name: "G8 Still Threshold"
|
||||
|
||||
button:
|
||||
- platform: template
|
||||
name: "Start Scenario"
|
||||
id: start_scenario_btn
|
||||
on_press:
|
||||
- lambda: 'id(mock_uart).start_scenario();'
|
||||
@@ -0,0 +1,165 @@
|
||||
"""Integration test for the LD2410 query parameters ACK with mock UART.
|
||||
|
||||
An ordinary data frame comes first, then two query ACKs. Move thresholds 4, 3, 2, 1
|
||||
put the command footer inside the ACK values. Every value must come from the whole
|
||||
38 byte frame, not from bytes left by the previous frame.
|
||||
The ACKs are synthetic, built from the layout the component parses, not device captures.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
|
||||
from aioesphomeapi import (
|
||||
ButtonInfo,
|
||||
EntityState,
|
||||
NumberInfo,
|
||||
NumberState,
|
||||
SensorInfo,
|
||||
SensorState,
|
||||
)
|
||||
import pytest
|
||||
|
||||
from .state_utils import InitialStateHelper, build_key_to_entity_mapping, find_entity
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
|
||||
# Number object_id: (value from the seed ACK, value from the ACK under test)
|
||||
EXPECTED: dict[str, tuple[float, float]] = {
|
||||
"max_move_distance_gate": (8.0, 6.0),
|
||||
"max_still_distance_gate": (8.0, 5.0),
|
||||
"timeout": (5.0, 300.0),
|
||||
"g0_move_threshold": (50.0, 4.0),
|
||||
"g0_still_threshold": (0.0, 20.0),
|
||||
"g4_move_threshold": (20.0, 60.0),
|
||||
"g4_still_threshold": (40.0, 24.0),
|
||||
"g8_move_threshold": (15.0, 48.0),
|
||||
"g8_still_threshold": (15.0, 28.0),
|
||||
}
|
||||
|
||||
# Sensor object_id: value from the data frame
|
||||
SENSOR_EXPECTED: dict[str, float] = {
|
||||
"moving_distance": 100.0,
|
||||
"still_distance": 120.0,
|
||||
"moving_energy": 50.0,
|
||||
"still_energy": 25.0,
|
||||
"detection_distance": 300.0,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_uart_mock_ld2410_query_ack(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Test a data frame, then a query ACK with the footer bytes in its values."""
|
||||
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()
|
||||
ack_errors: list[str] = []
|
||||
waited_for_frame_end = loop.create_future()
|
||||
|
||||
def line_callback(line: str) -> None:
|
||||
if "[ld2410" not in line:
|
||||
return
|
||||
if "Invalid " in line:
|
||||
ack_errors.append(line)
|
||||
if "Ack Data incomplete" in line and not waited_for_frame_end.done():
|
||||
waited_for_frame_end.set_result(True)
|
||||
|
||||
number_states: dict[str, list[float]] = {name: [] for name in EXPECTED}
|
||||
sensor_states: dict[str, list[float]] = {name: [] for name in SENSOR_EXPECTED}
|
||||
key_to_name: dict[int, str] = {}
|
||||
sensor_key_to_name: dict[int, str] = {}
|
||||
data_received = loop.create_future()
|
||||
final_received = loop.create_future()
|
||||
|
||||
def on_state(state: EntityState) -> None:
|
||||
if isinstance(state, SensorState) and not state.missing_state:
|
||||
if (name := sensor_key_to_name.get(state.key)) is None:
|
||||
return
|
||||
sensor_states[name].append(state.state)
|
||||
if not data_received.done() and all(sensor_states.values()):
|
||||
data_received.set_result(True)
|
||||
return
|
||||
if not isinstance(state, NumberState) or state.missing_state:
|
||||
return
|
||||
if (name := key_to_name.get(state.key)) is None:
|
||||
return
|
||||
number_states[name].append(state.state)
|
||||
if not final_received.done() and all(
|
||||
values and values[-1] == pytest.approx(EXPECTED[n][1])
|
||||
for n, values in number_states.items()
|
||||
):
|
||||
final_received.set_result(True)
|
||||
|
||||
async with (
|
||||
run_compiled(yaml_config, line_callback=line_callback),
|
||||
api_client_connected() as client,
|
||||
):
|
||||
entities, _ = await client.list_entities_services()
|
||||
# Map each kind separately; sensor names are substrings of number names
|
||||
key_to_name.update(
|
||||
build_key_to_entity_mapping(
|
||||
[e for e in entities if isinstance(e, NumberInfo)], list(EXPECTED)
|
||||
)
|
||||
)
|
||||
sensor_key_to_name.update(
|
||||
build_key_to_entity_mapping(
|
||||
[e for e in entities if isinstance(e, SensorInfo)],
|
||||
list(SENSOR_EXPECTED),
|
||||
)
|
||||
)
|
||||
assert set(key_to_name.values()) == set(EXPECTED), (
|
||||
f"Number entities missing, found: {sorted(key_to_name.values())}"
|
||||
)
|
||||
assert set(sensor_key_to_name.values()) == set(SENSOR_EXPECTED), (
|
||||
f"Sensor entities missing, found: {sorted(sensor_key_to_name.values())}"
|
||||
)
|
||||
|
||||
initial_state_helper = InitialStateHelper(entities)
|
||||
client.subscribe_states(initial_state_helper.on_state_wrapper(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)
|
||||
|
||||
try:
|
||||
await asyncio.wait_for(data_received, timeout=5.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(
|
||||
f"Timeout waiting for the data frame values. Received:\n"
|
||||
f" sensor_states: {sensor_states}"
|
||||
)
|
||||
|
||||
try:
|
||||
await asyncio.wait_for(final_received, timeout=5.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(
|
||||
f"Timeout waiting for the query ACK values. Received:\n"
|
||||
f" number_states: {number_states}\n"
|
||||
f" ack_errors: {ack_errors}"
|
||||
)
|
||||
|
||||
# One data frame, so each sensor is published once
|
||||
for name, value in SENSOR_EXPECTED.items():
|
||||
assert sensor_states[name] == pytest.approx([value]), name
|
||||
# Each number is published once per ACK: the seed value, then the value under test
|
||||
for name, (seed, value) in EXPECTED.items():
|
||||
assert number_states[name] == pytest.approx([seed, value]), name
|
||||
assert waited_for_frame_end.done(), (
|
||||
"Expected the parser to wait past the footer bytes in the values"
|
||||
)
|
||||
assert not ack_errors, f"Unexpected ACK errors: {ack_errors}"
|
||||
Reference in New Issue
Block a user