|
| 1 | +"""Tests for re-sending states when Home Assistant comes back online.""" |
| 2 | + |
| 3 | +from types import SimpleNamespace |
| 4 | +from typing import Any |
| 5 | +from unittest.mock import AsyncMock, MagicMock, call |
| 6 | + |
| 7 | +import pytest |
| 8 | + |
| 9 | +from ha_addon_sunsynk_multi import a_inverter |
| 10 | +from ha_addon_sunsynk_multi.a_inverter import AInverter, republish_all_states |
| 11 | +from ha_addon_sunsynk_multi.a_sensor import MQTT, ASensor |
| 12 | +from ha_addon_sunsynk_multi.sensor_options import SensorOption |
| 13 | +from sunsynk.sensors import Sensor |
| 14 | +from sunsynk.sunsynk import ValType |
| 15 | + |
| 16 | +from .conftest import NOSCHEDULE, ist_factory |
| 17 | + |
| 18 | + |
| 19 | +def _asensor( |
| 20 | + name: str, value: ValType, *, retain: bool = False, entity: bool = True |
| 21 | +) -> ASensor: |
| 22 | + """Return an ASensor whose last published value is `value`.""" |
| 23 | + ent = None |
| 24 | + if entity: |
| 25 | + ent = MagicMock() |
| 26 | + ent.send_state = AsyncMock() |
| 27 | + return ASensor( |
| 28 | + opt=SensorOption( |
| 29 | + sensor=Sensor(1, name, "W"), schedule=NOSCHEDULE, visible=True |
| 30 | + ), |
| 31 | + entity=ent, |
| 32 | + retain=retain, |
| 33 | + _last=value, |
| 34 | + ) |
| 35 | + |
| 36 | + |
| 37 | +def _running_ist() -> AInverter: |
| 38 | + ist = ist_factory() |
| 39 | + ist.lifecycle = "running" |
| 40 | + ist.ss = { |
| 41 | + "power": _asensor("power", 100), |
| 42 | + "energy": _asensor("energy", 12.3), |
| 43 | + "never": _asensor("never", None), |
| 44 | + "rw": _asensor("rw", 50, retain=True), |
| 45 | + "hidden": _asensor("hidden", 7, entity=False), |
| 46 | + } |
| 47 | + return ist |
| 48 | + |
| 49 | + |
| 50 | +async def test_republish_states_sends_last_published_non_retained() -> None: |
| 51 | + """Only non-retained sensors with an entity and a published value are re-sent.""" |
| 52 | + ist = _running_ist() |
| 53 | + |
| 54 | + assert await ist.republish_states() == 2 |
| 55 | + |
| 56 | + ist.ss["power"].entity.send_state.assert_awaited_once_with(MQTT, 100) # type: ignore[union-attr] |
| 57 | + ist.ss["energy"].entity.send_state.assert_awaited_once_with(MQTT, 12.3) # type: ignore[union-attr] |
| 58 | + ist.ss["never"].entity.send_state.assert_not_awaited() # type: ignore[union-attr] |
| 59 | + ist.ss["rw"].entity.send_state.assert_not_awaited() # type: ignore[union-attr] |
| 60 | + # never written by a republish |
| 61 | + assert ist.ss["power"].value == 100 |
| 62 | + |
| 63 | + |
| 64 | +@pytest.mark.parametrize("lifecycle", ["starting", "stale_quiet", "stale_probing"]) |
| 65 | +async def test_republish_states_only_when_running(lifecycle: str) -> None: |
| 66 | + """Do not re-send values from an inverter that is not being read.""" |
| 67 | + ist = _running_ist() |
| 68 | + ist.lifecycle = lifecycle # type: ignore[assignment] |
| 69 | + |
| 70 | + assert await ist.republish_states() == 0 |
| 71 | + ist.ss["power"].entity.send_state.assert_not_awaited() # type: ignore[union-attr] |
| 72 | + |
| 73 | + |
| 74 | +async def test_republish_states_resends_value_published_meanwhile() -> None: |
| 75 | + """A publish in flight updates _last after our send: re-send the newer value.""" |
| 76 | + ist = _running_ist() |
| 77 | + power = ist.ss["power"] |
| 78 | + |
| 79 | + async def send_state(_client: Any, payload: Any, **_kw: Any) -> None: |
| 80 | + if payload == 100: |
| 81 | + power._last = 900 # the in-flight ASensor.publish(900) completes |
| 82 | + |
| 83 | + power.entity.send_state.side_effect = send_state # type: ignore[union-attr] |
| 84 | + |
| 85 | + await ist.republish_states() |
| 86 | + |
| 87 | + assert power.entity.send_state.await_args_list == [call(MQTT, 100), call(MQTT, 900)] # type: ignore[union-attr] |
| 88 | + assert power.value == 900 |
| 89 | + |
| 90 | + |
| 91 | +async def test_republish_states_send_failure_does_not_stop_the_pass() -> None: |
| 92 | + """A failed send is logged and the other sensors are still re-sent.""" |
| 93 | + ist = _running_ist() |
| 94 | + ist.ss["power"].entity.send_state.side_effect = ConnectionError("broker gone") # type: ignore[union-attr] |
| 95 | + |
| 96 | + assert await ist.republish_states() == 1 |
| 97 | + |
| 98 | + ist.ss["energy"].entity.send_state.assert_awaited_once_with(MQTT, 12.3) # type: ignore[union-attr] |
| 99 | + |
| 100 | + |
| 101 | +async def test_republish_all_states_every_running_inverter_every_pass( |
| 102 | + monkeypatch: pytest.MonkeyPatch, |
| 103 | +) -> None: |
| 104 | + """Each pass re-sends the states of every running inverter, and only those.""" |
| 105 | + ist1, ist2 = _running_ist(), _running_ist() |
| 106 | + ist2.lifecycle = "starting" |
| 107 | + monkeypatch.setattr(a_inverter, "STATE", [ist1, ist2]) |
| 108 | + |
| 109 | + await republish_all_states(delays=(0, 0)) |
| 110 | + |
| 111 | + assert ist1.ss["power"].entity.send_state.await_count == 2 # type: ignore[union-attr] |
| 112 | + assert ist1.ss["energy"].entity.send_state.await_count == 2 # type: ignore[union-attr] |
| 113 | + ist2.ss["power"].entity.send_state.assert_not_awaited() # type: ignore[union-attr] |
| 114 | + |
| 115 | + |
| 116 | +async def test_republish_all_states_default_delays_from_ha_online( |
| 117 | + monkeypatch: pytest.MonkeyPatch, |
| 118 | +) -> None: |
| 119 | + """The passes run 10 s and 60 s after HA came online, not 60 s after pass one.""" |
| 120 | + clock = [1000.0] |
| 121 | + sleeps: list[float] = [] |
| 122 | + |
| 123 | + async def fake_sleep(seconds: float) -> None: |
| 124 | + sleeps.append(seconds) |
| 125 | + clock[0] += seconds |
| 126 | + |
| 127 | + async def pass_takes_2s() -> int: |
| 128 | + clock[0] += 2 |
| 129 | + return 1 |
| 130 | + |
| 131 | + ist = _running_ist() |
| 132 | + monkeypatch.setattr(ist, "republish_states", pass_takes_2s) |
| 133 | + monkeypatch.setattr(a_inverter, "STATE", [ist]) |
| 134 | + monkeypatch.setattr(a_inverter, "asyncio", SimpleNamespace(sleep=fake_sleep)) |
| 135 | + monkeypatch.setattr(a_inverter, "time", SimpleNamespace(monotonic=lambda: clock[0])) |
| 136 | + |
| 137 | + await republish_all_states() |
| 138 | + |
| 139 | + assert sleeps == [10, 48] |
0 commit comments