Files
esphome/tests/integration/test_uart_mock_pylontech.py
T

126 lines
4.1 KiB
Python

"""Integration test for the pylontech component with mock UART.
Phases, all injected after the Start Scenario button press:
1. Older firmware header and two battery lines publish exact values
2. Garbage bytes are skipped
3. A truncated battery line publishes nothing and is counted in a warning
4. A line over the 199 character limit is dropped with a warning
5. Recovery: a newer firmware header, itself over the limit, still selects the
layout with the extra Id columns, so its battery lines parse correctly
"""
from __future__ import annotations
import asyncio
from aioesphomeapi import ButtonInfo
import pytest
from .log_utils import LineWaiter
from .state_utils import (
SensorStateCollector,
require_entity,
subscribe_after_initial_states,
)
from .types import APIClientConnectedFactory, RunCompiledFunction
from .wait_utils import wait_or_fail
SENSORS = [
"bat1_voltage",
"bat1_current",
"bat1_temperature",
"bat1_temperature_low",
"bat1_temperature_high",
"bat1_voltage_low",
"bat1_voltage_high",
"bat1_coulomb",
"bat1_mos_temperature",
"bat2_voltage",
"bat2_current",
"bat2_temperature_high",
"bat2_coulomb",
]
TEXT_SENSORS = ["bat1_base_state", "bat2_base_state"]
# Values per entity: phase 1 reply, then the phase 5 recovery reply
EXPECTED = {
"bat1_voltage": [50.548, 49.405],
"bat1_current": [8.91, 0.0],
"bat1_temperature": [25.0, 17.6],
"bat1_temperature_low": [24.2, 13.7],
"bat1_temperature_high": [25.0, 14.5],
"bat1_voltage_low": [3.368, 3.293],
"bat1_voltage_high": [3.371, 3.294],
"bat1_coulomb": [97.0, 60.0],
"bat1_mos_temperature": [22.7, 16.6],
"bat2_voltage": [46.012, 48.1],
"bat2_current": [-1.255, -2.5],
"bat2_temperature_high": [5.5, 14.1],
"bat2_coulomb": [4.0, 42.0],
}
EXPECTED_TEXT = {
"bat1_base_state": ["Charge", "Idle"],
"bat2_base_state": ["SysError", "Dischg"],
}
@pytest.mark.asyncio
async def test_uart_mock_pylontech(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> None:
"""Test pylontech parsing with happy path, garbage, overflow and recovery."""
waiter = LineWaiter()
collector = SensorStateCollector(
sensor_names=SENSORS, text_sensor_names=TEXT_SENSORS
)
recovery_received = collector.add_waiter(
lambda: (
all(len(collector.sensor_states[name]) >= 2 for name in SENSORS)
and all(
len(collector.text_sensor_states[name]) >= 2 for name in TEXT_SENSORS
)
)
)
async with (
run_compiled(yaml_config, line_callback=waiter.callback),
api_client_connected() as client,
):
entities, _ = await client.list_entities_services()
collector.build_key_mapping(entities)
await subscribe_after_initial_states(
client, entities, on_state=collector.on_state
)
start_btn = require_entity(entities, "start_scenario", ButtonInfo)
client.button_command(start_btn.key)
await wait_or_fail(
recovery_received,
5.0,
"Timeout waiting for the recovery reply",
sensor_states=collector.sensor_states,
text_states=collector.text_sensor_states,
)
# Let any stray publish from the bad phases arrive before the exact checks
await asyncio.sleep(0.2)
for name, values in EXPECTED.items():
assert collector.sensor_states[name] == pytest.approx(values), name
for name, values in EXPECTED_TEXT.items():
assert collector.text_sensor_states[name] == values, name
# Phase 4 and the phase 5 header both overflow
overflow_lines = [
line for line in waiter.lines if "Line over 199 chars dropped" in line
]
assert len(overflow_lines) == 2, overflow_lines
# The phase 3 cut line is counted and reported once the UART is idle
assert any("malformed lines" in line for line in waiter.lines), (
"no rejected-line summary logged"
)