the boiler app now use mqtt instead of relying on an automation on ha side

This commit is contained in:
Pierre committed 2026-10-10 16:19:37 +02:00
1 parent a2f40f81ba
commit 6447d754ca
8 files changed
+261 -77

No files matched your search

+34 -14
View File
@@ -1,8 +1,4 @@
"""MQTT-discovered boiler climate; MQTT transport is Home Assistant's service.
Incoming MQTT is forwarded by the companion Home Assistant automation. All
callbacks run on the app's pinned AppDaemon thread; no broker credentials.
"""
"""MQTT-discovered climate using AppDaemon's MQTT plugin and HA sensors."""
from datetime import datetime, timezone
import json
from pathlib import Path
@@ -16,12 +12,22 @@ from logger_interface import LoggerInterface
class BoilerClimate(hass.Hass, LoggerInterface):
def initialize(self):
self.initialize_logger_interface(self.get_ad_api(), ascii_encode=False)
self.mqtt_namespace = self.args.get('mqtt_namespace', 'mqtt')
if self.AD.plugins.get_plugin_object(self.mqtt_namespace) is None:
raise RuntimeError('MQTT plugin missing; configure it and restart AppDaemon')
self.base = self.args.get('mqtt_topic', 'appdaemon/radiator_heating').rstrip('/')
self.boiler = self.args.get('boiler_topic', 'themaplus-condens').rstrip('/')
self.connection_topic = self.args.get('mqtt_connection_topic', self.base + '/connection')
self.uid = self.args.get('unique_id', 'appdaemon_radiator_heating')
self.title = self.args.get('display_name', 'Chauffage radiateurs')
self.indoor = self.args['indoor_sensor']
self.outdoor = self.args['outdoor_sensor']
outdoors = self.args.get('outdoor_sensors')
if outdoors is None:
outdoors = [self.args['outdoor_sensor']]
if not isinstance(outdoors, list) or not outdoors or any(not isinstance(e, str) or not e for e in outdoors):
raise ValueError('outdoor_sensors must be a non-empty list of entity IDs')
self.outdoors = tuple(dict.fromkeys(outdoors))
self.selected_outdoor = None
self.interval = finite(self.args.get('command_interval', 60))
if not 10 <= self.interval <= 300:
raise ValueError('command_interval must be between 10 and 300 seconds')
@@ -43,9 +49,10 @@ class BoilerClimate(hass.Hass, LoggerInterface):
self.sensor_error = None
self.last_status_time = 0.0
self._state = None
self.listen_event(self._mqtt_event, 'APPDAEMON_BOILER_MQTT')
self.listen_event(self._mqtt_event, 'MQTT_MESSAGE', namespace=self.mqtt_namespace)
self.listen_state(self._sensor_changed, self.indoor, attribute='all')
self.listen_state(self._sensor_changed, self.outdoor, attribute='all')
for entity in self.outdoors:
self.listen_state(self._sensor_changed, entity, attribute='all')
self._discovery()
self.run_every(self._tick, 'now', self.interval)
self._control()
@@ -54,12 +61,15 @@ class BoilerClimate(hass.Hass, LoggerInterface):
def _publish(self, topic, payload, retain=False):
if not isinstance(payload, str):
payload = json.dumps(payload, allow_nan=False, ensure_ascii=False)
self.call_service('mqtt/publish', topic=topic, payload=payload, qos=0, retain=retain)
self.call_service('mqtt/publish', topic=topic, payload=payload, qos=0,
retain=retain, namespace=self.mqtt_namespace)
def _discovery(self):
device = dict(identifiers=[self.uid], name=self.title,
manufacturer='AppDaemon', model='Weather compensation + room PI')
common = dict(device=device, availability_topic=self.base + '/availability')
common = dict(device=device, availability=[{'topic': self.base + '/availability'},
{'topic': self.connection_topic}],
availability_mode='all')
config = dict(common, name=None, unique_id=self.uid,
default_entity_id='climate.radiator_heating',
modes=['off', 'heat'], temperature_unit='C', precision=0.1,
@@ -147,6 +157,11 @@ class BoilerClimate(hass.Hass, LoggerInterface):
def _mqtt_event(self, event, data, kwargs):
topic, payload = data.get('topic'), data.get('payload', '')
if topic is None:
if data.get('state') == 'Connected':
self._discovery()
self._control(force=True)
return
if topic == 'homeassistant/status' and payload == 'online':
self._discovery()
self._control()
@@ -201,7 +216,8 @@ class BoilerClimate(hass.Hass, LoggerInterface):
def _attributes(self):
self._publish(self.base + '/attributes', dict(
indoor_sensor=self.indoor, outdoor_sensor=self.outdoor,
indoor_sensor=self.indoor, outdoor_sensors=list(self.outdoors),
outdoor_sensor=self.selected_outdoor,
commands_enabled=self.send_commands,
sensor_error=getattr(self, 'sensor_error', None),
last_command_at=self.last_command_at, last_c6_echo_at=self.last_echo_at,
@@ -215,16 +231,20 @@ class BoilerClimate(hass.Hass, LoggerInterface):
indoor = outdoor = None
result = None
readings, errors = {}, []
for entity in (self.indoor, self.outdoor):
sensors = (self.indoor, *self.outdoors)
for entity in sensors:
try:
readings[entity] = self._sensor_value(entity)
except (ValueError, TypeError, KeyError) as exc:
errors.append(str(exc))
indoor, outdoor = readings.get(self.indoor), readings.get(self.outdoor)
indoor = readings.get(self.indoor)
self.selected_outdoor = (min(self.outdoors, key=readings.__getitem__)
if all(entity in readings for entity in self.outdoors) else None)
outdoor = readings.get(self.selected_outdoor)
valid = not errors
sensor_error = '; '.join(errors) if errors else None
# Log once per set of failing sensors, not every minute as age increases.
signature = tuple((entity, entity in readings) for entity in (self.indoor, self.outdoor))
signature = tuple((entity, entity in readings) for entity in sensors)
previous = getattr(self, '_sensor_fault_signature', None)
if not valid and signature != previous:
self.log_error(f'Heating inhibited: sensor fault: {sensor_error}')