From 08787e61a392561d66337b4a74002ba2bc5fcc4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20K=C5=82ys?= Date: Mon, 6 Jul 2026 09:44:24 +0200 Subject: [PATCH 1/3] Surface ComfoConnect alarms in Home Assistant (#1) --- custom_components/comfoconnect/__init__.py | 38 ++++++++++++- .../comfoconnect/binary_sensor.py | 55 ++++++++++++++++++- 2 files changed, 91 insertions(+), 2 deletions(-) diff --git a/custom_components/comfoconnect/__init__.py b/custom_components/comfoconnect/__init__.py index 5c8a2ff..fe5658b 100644 --- a/custom_components/comfoconnect/__init__.py +++ b/custom_components/comfoconnect/__init__.py @@ -19,6 +19,7 @@ ) from aiocomfoconnect.sensors import Sensor from aiocomfoconnect.util import version_decode +from homeassistant.components import persistent_notification from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STOP, Platform from homeassistant.core import HomeAssistant, callback @@ -45,6 +46,9 @@ _LOGGER = logging.getLogger(__name__) SIGNAL_COMFOCONNECT_UPDATE_RECEIVED = "comfoconnect_update_{}_{}" +SIGNAL_COMFOCONNECT_ALARM_RECEIVED = "comfoconnect_alarm_{}" +EVENT_COMFOCONNECT_ALARM = "comfoconnect_alarm" +PERSISTENT_NOTIFICATION_ID = "comfoconnect_alarm_{}" KEEP_ALIVE_INTERVAL = timedelta(seconds=30) @@ -191,6 +195,8 @@ def __init__(self, hass: HomeAssistant, host: str, uuid: str): self.alarm_callback, ) self.hass = hass + self.active_alarm_node_id: int | None = None + self.active_alarms: dict[int, str] = {} @callback def sensor_callback(self, sensor: Sensor, value): @@ -203,8 +209,38 @@ def sensor_callback(self, sensor: Sensor, value): @callback def alarm_callback(self, node_id, errors): - """Print alarm updates.""" + """Handle alarm updates.""" + self.active_alarm_node_id = node_id + self.active_alarms = errors + + event_data = { + "bridge_uuid": self.uuid, + "node_id": node_id, + "errors": [{"id": error_id, "message": error} for error_id, error in errors.items()], + } + + dispatcher_send( + self.hass, + SIGNAL_COMFOCONNECT_ALARM_RECEIVED.format(self.uuid), + node_id, + errors, + ) + self.hass.bus.async_fire(EVENT_COMFOCONNECT_ALARM, event_data) + + notification_id = PERSISTENT_NOTIFICATION_ID.format(self.uuid) + if not errors: + _LOGGER.info("Alarms cleared for Node %s", node_id) + persistent_notification.async_dismiss(self.hass, notification_id) + return + message = f"Alarm received for Node {node_id}:\n" for error_id, error in errors.items(): message += f"* {error_id}: {error}\n" + _LOGGER.warning(message) + persistent_notification.async_create( + self.hass, + message, + title="ComfoConnect alarm", + notification_id=notification_id, + ) diff --git a/custom_components/comfoconnect/binary_sensor.py b/custom_components/comfoconnect/binary_sensor.py index 93fd87d..d957815 100644 --- a/custom_components/comfoconnect/binary_sensor.py +++ b/custom_components/comfoconnect/binary_sensor.py @@ -16,6 +16,7 @@ Sensor as AioComfoConnectSensor, ) from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, BinarySensorEntity, BinarySensorEntityDescription, ) @@ -25,7 +26,7 @@ from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import DOMAIN, SIGNAL_COMFOCONNECT_UPDATE_RECEIVED, ComfoConnectBridge +from . import DOMAIN, SIGNAL_COMFOCONNECT_ALARM_RECEIVED, SIGNAL_COMFOCONNECT_UPDATE_RECEIVED, ComfoConnectBridge _LOGGER = logging.getLogger(__name__) @@ -83,6 +84,7 @@ async def async_setup_entry( ccb = hass.data[DOMAIN][config_entry.entry_id] sensors = [ComfoConnectBinarySensor(ccb=ccb, config_entry=config_entry, description=description) for description in SENSOR_TYPES] + sensors.append(ComfoConnectAlarmBinarySensor(ccb=ccb)) async_add_entities(sensors, True) @@ -136,3 +138,54 @@ def _handle_update(self, value): self._attr_is_on = True if value else False self.schedule_update_ha_state() + + +class ComfoConnectAlarmBinarySensor(BinarySensorEntity): + """Representation of active ComfoConnect alarms.""" + + _attr_device_class = BinarySensorDeviceClass.PROBLEM + _attr_entity_category = EntityCategory.DIAGNOSTIC + _attr_has_entity_name = True + _attr_name = "Active alarms" + _attr_should_poll = False + + def __init__(self, ccb: ComfoConnectBridge) -> None: + """Initialize the ComfoConnect alarm sensor.""" + self._ccb = ccb + self._node_id = ccb.active_alarm_node_id + self._errors = ccb.active_alarms + self._attr_unique_id = f"{self._ccb.uuid}-active_alarms" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self._ccb.uuid)}, + ) + self._update_state() + + async def async_added_to_hass(self) -> None: + """Register for alarm updates.""" + self.async_on_remove( + async_dispatcher_connect( + self.hass, + SIGNAL_COMFOCONNECT_ALARM_RECEIVED.format(self._ccb.uuid), + self._handle_update, + ) + ) + + @property + def extra_state_attributes(self) -> dict: + """Return alarm details.""" + return { + "node_id": self._node_id, + "alarm_count": len(self._errors), + "alarms": [{"id": error_id, "message": error} for error_id, error in self._errors.items()], + } + + def _handle_update(self, node_id: int, errors: dict[int, str]) -> None: + """Handle alarm update callbacks.""" + self._node_id = node_id + self._errors = errors + self._update_state() + self.schedule_update_ha_state() + + def _update_state(self) -> None: + """Update the active alarm state.""" + self._attr_is_on = bool(self._errors) From 3ec377a07e658501ce361da048a2fe7fb0ea38af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20K=C5=82ys?= Date: Mon, 6 Jul 2026 09:56:20 +0200 Subject: [PATCH 2/3] Fix fan not connected exception import (#2) --- custom_components/comfoconnect/fan.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/custom_components/comfoconnect/fan.py b/custom_components/comfoconnect/fan.py index 88314c5..7b14562 100644 --- a/custom_components/comfoconnect/fan.py +++ b/custom_components/comfoconnect/fan.py @@ -6,7 +6,7 @@ from typing import Any from aiocomfoconnect.const import VentilationMode, VentilationSpeed -from aiocomfoconnect.exceptions import ComfoConnectNotConnected, ComfoConnectRmiError +from aiocomfoconnect.exceptions import AioComfoConnectNotConnected, ComfoConnectRmiError from aiocomfoconnect.sensors import ( SENSOR_FAN_SPEED_MODE, SENSOR_OPERATING_MODE, @@ -150,7 +150,7 @@ async def async_set_percentage(self, percentage: int) -> None: try: await self._ccb.set_speed(speed) - except ComfoConnectNotConnected as err: + except AioComfoConnectNotConnected as err: raise HomeAssistantError(f"Not connected to ComfoConnect bridge: {err}") from err except ComfoConnectRmiError as err: raise HomeAssistantError(f"Failed to set fan speed: {err}") from err @@ -163,7 +163,7 @@ async def async_set_preset_mode(self, preset_mode: str) -> None: _LOGGER.debug("Changing preset mode to %s", preset_mode) try: await self._ccb.set_mode(preset_mode) - except ComfoConnectNotConnected as err: + except AioComfoConnectNotConnected as err: raise HomeAssistantError(f"Not connected to ComfoConnect bridge: {err}") from err except ComfoConnectRmiError as err: raise HomeAssistantError(f"Failed to set preset mode: {err}") from err From 300c50422e37d3a842866243666ae5b885ff7ce8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20K=C5=82ys?= Date: Mon, 6 Jul 2026 11:16:52 +0200 Subject: [PATCH 3/3] Avoid warning log entries for ComfoConnect alarms (#3) * Demote ComfoConnect alarm log level * Polish ComfoConnect alarm notifications * Format alarm notification polish --- custom_components/comfoconnect/__init__.py | 27 ++++++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/custom_components/comfoconnect/__init__.py b/custom_components/comfoconnect/__init__.py index fe5658b..859a650 100644 --- a/custom_components/comfoconnect/__init__.py +++ b/custom_components/comfoconnect/__init__.py @@ -49,6 +49,7 @@ SIGNAL_COMFOCONNECT_ALARM_RECEIVED = "comfoconnect_alarm_{}" EVENT_COMFOCONNECT_ALARM = "comfoconnect_alarm" PERSISTENT_NOTIFICATION_ID = "comfoconnect_alarm_{}" +ALARM_RECHECK_ERROR_ID = 100 KEEP_ALIVE_INTERVAL = timedelta(seconds=30) @@ -233,14 +234,30 @@ def alarm_callback(self, node_id, errors): persistent_notification.async_dismiss(self.hass, notification_id) return - message = f"Alarm received for Node {node_id}:\n" - for error_id, error in errors.items(): - message += f"* {error_id}: {error}\n" + title, message = self._format_alarm_notification(node_id, errors) - _LOGGER.warning(message) + _LOGGER.info(message) persistent_notification.async_create( self.hass, message, - title="ComfoConnect alarm", + title=title, notification_id=notification_id, ) + + @staticmethod + def _format_alarm_notification(node_id: int, errors: dict[int, str]) -> tuple[str, str]: + """Format active alarms for Home Assistant notifications.""" + is_recheck = set(errors) == {ALARM_RECHECK_ERROR_ID} + title = "ComfoConnect is checking alarms" if is_recheck else "ComfoConnect needs attention" + intro = "The ventilation unit is checking whether alarms are still active." if is_recheck else "The ventilation unit reported active alarms." + alarm_lines = [f"- **{error_id}**: {error}" for error_id, error in errors.items()] + message = "\n".join( + [ + intro, + "", + f"Node: {node_id}", + "", + *alarm_lines, + ] + ) + return title, message