mirror of
https://github.com/esphome/esphome.git
synced 2026-10-27 00:12:11 +00:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
738caf8831 | ||
|
|
1d8c775223 | ||
|
|
d7e8efa03f |
No files matched your search
@@ -74,6 +74,22 @@ uart_mock:
|
||||
inject_rx: [0xFD, 0xFC, 0xFB, 0xFA, 0x04, 0x00, 0x70, 0x01, 0x00, 0x00, 0x04, 0x03, 0x02, 0x01]
|
||||
- expect_tx: [0xFD, 0xFC, 0xFB, 0xFA, 0x02, 0x00, 0xFE, 0x00, 0x04, 0x03, 0x02, 0x01]
|
||||
inject_rx: [0xFD, 0xFC, 0xFB, 0xFA, 0x04, 0x00, 0xFE, 0x01, 0x00, 0x00, 0x04, 0x03, 0x02, 0x01]
|
||||
# Then the response speed Fast: the same values with speed 10, acknowledged
|
||||
- expect_tx:
|
||||
[
|
||||
0xFD, 0xFC, 0xFB, 0xFA, 0x26, 0x00, 0x70, 0x00, 0x05, 0x00, 0x0A, 0x00, 0x00, 0x00, 0x0A, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x0A, 0x00, 0x00, 0x00, 0x02, 0x00, 0x50, 0x00, 0x00, 0x00,
|
||||
0x0C, 0x00, 0x50, 0x00, 0x00, 0x00, 0x0B, 0x00, 0x0A, 0x00, 0x00, 0x00, 0x04, 0x03, 0x02, 0x01,
|
||||
]
|
||||
inject_rx: [0xFD, 0xFC, 0xFB, 0xFA, 0x04, 0x00, 0x70, 0x01, 0x00, 0x00, 0x04, 0x03, 0x02, 0x01]
|
||||
# Then no delay 20 s, rejected with ack 1: the component reads the module's values back
|
||||
- expect_tx:
|
||||
[
|
||||
0xFD, 0xFC, 0xFB, 0xFA, 0x26, 0x00, 0x70, 0x00, 0x05, 0x00, 0x0A, 0x00, 0x00, 0x00, 0x0A, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x14, 0x00, 0x00, 0x00, 0x02, 0x00, 0x50, 0x00, 0x00, 0x00,
|
||||
0x0C, 0x00, 0x50, 0x00, 0x00, 0x00, 0x0B, 0x00, 0x0A, 0x00, 0x00, 0x00, 0x04, 0x03, 0x02, 0x01,
|
||||
]
|
||||
inject_rx: [0xFD, 0xFC, 0xFB, 0xFA, 0x04, 0x00, 0x70, 0x01, 0x01, 0x00, 0x04, 0x03, 0x02, 0x01]
|
||||
injections:
|
||||
# Short frame, state 2 (target), distance 300 -> presence on
|
||||
- delay: 100ms
|
||||
@@ -137,6 +153,14 @@ number:
|
||||
name: "Max Distance"
|
||||
no_delay:
|
||||
name: "No Delay"
|
||||
status_reporting_frequency:
|
||||
name: "Status Reporting Frequency"
|
||||
|
||||
select:
|
||||
- platform: ld2410s
|
||||
ld2410s_id: ld2410s_dev
|
||||
response_speed:
|
||||
name: "Response Speed"
|
||||
|
||||
button:
|
||||
- platform: template
|
||||
|
||||
@@ -92,6 +92,19 @@ binary_sensor:
|
||||
has_target:
|
||||
name: "Has Target"
|
||||
|
||||
# Commanded before the module answered: the changes are ignored and nothing is published
|
||||
number:
|
||||
- platform: ld2410s
|
||||
ld2410s_id: ld2410s_dev
|
||||
max_distance:
|
||||
name: "Max Distance"
|
||||
|
||||
select:
|
||||
- platform: ld2410s
|
||||
ld2410s_id: ld2410s_dev
|
||||
response_speed:
|
||||
name: "Response Speed"
|
||||
|
||||
button:
|
||||
- platform: template
|
||||
name: "Start Scenario"
|
||||
|
||||
@@ -5,9 +5,15 @@ test_uart_mock_ld2410s:
|
||||
switch and config-mode end go out in order, each after the previous one is
|
||||
acknowledged, then the component logs "Setup done" and clears its warning;
|
||||
the version and parameter answers are parsed and logged
|
||||
1b. Setting the max distance number to 7 m writes all six parameters (gate 10
|
||||
1a. The numbers show the values read at init in their own units (gates of 0.7 m,
|
||||
seconds, tenths of a hertz) and the response speed select shows Normal
|
||||
1b. Setting the max distance number to 7.3 m writes all six parameters (gate 10
|
||||
plus the values read at init) inside a configuration-mode session, and the
|
||||
number reports 7 m
|
||||
number reports the 7 m the module got, not 7.3 m
|
||||
1c. Selecting the Fast response speed writes speed 10 with the same values and
|
||||
the select reports Fast
|
||||
1d. Setting no delay to 20 s reports 20 s at once; the module rejects the write,
|
||||
so the values are read back and every entity shows the module's own value again
|
||||
2. Presence from short frames (states 0/1 off, 2/3 on) and from a standard
|
||||
data frame
|
||||
3. Resync - a noise byte or a partial header directly before a short frame
|
||||
@@ -16,6 +22,8 @@ test_uart_mock_ld2410s:
|
||||
dropped without publishing, and the short frame after each still publishes
|
||||
|
||||
test_uart_mock_ld2410s_recovery (the mock never answers the init commands):
|
||||
0. A number or select changed before the module answered logs that the change
|
||||
is ignored and publishes nothing
|
||||
1. Config-mode start is resent after the acknowledgement timeout
|
||||
2. Once the retries run out, config-mode end is sent and the component warns
|
||||
that it is starting over
|
||||
@@ -36,9 +44,19 @@ test_uart_mock_ld2410s_link_lost:
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Callable
|
||||
import re
|
||||
|
||||
from aioesphomeapi import APIClient, ButtonInfo, EntityState, NumberInfo, NumberState
|
||||
from aioesphomeapi import (
|
||||
APIClient,
|
||||
ButtonInfo,
|
||||
EntityInfo,
|
||||
EntityState,
|
||||
NumberInfo,
|
||||
NumberState,
|
||||
SelectInfo,
|
||||
SelectState,
|
||||
)
|
||||
import pytest
|
||||
|
||||
from .log_utils import LineWaiter
|
||||
@@ -66,12 +84,20 @@ INIT_TX = [
|
||||
OUTPUT_MODE_TX,
|
||||
CONFIG_END_TX,
|
||||
]
|
||||
# All six parameters: max gate 10, min gate 0, no delay 10, both frequencies 80, speed 5
|
||||
PARAMETERS_WRITE_TX = (
|
||||
"FD:FC:FB:FA:26:00:70:00:05:00:0A:00:00:00:0A:00:00:00:00:00:06:00:0A:00:00:00:02:00:50:00:00:00:"
|
||||
"0C:00:50:00:00:00:0B:00:05:00:00:00:04:03:02:01"
|
||||
)
|
||||
WRITE_TX = [CONFIG_START_TX, PARAMETERS_WRITE_TX, CONFIG_END_TX]
|
||||
|
||||
|
||||
def _parameters_write_tx(no_delay: int, speed: int) -> str:
|
||||
"""The 0x0070 write with max gate 10, min gate 0 and both frequencies 80."""
|
||||
values = [(0x05, 10), (0x0A, 0), (0x06, no_delay), (0x02, 80), (0x0C, 80)]
|
||||
body = "".join(f"{key:02X}:00:{value:02X}:00:00:00:" for key, value in values)
|
||||
return f"FD:FC:FB:FA:26:00:70:00:{body}0B:00:{speed:02X}:00:00:00:04:03:02:01"
|
||||
|
||||
|
||||
WRITE_TX = [CONFIG_START_TX, _parameters_write_tx(10, 5), CONFIG_END_TX]
|
||||
# The same values with response speed 10 (Fast)
|
||||
SPEED_WRITE_TX = [CONFIG_START_TX, _parameters_write_tx(10, 10), CONFIG_END_TX]
|
||||
# No delay 20 s; the fixture rejects it
|
||||
REJECTED_WRITE_TX = [CONFIG_START_TX, _parameters_write_tx(20, 10), CONFIG_END_TX]
|
||||
# Logged once the version and parameter answers are parsed
|
||||
PARSED_ANSWERS = [
|
||||
"Firmware 1.2.3",
|
||||
@@ -117,13 +143,20 @@ def _tx_frame(line: str) -> str | None:
|
||||
return ANSI_ESCAPE.sub("", line.split("bytes: ", 1)[1]).strip()
|
||||
|
||||
|
||||
async def _start_scenario(client: APIClient, collector: SensorStateCollector) -> None:
|
||||
"""Subscribe the collector past the initial states, then start the mock scenario."""
|
||||
async def _start_scenario(
|
||||
client: APIClient,
|
||||
collector: SensorStateCollector,
|
||||
on_state: Callable[[EntityState], None] | None = None,
|
||||
) -> list[EntityInfo]:
|
||||
"""Subscribe past the initial states, then start the mock scenario; returns the entities."""
|
||||
entities, _ = await client.list_entities_services()
|
||||
collector.build_key_mapping(entities)
|
||||
await subscribe_after_initial_states(client, entities, on_state=collector.on_state)
|
||||
await subscribe_after_initial_states(
|
||||
client, entities, on_state=on_state or collector.on_state
|
||||
)
|
||||
start_btn = require_entity(entities, "start_scenario", ButtonInfo)
|
||||
client.button_command(start_btn.key)
|
||||
return entities
|
||||
|
||||
|
||||
async def _wait_for_presence(
|
||||
@@ -156,12 +189,15 @@ async def test_uart_mock_ld2410s(
|
||||
collector = SensorStateCollector(
|
||||
sensor_names=["distance"], binary_sensor_names=["has_target"]
|
||||
)
|
||||
number_states: list[float] = []
|
||||
number_states: dict[int, list[float]] = {}
|
||||
select_states: list[str] = []
|
||||
|
||||
def on_state(state: EntityState) -> None:
|
||||
collector.on_state(state)
|
||||
if isinstance(state, NumberState) and state.key == max_distance_key:
|
||||
number_states.append(state.state)
|
||||
if isinstance(state, NumberState):
|
||||
number_states.setdefault(state.key, []).append(state.state)
|
||||
if isinstance(state, SelectState):
|
||||
select_states.append(state.state)
|
||||
|
||||
all_presence = collector.add_waiter(
|
||||
lambda: len(collector.binary_states["has_target"]) >= len(EXPECTED_PRESENCE)
|
||||
@@ -191,16 +227,28 @@ async def test_uart_mock_ld2410s(
|
||||
collector.build_key_mapping(entities)
|
||||
max_distance_key = require_entity(entities, "max_distance", NumberInfo).key
|
||||
sub = await subscribe_after_initial_states(client, entities, on_state=on_state)
|
||||
# The values read from the module: gate 16 and 10 s
|
||||
# The values read from the module: gate 16, 10 s, 80 tenths of a hertz, speed 5
|
||||
no_delay_key = require_entity(entities, "no_delay", NumberInfo).key
|
||||
frequency_key = require_entity(
|
||||
entities, "status_reporting_frequency", NumberInfo
|
||||
).key
|
||||
speed_key = require_entity(entities, "response_speed", SelectInfo).key
|
||||
initial = sub.initial_states
|
||||
assert initial[max_distance_key].state == pytest.approx(11.2)
|
||||
assert initial[no_delay_key].state == pytest.approx(10)
|
||||
assert initial[frequency_key].state == pytest.approx(8.0)
|
||||
assert initial[speed_key].state == "Normal"
|
||||
|
||||
client.number_command(max_distance_key, 7.0)
|
||||
# 7.3 m is between gates 10 and 11: gate 10 is written and 7 m reported
|
||||
client.number_command(max_distance_key, 7.3)
|
||||
await waiter.wait_count(TX_LINE, 9, 5.0, "Parameter write not sent")
|
||||
assert tx_frames()[6:9] == WRITE_TX, tx_frames()
|
||||
assert number_states == [pytest.approx(7.0)]
|
||||
assert number_states[max_distance_key] == [pytest.approx(7.0)]
|
||||
|
||||
client.select_command(speed_key, "Fast")
|
||||
await waiter.wait_count(TX_LINE, 12, 5.0, "Response speed write not sent")
|
||||
assert tx_frames()[9:12] == SPEED_WRITE_TX, tx_frames()
|
||||
assert select_states == ["Fast"]
|
||||
|
||||
start_btn = require_entity(entities, "start_scenario", ButtonInfo)
|
||||
client.button_command(start_btn.key)
|
||||
@@ -218,7 +266,29 @@ async def test_uart_mock_ld2410s(
|
||||
# A target 300 cm away, then none: the distance follows the presence
|
||||
assert collector.sensor_states["distance"][:2] == [300, 0]
|
||||
# Data frames must not restart the init sequence
|
||||
assert len(tx_frames()) == 9, tx_frames()
|
||||
assert len(tx_frames()) == 12, tx_frames()
|
||||
|
||||
# A rejected write leaves configuration mode and reads the module's values back
|
||||
read_back = waiter.expect("Parameters: gates 0 to 16, no delay 10 s")
|
||||
client.number_command(no_delay_key, 20)
|
||||
await wait_or_fail(read_back, 5.0, "Values not read back", tx_frames=tx_frames)
|
||||
|
||||
async def republished() -> None:
|
||||
while (
|
||||
len(number_states.get(no_delay_key, ())) < 2
|
||||
or len(number_states.get(max_distance_key, ())) < 2
|
||||
or len(select_states) < 2
|
||||
):
|
||||
await asyncio.sleep(0.01)
|
||||
|
||||
await wait_or_fail(republished(), 5.0, "Read-back values not published")
|
||||
assert tx_frames()[12:15] == REJECTED_WRITE_TX, tx_frames()
|
||||
assert number_states[no_delay_key] == [pytest.approx(20), pytest.approx(10)]
|
||||
assert number_states[max_distance_key] == [
|
||||
pytest.approx(7.0),
|
||||
pytest.approx(11.2),
|
||||
]
|
||||
assert select_states == ["Fast", "Normal"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -234,10 +304,14 @@ async def test_uart_mock_ld2410s_recovery(
|
||||
gave_up = loop.create_future()
|
||||
oversized_dropped = loop.create_future()
|
||||
footer_mismatches: list[str] = []
|
||||
ignored_changes = 0
|
||||
|
||||
def line_callback(line: str) -> None:
|
||||
nonlocal ignored_changes
|
||||
if (frame := _tx_frame(line)) is not None:
|
||||
tx_frames.append((loop.time(), frame))
|
||||
if "Parameters not read yet, change ignored" in line:
|
||||
ignored_changes += 1
|
||||
if "No acknowledgement for command 00FF, resending" in line and not (
|
||||
resent.done()
|
||||
):
|
||||
@@ -267,12 +341,25 @@ async def test_uart_mock_ld2410s_recovery(
|
||||
>= len(EXPECTED_RECOVERY_PRESENCE)
|
||||
)
|
||||
)
|
||||
setting_states: list[EntityState] = []
|
||||
|
||||
def on_state(state: EntityState) -> None:
|
||||
collector.on_state(state)
|
||||
if isinstance(state, (NumberState, SelectState)):
|
||||
setting_states.append(state)
|
||||
|
||||
async with (
|
||||
run_compiled(yaml_config, line_callback=line_callback),
|
||||
api_client_connected() as client,
|
||||
):
|
||||
await _start_scenario(client, collector)
|
||||
entities = await _start_scenario(client, collector, on_state)
|
||||
# The module never answered, so both changes are refused and nothing is published
|
||||
client.number_command(
|
||||
require_entity(entities, "max_distance", NumberInfo).key, 7.0
|
||||
)
|
||||
client.select_command(
|
||||
require_entity(entities, "response_speed", SelectInfo).key, "Fast"
|
||||
)
|
||||
await _wait_for_presence(collector, all_presence)
|
||||
await wait_or_fail(
|
||||
asyncio.gather(resent, gave_up),
|
||||
@@ -286,6 +373,8 @@ async def test_uart_mock_ld2410s_recovery(
|
||||
# Recovery shows in the published states alone; the footer log below is
|
||||
# only a cross-check that needs the fixture's VERBOSE logger level
|
||||
assert collector.binary_states["has_target"] == EXPECTED_RECOVERY_PRESENCE
|
||||
assert ignored_changes == 2
|
||||
assert setting_states == []
|
||||
assert oversized_dropped.done(), "Oversized length field was not dropped"
|
||||
assert footer_mismatches == [TRUNCATED_FRAME_HEX, BAD_COMMAND_FRAME_HEX]
|
||||
|
||||
|
||||
Reference in new issue
Block a user