diff --git a/app/fanout/community_mqtt.py b/app/fanout/community_mqtt.py index 2d616a8..1c8228d 100644 --- a/app/fanout/community_mqtt.py +++ b/app/fanout/community_mqtt.py @@ -245,7 +245,7 @@ def _get_client_version() -> str: class CommunityMqttPublisher(BaseMqttPublisher): """Manages the community MQTT connection and publishes raw packets.""" - _backoff_max = 60 + _backoff_max = 3600 _log_prefix = "Community MQTT" _not_configured_timeout: float | None = 30 diff --git a/app/fanout/mqtt.py b/app/fanout/mqtt.py index c2965a4..605740c 100644 --- a/app/fanout/mqtt.py +++ b/app/fanout/mqtt.py @@ -27,7 +27,7 @@ class PrivateMqttSettings(Protocol): class MqttPublisher(BaseMqttPublisher): """Manages an MQTT connection and publishes mesh network events.""" - _backoff_max = 30 + _backoff_max = 3600 _log_prefix = "MQTT" def _is_configured(self) -> bool: diff --git a/app/fanout/mqtt_base.py b/app/fanout/mqtt_base.py index 4c7fc99..61ec2dd 100644 --- a/app/fanout/mqtt_base.py +++ b/app/fanout/mqtt_base.py @@ -65,6 +65,7 @@ class BaseMqttPublisher(ABC): self.connected: bool = False self.integration_name: str = "" self._last_error: str | None = None + self._error_notified: bool = False def set_integration_name(self, name: str) -> None: """Attach the configured fanout-module name for operator-facing logs.""" @@ -104,6 +105,7 @@ class BaseMqttPublisher(ABC): self._client = None self.connected = False self._last_error = None + self._error_notified = False async def restart(self, settings: object) -> None: """Called when settings change — stop + start.""" @@ -217,6 +219,7 @@ class BaseMqttPublisher(ABC): self._client = client self.connected = True self._last_error = None + self._error_notified = False backoff = _BACKOFF_MIN title, detail = self._on_connected(settings) @@ -281,9 +284,11 @@ class BaseMqttPublisher(ABC): ) return - title, detail = self._on_error() - broadcast_error(title, detail) - _broadcast_health() + if not self._error_notified: + title, detail = self._on_error() + broadcast_error(title, detail) + _broadcast_health() + self._error_notified = True logger.warning( "%s connection error. This is usually transient network noise; " "if it self-resolves, it is generally not a concern: %s " diff --git a/app/fanout/mqtt_ha.py b/app/fanout/mqtt_ha.py index 7df8af8..093401d 100644 --- a/app/fanout/mqtt_ha.py +++ b/app/fanout/mqtt_ha.py @@ -316,7 +316,7 @@ def _device_payload( class _HaMqttPublisher(BaseMqttPublisher): """Thin MQTT lifecycle wrapper for the HA discovery module.""" - _backoff_max = 30 + _backoff_max = 3600 _log_prefix = "HA-MQTT" def __init__(self) -> None: diff --git a/tests/test_mqtt.py b/tests/test_mqtt.py index 15b6b6d..1da4365 100644 --- a/tests/test_mqtt.py +++ b/tests/test_mqtt.py @@ -342,8 +342,8 @@ class TestConnectionLoop: assert sleep_args[0] == _BACKOFF_MIN assert sleep_args[1] == _BACKOFF_MIN * 2 assert sleep_args[2] == _BACKOFF_MIN * 4 - # Fourth should be capped at _backoff_max (5*8=40 > 30) - assert sleep_args[3] == MqttPublisher._backoff_max + # Fourth is still doubling (5*8=40), not yet at _backoff_max + assert sleep_args[3] == _BACKOFF_MIN * 8 @pytest.mark.asyncio async def test_waits_for_settings_when_unconfigured(self):