Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 58 additions & 5 deletions custom_components/comfoconnect/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -45,6 +46,10 @@
_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_{}"
ALARM_RECHECK_ERROR_ID = 100

KEEP_ALIVE_INTERVAL = timedelta(seconds=30)

Expand Down Expand Up @@ -191,6 +196,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):
Expand All @@ -203,8 +210,54 @@ def sensor_callback(self, sensor: Sensor, value):

@callback
def alarm_callback(self, node_id, errors):
"""Print alarm updates."""
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)
"""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

title, message = self._format_alarm_notification(node_id, errors)

_LOGGER.info(message)
persistent_notification.async_create(
self.hass,
message,
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
55 changes: 54 additions & 1 deletion custom_components/comfoconnect/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
Sensor as AioComfoConnectSensor,
)
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
BinarySensorEntityDescription,
)
Expand All @@ -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__)

Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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)
6 changes: 3 additions & 3 deletions custom_components/comfoconnect/fan.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -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