Added a smart condition to the boiler
This commit is contained in:
1 parent
6447d754ca
commit
79dd13f31d
4 files changed
+215
-6
No files matched your search
+132
-1
@@ -5,12 +5,13 @@ import tempfile
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
import unittest
|
||||
import yaml
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
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 boilerclimate import BoilerClimate, SmartCondition
|
||||
from boilerwatchdog import BoilerWatchdog
|
||||
|
||||
|
||||
@@ -69,6 +70,7 @@ 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.entity_id = 'climate.radiator_heating'
|
||||
app.mqtt_namespace = 'mqtt'
|
||||
app.connection_topic = app.base + '/connection'
|
||||
app.mode, app.target = 'heat', 18
|
||||
@@ -86,6 +88,8 @@ class ClimateTests(unittest.TestCase):
|
||||
app._publish = Mock()
|
||||
app._save = Mock()
|
||||
app.log = Mock()
|
||||
app.entity_exists = Mock(return_value=True)
|
||||
app.get_state = Mock(return_value=18)
|
||||
app.initialize_logger_interface(app, ascii_encode=False)
|
||||
readings = {app.indoor: 18, app.outdoor: 10}
|
||||
app._sensor_value = lambda entity: readings[entity]
|
||||
@@ -195,6 +199,133 @@ class ClimateTests(unittest.TestCase):
|
||||
a._mqtt_event('', dict(topic=a.base + '/command/mode', payload='off'), {})
|
||||
self.assertEqual(self.writes(a)[0].args[1]['disablehc'], 1)
|
||||
|
||||
def test_smart_conditions_without_configuration_keep_manual_mode(self):
|
||||
a = self.app()
|
||||
a.args = {}
|
||||
a._initialize_smart_conditions()
|
||||
self.assertIsNone(a.smart_conditions_evaluator)
|
||||
self.assertEqual(a.mode, 'heat')
|
||||
a._publish.assert_not_called()
|
||||
|
||||
def test_smart_condition_evaluates_initial_state_and_sensor_changes(self):
|
||||
a = self.app()
|
||||
a.args = {'smart_conditions': {'trigger_conditions': ['binary_sensor.test_heating']}}
|
||||
a.entity_exists = Mock(return_value=True)
|
||||
a.get_state = Mock(return_value='off')
|
||||
a.listen_state = Mock(return_value='condition_callback')
|
||||
a.cancel_listen_state = Mock()
|
||||
a._initialize_smart_conditions()
|
||||
self.assertEqual(a.mode, 'off')
|
||||
self.assertEqual(self.writes(a)[-1].args[1]['disablehc'], 1)
|
||||
callback = a.listen_state.call_args.args[0]
|
||||
a.get_state.return_value = 'on'
|
||||
callback('binary_sensor.test_heating', 'state', 'off', 'on', {})
|
||||
self.assertEqual(a.mode, 'heat')
|
||||
self.assertEqual(self.writes(a)[-1].args[1]['disablehc'], 0)
|
||||
self.assertEqual(a.target, 18)
|
||||
a.get_state.return_value = 'off'
|
||||
callback('binary_sensor.test_heating', 'state', 'on', 'off', {})
|
||||
self.assertEqual(a.mode, 'off')
|
||||
a.terminate()
|
||||
a.cancel_listen_state.assert_called_once_with('condition_callback')
|
||||
|
||||
def test_smart_condition_disabled_preserves_manual_mode(self):
|
||||
a = self.app()
|
||||
a.on_smart_conditions_change(None, SmartCondition.Result.Disabled)
|
||||
self.assertEqual(a.mode, 'heat')
|
||||
a._publish.assert_not_called()
|
||||
|
||||
def test_smart_condition_does_not_bypass_invalid_sensor(self):
|
||||
a = self.app()
|
||||
a.mode = 'off'
|
||||
a._sensor_value = Mock(side_effect=ValueError('unavailable'))
|
||||
a.on_smart_conditions_change(None, SmartCondition.Result.Succeeded)
|
||||
self.assertEqual(a.mode, 'heat')
|
||||
self.assertEqual(self.writes(a)[-1].args[1]['disablehc'], 1)
|
||||
|
||||
def test_condition_cleanup_failure_does_not_block_shutdown(self):
|
||||
a = self.app()
|
||||
a.smart_conditions_evaluator = Mock()
|
||||
a.smart_conditions_evaluator.deactivate.side_effect = RuntimeError('cleanup failed')
|
||||
a.terminate()
|
||||
self.assertEqual(self.writes(a)[-1].args[1]['disablehc'], 1)
|
||||
a._save.assert_called_once()
|
||||
|
||||
def test_configured_presence_weather_and_window_rule(self):
|
||||
config = yaml.safe_load((Path(__file__).resolve().parents[1] /
|
||||
'apps/global/heating.yaml').read_text())['radiator_heating']
|
||||
baseline = {'binary_sensor.someone_home': 'on',
|
||||
'sensor.ext_front_temperature': '30', 'sensor.ext_back_temperature': '10',
|
||||
'binary_sensor.group_windows': 'off',
|
||||
'binary_sensor.kitchen_french_door': 'off',
|
||||
'binary_sensor.living_room_french_door': 'off',
|
||||
'binary_sensor.bedroom_french_door': 'off'}
|
||||
cases = [({}, 'heat'),
|
||||
({'binary_sensor.someone_home': 'off'}, 'off'),
|
||||
({'sensor.ext_back_temperature': '18'}, 'off'),
|
||||
({'sensor.ext_front_temperature': '10', 'sensor.ext_back_temperature': '30'}, 'heat')]
|
||||
for entity in ['binary_sensor.group_windows', 'binary_sensor.kitchen_french_door',
|
||||
'binary_sensor.living_room_french_door', 'binary_sensor.bedroom_french_door']:
|
||||
cases.append(({entity: 'on'}, 'off'))
|
||||
for changes, expected in cases:
|
||||
with self.subTest(changes=changes):
|
||||
a = self.app()
|
||||
a.mode = 'off'
|
||||
a.args = config
|
||||
states = {**baseline, **changes}
|
||||
a.entity_exists = lambda entity: entity in states or entity == a.entity_id
|
||||
a.get_state = lambda entity, **kwargs: a.target if entity == a.entity_id else states[entity]
|
||||
a.listen_state = Mock(return_value='condition_callback')
|
||||
a._initialize_smart_conditions()
|
||||
self.assertEqual(a.mode, expected)
|
||||
listened = {c.args[1] for c in a.listen_state.call_args_list}
|
||||
self.assertTrue(set(baseline).issubset(listened))
|
||||
|
||||
def test_target_change_is_evaluated_when_ha_attribute_updates(self):
|
||||
a = self.app()
|
||||
a.args = {'smart_conditions': {
|
||||
'trigger_conditions': ['sensor.outside < self.temperature']}}
|
||||
a.entity_exists = Mock(return_value=True)
|
||||
states = {a.entity_id: 18, 'sensor.outside': 20}
|
||||
a.get_state = lambda entity, **kwargs: states[entity]
|
||||
a.listen_state = Mock(return_value='condition_callback')
|
||||
a.cancel_listen_state = Mock()
|
||||
a._initialize_smart_conditions()
|
||||
self.assertEqual(a.mode, 'off') # 20 outside >= target 18.
|
||||
evaluator = a.smart_conditions_evaluator
|
||||
callback = next(c.args[0] for c in a.listen_state.call_args_list
|
||||
if c.args[1] == a.entity_id and c.kwargs.get('attribute') == 'temperature')
|
||||
for target, mode in [(23, 'heat'), (20, 'off'), (21, 'heat')]:
|
||||
previous_mode = a.mode
|
||||
old_target = states[a.entity_id]
|
||||
a._mqtt_event('', dict(topic=a.base + '/command/temperature', payload=str(target)), {})
|
||||
self.assertEqual(a.mode, previous_mode) # Wait for HA's attribute update.
|
||||
states[a.entity_id] = target
|
||||
callback(a.entity_id, 'temperature', old_target, target, {})
|
||||
self.assertEqual(a.mode, mode)
|
||||
self.assertEqual(a.target, target)
|
||||
self.assertIs(a.smart_conditions_evaluator, evaluator)
|
||||
a.cancel_listen_state.assert_not_called()
|
||||
|
||||
def test_smart_conditions_wait_for_discovered_entity_and_temperature(self):
|
||||
a = self.app()
|
||||
a.mode, a.target = 'off', 23
|
||||
a.args = {'smart_conditions': {'trigger_conditions': ['sensor.outside < self.temperature']}}
|
||||
a.entity_exists = Mock(return_value=False)
|
||||
a.get_state = Mock(return_value=None)
|
||||
a.run_in = Mock()
|
||||
a.listen_state = Mock(return_value='condition_callback')
|
||||
a._initialize_smart_conditions()
|
||||
self.assertEqual(a.mode, 'off')
|
||||
self.assertIsNone(a.smart_conditions_evaluator)
|
||||
a.run_in.assert_called_once_with(a._initialize_smart_conditions, 1)
|
||||
a.entity_exists.return_value = True
|
||||
a._initialize_smart_conditions({}) # Entity exists but its target is not ready.
|
||||
self.assertIsNone(a.smart_conditions_evaluator)
|
||||
a.get_state = lambda entity, **kwargs: 20 if entity == 'sensor.outside' else 23
|
||||
a._initialize_smart_conditions()
|
||||
self.assertEqual(a.mode, 'heat')
|
||||
|
||||
def test_native_ha_template_payload_is_accepted(self):
|
||||
a = self.app()
|
||||
a._mqtt_event('', dict(topic='boiler/bai/Statenumber', payload={'value': 30}), {})
|
||||
|
||||
Reference in new issue
Block a user