From d79bba15b02155f4bd8cf02a9d60dbc84b965bf2 Mon Sep 17 00:00:00 2001 From: Pierre Date: Wed, 14 Oct 2026 19:05:40 +0200 Subject: [PATCH] Remove initialisation warning from the boiler --- apps/boilerwatchdog.py | 14 +++++++++++--- tests/test_boilerclimate.py | 21 +++++++++++++++++++-- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/apps/boilerwatchdog.py b/apps/boilerwatchdog.py index 22f970f..0d27f67 100644 --- a/apps/boilerwatchdog.py +++ b/apps/boilerwatchdog.py @@ -17,7 +17,9 @@ class BoilerWatchdog(hass.Hass, LoggerInterface): if self.AD.plugins.get_plugin_object(self.mqtt_namespace) is None: raise RuntimeError('MQTT plugin missing; configure it and restart AppDaemon') self.last_heartbeat = None + self.waiting_since = time.monotonic() self.fault = False + self.warned = False self.listen_event(self._mqtt_event, 'MQTT_MESSAGE', namespace=self.mqtt_namespace) self.run_every(self._check, 'now', 10) self._check() @@ -25,18 +27,24 @@ class BoilerWatchdog(hass.Hass, LoggerInterface): def _mqtt_event(self, event, data, kwargs): if data.get('topic') == self.base + '/heartbeat': self.last_heartbeat = time.monotonic() - if self.fault: + if self.warned: self.log_info('Boiler climate heartbeat recovered') self.fault = False + self.warned = False elif data.get('topic') is None and data.get('state') == 'Connected': self.last_heartbeat = None + self.waiting_since = time.monotonic() self.fault = False + self.warned = False self._check() def _check(self, kwargs=None): - if self.last_heartbeat is None or time.monotonic() - self.last_heartbeat >= self.timeout: + age = time.monotonic() - (self.waiting_since if self.last_heartbeat is None else self.last_heartbeat) + if self.last_heartbeat is None or age >= self.timeout: if not self.fault: self.call_service('mqtt/publish', namespace=self.mqtt_namespace, topic=self.base + '/availability', payload='offline', qos=0, retain=True) - self.log_warning('Boiler climate unavailable: waiting for a fresh heartbeat') self.fault = True + if age >= self.timeout and not self.warned: + self.log_warning('Boiler climate unavailable: no heartbeat within the timeout') + self.warned = True diff --git a/tests/test_boilerclimate.py b/tests/test_boilerclimate.py index 54f52f9..2669feb 100644 --- a/tests/test_boilerclimate.py +++ b/tests/test_boilerclimate.py @@ -467,16 +467,31 @@ class WatchdogTests(unittest.TestCase): a = object.__new__(BoilerWatchdog) a.base, a.mqtt_namespace, a.timeout = 'appdaemon/test', 'mqtt', 180 a.last_heartbeat, a.fault = None, False + a.waiting_since, a.warned = 1000, False a.call_service, a.log = Mock(), Mock() a.initialize_logger_interface(a, ascii_encode=False) return a def test_missing_heartbeat_publishes_offline_once(self): a = self.app() - a._check() - a._check() + with patch('boilerwatchdog.time.monotonic', return_value=1000): + a._check() + a._check() a.call_service.assert_called_once_with('mqtt/publish', namespace='mqtt', topic=a.base + '/availability', payload='offline', qos=0, retain=True) + a.log.assert_not_called() + a._mqtt_event('', {'topic': a.base + '/heartbeat'}, {}) + a.log.assert_not_called() + + def test_first_heartbeat_missing_beyond_timeout_warns_once(self): + a = self.app() + with patch('boilerwatchdog.time.monotonic', return_value=1000): + a._check() + with patch('boilerwatchdog.time.monotonic', return_value=1180): + a._check() + a._check() + self.assertEqual(a.log.call_count, 1) + self.assertEqual(a.log.call_args.kwargs['level'], 'WARNING') def test_timeout_and_recovery_do_not_override_sensor_availability(self): a = self.app() @@ -488,6 +503,7 @@ class WatchdogTests(unittest.TestCase): with patch('boilerwatchdog.time.monotonic', return_value=1180): a._check() self.assertTrue(a.fault) + self.assertTrue(a.warned) a.call_service.reset_mock() a._mqtt_event('', {'topic': a.base + '/heartbeat'}, {}) self.assertFalse(a.fault) @@ -500,6 +516,7 @@ class WatchdogTests(unittest.TestCase): self.assertIsNone(a.last_heartbeat) self.assertTrue(a.fault) self.assertEqual(a.call_service.call_args.kwargs['payload'], 'offline') + a.log.assert_not_called() if __name__ == '__main__':