M8-R02: report DSMR MQTT source health

This commit is contained in:
2026-08-24 06:45:30 +02:00
parent e59c192097
commit d9e82038dc
6 changed files with 483 additions and 9 deletions
+82
View File
@@ -437,6 +437,88 @@ def test_source_sync_connack_before_connect_returns_subscribes_all_topics() -> N
assert client.subscribed == ["telegram/topic", "tariff/topic"]
def test_source_health_tracks_connack_disconnect_and_ignores_stale_callbacks() -> None:
"""A source is connecting until CONNACK, and old generations cannot rewrite health."""
manager = MqttManager()
old_client = MagicMock()
new_client = MagicMock()
old_states: list[str] = []
new_states: list[str] = []
kwargs = {
"host": "broker.test",
"port": 1883,
"username": "",
"password": "",
"tls_enabled": False,
"subscriptions": {"topic": lambda _payload: None},
}
with patch("app.integrations.mqtt.mqtt.Client", side_effect=[old_client, new_client]):
assert manager.replace_source(1, **kwargs, state_handler=old_states.append)
assert old_states == ["connecting"]
assert manager.replace_source(1, **kwargs, state_handler=new_states.append)
accepted = MagicMock()
accepted.is_failure = False
old_client.on_connect(old_client, None, MagicMock(), accepted, None)
old_client.on_disconnect(old_client, None, MagicMock(), MagicMock(), None)
assert old_states == ["connecting"]
new_client.on_connect(new_client, None, MagicMock(), accepted, None)
new_client.on_disconnect(new_client, None, MagicMock(), MagicMock(), None)
assert new_states == ["connecting", "online", "error"]
def test_source_health_rejected_connack_reports_error_without_disconnect() -> None:
"""A failed CONNACK is distinct from a later disconnect callback."""
manager = MqttManager()
client = MagicMock()
states: list[str] = []
with patch("app.integrations.mqtt.mqtt.Client", return_value=client):
assert manager.replace_source(
1, host="broker.test", port=1883, username="", password="", tls_enabled=False,
subscriptions={"topic": lambda _payload: None}, state_handler=states.append,
)
accepted = MagicMock()
accepted.is_failure = False
refused = MagicMock()
refused.is_failure = True
client.on_connect(client, None, MagicMock(), accepted, None)
client.on_connect(client, None, MagicMock(), refused, None)
assert states == ["connecting", "online", "error"]
assert 1 not in manager._source_connected
def test_source_health_disconnect_then_reconnect_connack_is_ordered_and_isolated() -> None:
"""One source's reconnect sequence cannot change another source's health."""
manager = MqttManager()
first_client = MagicMock()
second_client = MagicMock()
first_states: list[str] = []
second_states: list[str] = []
kwargs = {
"host": "broker.test",
"port": 1883,
"username": "",
"password": "",
"tls_enabled": False,
"subscriptions": {"topic": lambda _payload: None},
}
with patch("app.integrations.mqtt.mqtt.Client", side_effect=[first_client, second_client]):
assert manager.replace_source(1, **kwargs, state_handler=first_states.append)
assert manager.replace_source(2, **kwargs, state_handler=second_states.append)
accepted = MagicMock()
accepted.is_failure = False
first_client.on_connect(first_client, None, MagicMock(), accepted, None)
first_client.on_disconnect(first_client, None, MagicMock(), MagicMock(), None)
first_client.on_connect(first_client, None, MagicMock(), accepted, None)
assert first_states == ["connecting", "online", "error", "online"]
assert second_states == ["connecting"]
def test_source_teardown_with_joining_loop_stop_waits_for_callback_before_aba() -> None:
"""loop_stop may join a callback that needs the manager lock to finish."""
manager = MqttManager()