the boiler app now use mqtt instead of relying on an automation on ha side
This commit is contained in:
1 parent
a2f40f81ba
commit
6447d754ca
8 files changed
+261
-77
No files matched your search
@@ -11,6 +11,7 @@ sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'apps'))
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'apps' / 'ad_toolbox'))
|
||||
from boilerregulator import BoilerRegulator, setmode
|
||||
from boilerclimate import BoilerClimate
|
||||
from boilerwatchdog import BoilerWatchdog
|
||||
|
||||
|
||||
class RegulatorTests(unittest.TestCase):
|
||||
@@ -68,8 +69,12 @@ class ClimateTests(unittest.TestCase):
|
||||
# Test the actual app callbacks without starting an AppDaemon server.
|
||||
app = object.__new__(BoilerClimate)
|
||||
app.base, app.boiler = 'appdaemon/test', 'boiler'
|
||||
app.mqtt_namespace = 'mqtt'
|
||||
app.connection_topic = app.base + '/connection'
|
||||
app.mode, app.target = 'heat', 18
|
||||
app.indoor, app.outdoor = 'sensor.indoor', 'sensor.outdoor'
|
||||
app.outdoors = (app.outdoor,)
|
||||
app.selected_outdoor = None
|
||||
app.min_target, app.max_target = 10, 23
|
||||
app.interval = 60
|
||||
app.controller = BoilerRegulator()
|
||||
@@ -89,6 +94,39 @@ class ClimateTests(unittest.TestCase):
|
||||
def writes(self, app):
|
||||
return [c for c in app._publish.call_args_list if c.args[0] == 'boiler/bai/SetMode/set']
|
||||
|
||||
def test_mqtt_transport_uses_plugin_namespace(self):
|
||||
a = self.app()
|
||||
a.call_service = Mock()
|
||||
BoilerClimate._publish(a, 'boiler/bai/SetMode/set', setmode(40, True))
|
||||
call = a.call_service.call_args
|
||||
self.assertEqual(call.args, ('mqtt/publish',))
|
||||
self.assertEqual(call.kwargs['namespace'], 'mqtt')
|
||||
self.assertFalse(call.kwargs['retain'])
|
||||
self.assertEqual(json.loads(call.kwargs['payload']), setmode(40, True))
|
||||
|
||||
def test_discovery_requires_app_and_connection_online(self):
|
||||
a = self.app()
|
||||
a.uid, a.title = 'test_boiler', 'Test boiler'
|
||||
a._discovery()
|
||||
config = a._publish.call_args_list[0].args[1]
|
||||
self.assertNotIn('availability_topic', config)
|
||||
self.assertEqual(config['availability_mode'], 'all')
|
||||
self.assertEqual(config['availability'], [{'topic': a.base + '/availability'},
|
||||
{'topic': a.connection_topic}])
|
||||
|
||||
def test_mqtt_reconnect_renews_discovery_and_command(self):
|
||||
a = self.app()
|
||||
a._discovery = Mock()
|
||||
a._mqtt_event('MQTT_MESSAGE', {'state': 'Connected', 'topic': None}, {})
|
||||
a._discovery.assert_called_once()
|
||||
self.assertEqual(len(self.writes(a)), 1)
|
||||
|
||||
def test_mqtt_disconnect_does_not_publish_or_advance_controller(self):
|
||||
a = self.app()
|
||||
a._mqtt_event('MQTT_MESSAGE', {'state': 'Disconnected', 'topic': None}, {})
|
||||
a._publish.assert_not_called()
|
||||
self.assertEqual(a.previous_time, 1000)
|
||||
|
||||
def test_single_change_and_periodic_refresh_only(self):
|
||||
a = self.app()
|
||||
with patch('boilerclimate.time.monotonic', return_value=1000):
|
||||
@@ -103,6 +141,34 @@ class ClimateTests(unittest.TestCase):
|
||||
for call in self.writes(a):
|
||||
self.assertFalse(call.args[2])
|
||||
|
||||
def test_lowest_outdoor_reading_controls_regulation(self):
|
||||
a = self.app()
|
||||
a.target = 23
|
||||
a.outdoors = ('sensor.front', 'sensor.back')
|
||||
readings = {a.indoor: 22.6, 'sensor.front': 30.2, 'sensor.back': 23.7}
|
||||
a._sensor_value = lambda entity: readings[entity]
|
||||
a._control()
|
||||
self.assertEqual(a._state['outdoor'], 23.7)
|
||||
self.assertEqual(a.selected_outdoor, 'sensor.back')
|
||||
self.assertTrue(a._state['heating_requested'])
|
||||
readings['sensor.front'] = 20
|
||||
a._sensor_changed('sensor.front', 'all', None, None, {})
|
||||
self.assertEqual(a._state['outdoor'], 20)
|
||||
self.assertEqual(a.selected_outdoor, 'sensor.front')
|
||||
|
||||
def test_invalid_second_outdoor_sensor_still_inhibits_heat(self):
|
||||
a = self.app()
|
||||
a.outdoors = ('sensor.front', 'sensor.back')
|
||||
def reading(entity):
|
||||
if entity == 'sensor.back':
|
||||
raise ValueError('sensor.back: unavailable')
|
||||
return 18 if entity == a.indoor else 5
|
||||
a._sensor_value = reading
|
||||
a._control()
|
||||
self.assertEqual(self.writes(a)[0].args[1]['disablehc'], 1)
|
||||
self.assertEqual(a._state['reason'], 'invalid_sensor')
|
||||
self.assertIn('sensor.back', a.sensor_error)
|
||||
|
||||
def test_invalid_sensor_explicitly_inhibits_heat(self):
|
||||
a = self.app()
|
||||
a._sensor_value = Mock(side_effect=ValueError('unavailable'))
|
||||
@@ -265,5 +331,45 @@ class ClimateTests(unittest.TestCase):
|
||||
self.assertEqual(a.controller.integral, integral)
|
||||
|
||||
|
||||
class WatchdogTests(unittest.TestCase):
|
||||
def app(self):
|
||||
a = object.__new__(BoilerWatchdog)
|
||||
a.base, a.mqtt_namespace, a.timeout = 'appdaemon/test', 'mqtt', 180
|
||||
a.last_heartbeat, a.fault = None, 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()
|
||||
a.call_service.assert_called_once_with('mqtt/publish', namespace='mqtt',
|
||||
topic=a.base + '/availability', payload='offline', qos=0, retain=True)
|
||||
|
||||
def test_timeout_and_recovery_do_not_override_sensor_availability(self):
|
||||
a = self.app()
|
||||
with patch('boilerwatchdog.time.monotonic', return_value=1000):
|
||||
a._mqtt_event('', {'topic': a.base + '/heartbeat', 'payload': 'timestamp'}, {})
|
||||
with patch('boilerwatchdog.time.monotonic', return_value=1179):
|
||||
a._check()
|
||||
a.call_service.assert_not_called()
|
||||
with patch('boilerwatchdog.time.monotonic', return_value=1180):
|
||||
a._check()
|
||||
self.assertTrue(a.fault)
|
||||
a.call_service.reset_mock()
|
||||
a._mqtt_event('', {'topic': a.base + '/heartbeat'}, {})
|
||||
self.assertFalse(a.fault)
|
||||
a.call_service.assert_not_called() # The climate alone decides online/offline for sensor faults.
|
||||
|
||||
def test_reconnection_waits_for_new_heartbeat(self):
|
||||
a = self.app()
|
||||
a.last_heartbeat = 1000
|
||||
a._mqtt_event('', {'topic': None, 'state': 'Connected'}, {})
|
||||
self.assertIsNone(a.last_heartbeat)
|
||||
self.assertTrue(a.fault)
|
||||
self.assertEqual(a.call_service.call_args.kwargs['payload'], 'offline')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user