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
+40
-5
@@ -5,6 +5,7 @@ from pathlib import Path
|
|||||||
import time
|
import time
|
||||||
|
|
||||||
import appdaemon.plugins.hass.hassapi as hass
|
import appdaemon.plugins.hass.hassapi as hass
|
||||||
|
import ad_toolbox.smartcondition as SmartCondition
|
||||||
from boilerregulator import BoilerRegulator, finite, setmode
|
from boilerregulator import BoilerRegulator, finite, setmode
|
||||||
from logger_interface import LoggerInterface
|
from logger_interface import LoggerInterface
|
||||||
|
|
||||||
@@ -19,6 +20,7 @@ class BoilerClimate(hass.Hass, LoggerInterface):
|
|||||||
self.boiler = self.args.get('boiler_topic', 'themaplus-condens').rstrip('/')
|
self.boiler = self.args.get('boiler_topic', 'themaplus-condens').rstrip('/')
|
||||||
self.connection_topic = self.args.get('mqtt_connection_topic', self.base + '/connection')
|
self.connection_topic = self.args.get('mqtt_connection_topic', self.base + '/connection')
|
||||||
self.uid = self.args.get('unique_id', 'appdaemon_radiator_heating')
|
self.uid = self.args.get('unique_id', 'appdaemon_radiator_heating')
|
||||||
|
self.entity_id = self.args.get('entity', 'climate.radiator_heating')
|
||||||
self.title = self.args.get('display_name', 'Chauffage radiateurs')
|
self.title = self.args.get('display_name', 'Chauffage radiateurs')
|
||||||
self.indoor = self.args['indoor_sensor']
|
self.indoor = self.args['indoor_sensor']
|
||||||
outdoors = self.args.get('outdoor_sensors')
|
outdoors = self.args.get('outdoor_sensors')
|
||||||
@@ -54,10 +56,40 @@ class BoilerClimate(hass.Hass, LoggerInterface):
|
|||||||
for entity in self.outdoors:
|
for entity in self.outdoors:
|
||||||
self.listen_state(self._sensor_changed, entity, attribute='all')
|
self.listen_state(self._sensor_changed, entity, attribute='all')
|
||||||
self._discovery()
|
self._discovery()
|
||||||
|
self._initialize_smart_conditions()
|
||||||
self.run_every(self._tick, 'now', self.interval)
|
self.run_every(self._tick, 'now', self.interval)
|
||||||
self._control()
|
self._control()
|
||||||
self.log_info(f'Boiler climate ready: MQTT writes={self.send_commands}, room target={self.target} °C')
|
self.log_info(f'Boiler climate ready: MQTT writes={self.send_commands}, room target={self.target} °C')
|
||||||
|
|
||||||
|
def _initialize_smart_conditions(self, kwargs=None):
|
||||||
|
if getattr(self, 'smart_conditions_evaluator', None) is not None:
|
||||||
|
return
|
||||||
|
self.smart_conditions_evaluator = None
|
||||||
|
if 'smart_conditions' in self.args:
|
||||||
|
if not self.entity_exists(self.entity_id) or self.get_state(self.entity_id, attribute='temperature') is None:
|
||||||
|
self.mode = 'off'
|
||||||
|
self.run_in(self._initialize_smart_conditions, 1)
|
||||||
|
return
|
||||||
|
constants = {**self.args.get('constants', {}), 'self': self.entity_id}
|
||||||
|
self.smart_conditions_evaluator = SmartCondition.Evaluator(
|
||||||
|
self, self.args['smart_conditions'], condition_name='smart_conditions',
|
||||||
|
on_change_cb=self.on_smart_conditions_change,
|
||||||
|
constants=constants)
|
||||||
|
|
||||||
|
def on_smart_conditions_change(self, previous, result):
|
||||||
|
if result == SmartCondition.Result.Disabled:
|
||||||
|
return
|
||||||
|
mode = 'heat' if result == SmartCondition.Result.Succeeded else 'off'
|
||||||
|
if mode != self.mode:
|
||||||
|
self._set_mode(mode, 'SmartCondition')
|
||||||
|
|
||||||
|
def _set_mode(self, mode, source):
|
||||||
|
self.log_info(f'Climate mode: {self.mode} -> {mode} ({source})')
|
||||||
|
self.mode = mode
|
||||||
|
self.controller.reset()
|
||||||
|
self._control(force=True)
|
||||||
|
self._save_safely()
|
||||||
|
|
||||||
def _publish(self, topic, payload, retain=False):
|
def _publish(self, topic, payload, retain=False):
|
||||||
if not isinstance(payload, str):
|
if not isinstance(payload, str):
|
||||||
payload = json.dumps(payload, allow_nan=False, ensure_ascii=False)
|
payload = json.dumps(payload, allow_nan=False, ensure_ascii=False)
|
||||||
@@ -71,7 +103,7 @@ class BoilerClimate(hass.Hass, LoggerInterface):
|
|||||||
{'topic': self.connection_topic}],
|
{'topic': self.connection_topic}],
|
||||||
availability_mode='all')
|
availability_mode='all')
|
||||||
config = dict(common, name=None, unique_id=self.uid,
|
config = dict(common, name=None, unique_id=self.uid,
|
||||||
default_entity_id='climate.radiator_heating',
|
default_entity_id=self.entity_id,
|
||||||
modes=['off', 'heat'], temperature_unit='C', precision=0.1,
|
modes=['off', 'heat'], temperature_unit='C', precision=0.1,
|
||||||
temp_step=0.5, min_temp=self.min_target, max_temp=self.max_target,
|
temp_step=0.5, min_temp=self.min_target, max_temp=self.max_target,
|
||||||
mode_command_topic=self.base + '/command/mode',
|
mode_command_topic=self.base + '/command/mode',
|
||||||
@@ -170,10 +202,7 @@ class BoilerClimate(hass.Hass, LoggerInterface):
|
|||||||
if payload not in ('off', 'heat'):
|
if payload not in ('off', 'heat'):
|
||||||
self.log_warning('Rejected invalid climate mode')
|
self.log_warning('Rejected invalid climate mode')
|
||||||
return
|
return
|
||||||
self.mode = payload
|
self._set_mode(payload, 'MQTT')
|
||||||
self.controller.reset()
|
|
||||||
self._control(force=True)
|
|
||||||
self._save_safely()
|
|
||||||
elif topic == self.base + '/command/temperature':
|
elif topic == self.base + '/command/temperature':
|
||||||
try:
|
try:
|
||||||
target = finite(payload)
|
target = finite(payload)
|
||||||
@@ -286,11 +315,17 @@ class BoilerClimate(hass.Hass, LoggerInterface):
|
|||||||
# on boiler command expiration. No retained actuator command, ever.
|
# on boiler command expiration. No retained actuator command, ever.
|
||||||
if not hasattr(self, 'base'):
|
if not hasattr(self, 'base'):
|
||||||
return
|
return
|
||||||
|
evaluator = getattr(self, 'smart_conditions_evaluator', None)
|
||||||
try:
|
try:
|
||||||
if self.send_commands:
|
if self.send_commands:
|
||||||
self._publish(self.boiler + '/bai/SetMode/set', setmode(0, False))
|
self._publish(self.boiler + '/bai/SetMode/set', setmode(0, False))
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
self.log_error(f'Could not publish shutdown; boiler expiration remains the fallback: {exc}')
|
self.log_error(f'Could not publish shutdown; boiler expiration remains the fallback: {exc}')
|
||||||
|
if evaluator is not None:
|
||||||
|
try:
|
||||||
|
evaluator.deactivate()
|
||||||
|
except Exception as exc:
|
||||||
|
self.log_warning(f'Could not deactivate SmartCondition: {exc}')
|
||||||
try:
|
try:
|
||||||
self._publish(self.base + '/availability', 'offline', True)
|
self._publish(self.base + '/availability', 'offline', True)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
|
|||||||
@@ -221,6 +221,7 @@ guestroom_heatpump:
|
|||||||
radiator_heating:
|
radiator_heating:
|
||||||
module: boilerclimate
|
module: boilerclimate
|
||||||
class: BoilerClimate
|
class: BoilerClimate
|
||||||
|
entity: climate.radiator_heating
|
||||||
display_name: Chauffage radiateurs
|
display_name: Chauffage radiateurs
|
||||||
unique_id: appdaemon_radiator_heating
|
unique_id: appdaemon_radiator_heating
|
||||||
indoor_sensor: sensor.corridor_temperature
|
indoor_sensor: sensor.corridor_temperature
|
||||||
@@ -230,6 +231,14 @@ radiator_heating:
|
|||||||
target_temperature: 18
|
target_temperature: 18
|
||||||
initial_mode: heat
|
initial_mode: heat
|
||||||
|
|
||||||
|
smart_conditions:
|
||||||
|
local_expressions:
|
||||||
|
outdoor_temperature: "sensor.ext_front_temperature < sensor.ext_back_temperature ? sensor.ext_front_temperature : sensor.ext_back_temperature"
|
||||||
|
trigger_conditions: binary_sensor.someone_home and outdoor_temperature < self.temperature
|
||||||
|
blocking_conditions:
|
||||||
|
- binary_sensor.group_windows
|
||||||
|
- binary_sensor.kitchen_french_door or binary_sensor.living_room_french_door or binary_sensor.bedroom_french_door
|
||||||
|
|
||||||
# Radiator heating enabled with the room-thermostat bridge CLOSED.
|
# Radiator heating enabled with the room-thermostat bridge CLOSED.
|
||||||
# false keeps the climate/diagnostics but suppresses SetMode writes.
|
# false keeps the climate/diagnostics but suppresses SetMode writes.
|
||||||
send_commands: true
|
send_commands: true
|
||||||
|
|||||||
@@ -28,6 +28,40 @@ télémétrie en MQTT direct. Aucun relais MQTT dans Home Assistant n'est néces
|
|||||||
|
|
||||||
## Régulation
|
## Régulation
|
||||||
|
|
||||||
|
Le bloc facultatif `smart_conditions` contrôle le mode `heat`/`off` avec le même
|
||||||
|
évaluateur que les autres apps : `Succeeded` passe en `heat`, `Failed` passe
|
||||||
|
en `off`. L'évaluation initiale est appliquée au démarrage, puis chaque changement
|
||||||
|
de résultat. Un résultat `Disabled` suspend l'automatisme et conserve le mode.
|
||||||
|
Les commandes manuelles restent possibles ; elles restent en vigueur jusqu'au
|
||||||
|
prochain changement de résultat de la condition ou redémarrage de l'app.
|
||||||
|
Le changement de mode envoie immédiatement la commande chaudière et est persisté.
|
||||||
|
La consigne ambiante n'est pas modifiée et les sécurités capteurs restent actives.
|
||||||
|
Sans ce bloc, le mode reste entièrement manuel comme auparavant. Exemple :
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
smart_conditions:
|
||||||
|
trigger_conditions:
|
||||||
|
- binary_sensor.heatpump_winter_mode
|
||||||
|
```
|
||||||
|
|
||||||
|
Les options standard (`blocking_conditions`, `disable_conditions`,
|
||||||
|
`callback_delay`, constantes locales) sont prises en charge.
|
||||||
|
|
||||||
|
La règle active demande `heat` uniquement si quelqu'un est à la maison,
|
||||||
|
si le minimum extérieur est inférieur à la consigne ambiante (`self.temperature`),
|
||||||
|
et si toutes les fenêtres et portes-fenêtres surveillées sont
|
||||||
|
fermées. `binary_sensor.group_windows` couvre bureau, salon, chambre d'amis
|
||||||
|
et dressing ; les portes-fenêtres cuisine, salon et chambre sont ajoutées
|
||||||
|
séparément. La règle utilise les états booléens de présence et d'ouverture.
|
||||||
|
Le mode est recalculé au démarrage et lors des changements de résultat de la
|
||||||
|
règle, sans temporisation ajoutée. Après un changement de consigne, la règle
|
||||||
|
est réévaluée lorsque Home Assistant actualise l'attribut `temperature` du climate.
|
||||||
|
`self` désigne l'entité configurée par `entity: climate.radiator_heating`.
|
||||||
|
Toutes les expressions lisent les états Home Assistant normalement. Au premier
|
||||||
|
démarrage, le chauffage reste en `off` pendant l'attente de l'entité et de sa
|
||||||
|
consigne, avant de créer l'évaluateur. Celui-ci est ensuite conservé pendant
|
||||||
|
toute la durée de vie de l'app, y compris lors des changements de consigne.
|
||||||
|
|
||||||
Avec S la consigne ambiante, Ti la température du couloir et Te la plus basse
|
Avec S la consigne ambiante, Ti la température du couloir et Te la plus basse
|
||||||
des températures extérieures (`sensor.ext_front_temperature` et
|
des températures extérieures (`sensor.ext_front_temperature` et
|
||||||
`sensor.ext_back_temperature`, configurées dans `outdoor_sensors`) :
|
`sensor.ext_back_temperature`, configurées dans `outdoor_sensors`) :
|
||||||
|
|||||||
+132
-1
@@ -5,12 +5,13 @@ import tempfile
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
import unittest
|
import unittest
|
||||||
|
import yaml
|
||||||
from unittest.mock import Mock, patch
|
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'))
|
||||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'apps' / 'ad_toolbox'))
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'apps' / 'ad_toolbox'))
|
||||||
from boilerregulator import BoilerRegulator, setmode
|
from boilerregulator import BoilerRegulator, setmode
|
||||||
from boilerclimate import BoilerClimate
|
from boilerclimate import BoilerClimate, SmartCondition
|
||||||
from boilerwatchdog import BoilerWatchdog
|
from boilerwatchdog import BoilerWatchdog
|
||||||
|
|
||||||
|
|
||||||
@@ -69,6 +70,7 @@ class ClimateTests(unittest.TestCase):
|
|||||||
# Test the actual app callbacks without starting an AppDaemon server.
|
# Test the actual app callbacks without starting an AppDaemon server.
|
||||||
app = object.__new__(BoilerClimate)
|
app = object.__new__(BoilerClimate)
|
||||||
app.base, app.boiler = 'appdaemon/test', 'boiler'
|
app.base, app.boiler = 'appdaemon/test', 'boiler'
|
||||||
|
app.entity_id = 'climate.radiator_heating'
|
||||||
app.mqtt_namespace = 'mqtt'
|
app.mqtt_namespace = 'mqtt'
|
||||||
app.connection_topic = app.base + '/connection'
|
app.connection_topic = app.base + '/connection'
|
||||||
app.mode, app.target = 'heat', 18
|
app.mode, app.target = 'heat', 18
|
||||||
@@ -86,6 +88,8 @@ class ClimateTests(unittest.TestCase):
|
|||||||
app._publish = Mock()
|
app._publish = Mock()
|
||||||
app._save = Mock()
|
app._save = Mock()
|
||||||
app.log = 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)
|
app.initialize_logger_interface(app, ascii_encode=False)
|
||||||
readings = {app.indoor: 18, app.outdoor: 10}
|
readings = {app.indoor: 18, app.outdoor: 10}
|
||||||
app._sensor_value = lambda entity: readings[entity]
|
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'), {})
|
a._mqtt_event('', dict(topic=a.base + '/command/mode', payload='off'), {})
|
||||||
self.assertEqual(self.writes(a)[0].args[1]['disablehc'], 1)
|
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):
|
def test_native_ha_template_payload_is_accepted(self):
|
||||||
a = self.app()
|
a = self.app()
|
||||||
a._mqtt_event('', dict(topic='boiler/bai/Statenumber', payload={'value': 30}), {})
|
a._mqtt_event('', dict(topic='boiler/bai/Statenumber', payload={'value': 30}), {})
|
||||||
|
|||||||
Reference in new issue
Block a user