mirror of
https://github.com/esphome/esphome.git
synced 2026-10-07 03:16:37 +00:00
[ld2420] Address fourth review round
Sync new_config with current_config on the give-up path and gate the config writing actions on an explicit config read complete flag so a partial read can never be written to the module's NVM, raise the retry listen window above the module boot silence, treat replies shorter than the expected data length as silence, apply the parser mode only after the mode write is acknowledged, propagate write errors in the apply and factory reset actions before adopting the new config or clearing the warning, log drain failures and unbuildable startup frames, watch the runtime marked as failed log string in the tests, give the first enable command in the retry fixture genuine silence, and alternate the warm restart stream values so state deduplication cannot starve a late subscriber.
This commit is contained in:
@@ -42,9 +42,15 @@ uart_mock:
|
||||
0xF8, 0xF7, 0xF6, 0xF5,
|
||||
]
|
||||
|
||||
# The config mode enable command is answered from the on_tx hook below so
|
||||
# that the first attempt can be ignored; it must not have a responder here.
|
||||
# The config mode enable command is matched by an empty responder so the
|
||||
# catch-all cannot answer it (responders match on the TX suffix and every
|
||||
# command ends with the frame footer); the on_tx hook below acks it from
|
||||
# the second attempt on, so the first attempt is genuine silence.
|
||||
responses:
|
||||
- expect_tx:
|
||||
[0xFD, 0xFC, 0xFB, 0xFA, 0x04, 0x00, 0xFF, 0x00, 0x02, 0x00, 0x04, 0x03, 0x02, 0x01]
|
||||
inject_rx: []
|
||||
|
||||
# Version response: returns "v2.0.0" → 200 >= 154 → energy mode
|
||||
- expect_tx:
|
||||
[0xFD, 0xFC, 0xFB, 0xFA, 0x02, 0x00, 0x00, 0x00, 0x04, 0x03, 0x02, 0x01]
|
||||
|
||||
@@ -26,7 +26,12 @@ uart_mock:
|
||||
baud_rate: 115200
|
||||
auto_start: true
|
||||
|
||||
# Module streams a valid energy frame (presence=1, distance=100) continuously
|
||||
# Module streams valid energy frames continuously. Two alternating frames
|
||||
# are used (presence=1/distance=100 and presence=0/distance=75) so states
|
||||
# keep changing: with a constant frame the API deduplicates the repeated
|
||||
# identical states, and a client that subscribes after the first publish
|
||||
# would swallow the only transition as the initial state and never see an
|
||||
# update.
|
||||
periodic_rx:
|
||||
- interval: 250ms
|
||||
data:
|
||||
@@ -41,6 +46,19 @@ uart_mock:
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0xF8, 0xF7, 0xF6, 0xF5,
|
||||
]
|
||||
- interval: 1050ms
|
||||
data:
|
||||
[
|
||||
0xF4, 0xF3, 0xF2, 0xF1,
|
||||
0x23, 0x00,
|
||||
0x00,
|
||||
0x4B, 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,
|
||||
]
|
||||
|
||||
responses:
|
||||
# Version response: returns "v2.0.0" → 200 >= 154 → energy mode
|
||||
|
||||
@@ -287,6 +287,7 @@ async def _run_listen_first_test(
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
*,
|
||||
post_setup_distance: float | None = None,
|
||||
strict_first: bool = True,
|
||||
) -> None:
|
||||
"""Shared body for the listen-first startup tests.
|
||||
|
||||
@@ -294,7 +295,9 @@ async def _run_listen_first_test(
|
||||
(real hardware locks up until power cycled if it does), that the setup
|
||||
handshake completes, and that sensor data publishes. When
|
||||
post_setup_distance is given, additionally waits for that value to prove
|
||||
streaming still works after the handshake.
|
||||
streaming still works after the handshake. strict_first asserts on the
|
||||
first collected state; pass False for fixtures whose stream alternates
|
||||
values, where the first collected state depends on subscribe timing.
|
||||
"""
|
||||
loop = asyncio.get_running_loop()
|
||||
|
||||
@@ -314,6 +317,7 @@ async def _run_listen_first_test(
|
||||
setup_complete.set_result(True)
|
||||
if (
|
||||
"marked FAILED" in line
|
||||
or "was marked as failed" in line
|
||||
or "Communication failed" in line
|
||||
or "No data received from the module" in line
|
||||
):
|
||||
@@ -358,8 +362,12 @@ async def _run_listen_first_test(
|
||||
),
|
||||
)
|
||||
|
||||
assert collector.sensor_states["moving_distance"][0] == pytest.approx(100.0)
|
||||
assert collector.binary_states["has_target"][0] is True
|
||||
if strict_first:
|
||||
assert collector.sensor_states["moving_distance"][0] == pytest.approx(100.0)
|
||||
assert collector.binary_states["has_target"][0] is True
|
||||
else:
|
||||
assert pytest.approx(100.0) in collector.sensor_states["moving_distance"]
|
||||
assert True in collector.binary_states["has_target"]
|
||||
|
||||
if post_setup_received is not None:
|
||||
await _wait_or_fail(
|
||||
@@ -389,7 +397,9 @@ async def test_uart_mock_ld2420_warm_restart(
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Module streams from boot; component must listen first, then set up."""
|
||||
await _run_listen_first_test(yaml_config, run_compiled, api_client_connected)
|
||||
await _run_listen_first_test(
|
||||
yaml_config, run_compiled, api_client_connected, strict_first=False
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -414,7 +424,12 @@ async def test_uart_mock_ld2420_cmd_retry(
|
||||
watcher = _LogWatcher()
|
||||
resend_seen = watcher.watch("No reply to startup command")
|
||||
setup_complete = watcher.watch(SETUP_COMPLETE_LOG)
|
||||
watcher.collect("marked FAILED", "Communication failed", "Module setup attempt")
|
||||
watcher.collect(
|
||||
"marked FAILED",
|
||||
"was marked as failed",
|
||||
"Communication failed",
|
||||
"Module setup attempt",
|
||||
)
|
||||
|
||||
collector = SensorStateCollector(
|
||||
sensor_names=["moving_distance"],
|
||||
@@ -472,7 +487,7 @@ async def test_uart_mock_ld2420_give_up(
|
||||
parser_alive_after_give_up = watcher.watch(
|
||||
"Max command length exceeded", after=give_up_seen
|
||||
)
|
||||
watcher.collect("marked FAILED")
|
||||
watcher.collect("marked FAILED", "was marked as failed")
|
||||
|
||||
collector = SensorStateCollector(
|
||||
sensor_names=["moving_distance"],
|
||||
@@ -535,7 +550,7 @@ async def test_uart_mock_ld2420_restart_button(
|
||||
after=restart_seen,
|
||||
until=module_frame_after_restart,
|
||||
)
|
||||
watcher.collect("marked FAILED", "Communication failed")
|
||||
watcher.collect("marked FAILED", "was marked as failed", "Communication failed")
|
||||
|
||||
async with (
|
||||
run_compiled(yaml_config, line_callback=watcher),
|
||||
|
||||
Reference in New Issue
Block a user