From a19b1c02d5c691c1422cf7d8b311ff7bf29a0c80 Mon Sep 17 00:00:00 2001 From: kongo09 Date: Sun, 17 Mar 2024 22:52:00 +0100 Subject: [PATCH] introduce binary sensor for humidification and water error --- .../philips_airpurifier_coap/__init__.py | 4 +- .../philips_airpurifier_coap/binary_sensor.py | 114 ++++++++++++++++++ .../philips_airpurifier_coap/const.py | 46 +++++++ .../philips_airpurifier_coap/philips.py | 3 + 4 files changed, 165 insertions(+), 2 deletions(-) create mode 100644 custom_components/philips_airpurifier_coap/binary_sensor.py diff --git a/custom_components/philips_airpurifier_coap/__init__.py b/custom_components/philips_airpurifier_coap/__init__.py index c432e26..04f3550 100644 --- a/custom_components/philips_airpurifier_coap/__init__.py +++ b/custom_components/philips_airpurifier_coap/__init__.py @@ -36,7 +36,7 @@ _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["fan", "sensor", "switch", "light", "select", "number"] +PLATFORMS = ["fan", "binary_sensor", "sensor", "switch", "light", "select", "number"] # icons code thanks to Thomas Loven: @@ -117,7 +117,7 @@ async def async_get_mac_address_from_host(hass: HomeAssistant, host: str) -> str ) if not mac_address: return None - + return format_mac(mac_address) diff --git a/custom_components/philips_airpurifier_coap/binary_sensor.py b/custom_components/philips_airpurifier_coap/binary_sensor.py new file mode 100644 index 0000000..a8690aa --- /dev/null +++ b/custom_components/philips_airpurifier_coap/binary_sensor.py @@ -0,0 +1,114 @@ +"""Philips Air Purifier & Humidifier Binary Sensors.""" +from __future__ import annotations + +from collections.abc import Callable +import logging +from typing import Any, cast + +from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + CONF_ENTITY_CATEGORY, + CONF_HOST, + CONF_NAME, +) +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import PlatformNotReady +from homeassistant.helpers.entity import Entity + +from .const import ( + BINARY_SENSOR_TYPES, + CONF_MODEL, + DATA_KEY_COORDINATOR, + DOMAIN, + FanAttributes, + PhilipsApi, +) +from .philips import Coordinator, PhilipsEntity, model_to_class + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( # noqa: D103 + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: Callable[[list[Entity], bool], None], +) -> None: + _LOGGER.debug("async_setup_entry called for platform binary_sensor") + + host = entry.data[CONF_HOST] + model = entry.data[CONF_MODEL] + name = entry.data[CONF_NAME] + + data = hass.data[DOMAIN][host] + + coordinator = data[DATA_KEY_COORDINATOR] + status = coordinator.status + + model_class = model_to_class.get(model) + available_binary_sensors = [] + + if model_class: + for cls in reversed(model_class.__mro__): + cls_available_binary_sensors = getattr(cls, "AVAILABLE_BINARY_SENSORS", []) + available_binary_sensors.extend(cls_available_binary_sensors) + + binary_sensors = [] + + for binary_sensor in BINARY_SENSOR_TYPES: + if binary_sensor in status and binary_sensor in available_binary_sensors: + binary_sensors.append( + PhilipsBinarySensor(coordinator, name, model, binary_sensor) + ) + + async_add_entities(binary_sensors, update_before_add=False) + + +class PhilipsBinarySensor(PhilipsEntity, BinarySensorEntity): + """Define a Philips AirPurifier binary_sensor.""" + + def __init__( # noqa: D107 + self, coordinator: Coordinator, name: str, model: str, kind: str + ) -> None: + super().__init__(coordinator) + self._model = model + self._description = BINARY_SENSOR_TYPES[kind] + self._icon_map = self._description.get(FanAttributes.ICON_MAP) + self._norm_icon = ( + next(iter(self._icon_map.items()))[1] + if self._icon_map is not None + else None + ) + self._attr_device_class = self._description.get(ATTR_DEVICE_CLASS) + self._attr_entity_category = self._description.get(CONF_ENTITY_CATEGORY) + self._attr_name = ( + f"{name} {self._description[FanAttributes.LABEL].replace('_', ' ').title()}" + ) + + try: + device_id = self._device_status[PhilipsApi.DEVICE_ID] + self._attr_unique_id = f"{self._model}-{device_id}-{kind.lower()}" + except Exception as e: + _LOGGER.error("Failed retrieving unique_id: %s", e) + raise PlatformNotReady + self._attrs: dict[str, Any] = {} + self.kind = kind + + @property + def is_on(self) -> bool: + """Return the state of the binary sensor.""" + value = self._device_status[self.kind] + convert = self._description.get(FanAttributes.VALUE) + if convert: + value = convert(value) + return cast(bool, value) + + @property + def icon(self) -> str: + """Return the icon of the binary sensor.""" + icon = self._norm_icon + if not self._icon_map: + return icon + + return self._icon_map[self.is_on] diff --git a/custom_components/philips_airpurifier_coap/const.py b/custom_components/philips_airpurifier_coap/const.py index edbcc2f..482ef36 100644 --- a/custom_components/philips_airpurifier_coap/const.py +++ b/custom_components/philips_airpurifier_coap/const.py @@ -223,6 +223,7 @@ class FanAttributes(StrEnum): FILTER_NANOPROTECT_CLEAN = "pre_filter" FUNCTION = "function" HUMIDITY = "humidity" + HUMIDIFIER = "humidification" HUMIDITY_TARGET = "humidity_target" INDOOR_ALLERGEN_INDEX = "indoor_allergen_index" LABEL = "label" @@ -262,6 +263,7 @@ class FanAttributes(StrEnum): TARGET_TEMP = "target_temperature" STANDBY_SENSORS = "standby_sensors" AUTO_PLUS = "auto_plus" + WATER_TANK = "water_tank" class FanUnits(StrEnum): @@ -596,6 +598,50 @@ class PhilipsApi: # }, } +BINARY_SENSOR_TYPES: dict[str, SensorDescription] = { + # binary device sensors + PhilipsApi.ERROR_CODE: { + # test for out of water error, which is in bit 9 of the error number + FanAttributes.ICON_MAP: { + True: "mdi:water", + False: "mdi:water-off", + }, + FanAttributes.LABEL: FanAttributes.WATER_TANK, + ATTR_DEVICE_CLASS: SensorDeviceClass.MOISTURE, + FanAttributes.VALUE: lambda value: not value & (1 << 8), + CONF_ENTITY_CATEGORY: EntityCategory.DIAGNOSTIC, + }, + PhilipsApi.NEW2_ERROR_CODE: { + # test for out of water error, which is in bit 9 of the error number + FanAttributes.ICON_MAP: { + True: "mdi:water", + False: "mdi:water-off", + }, + FanAttributes.LABEL: FanAttributes.WATER_TANK, + ATTR_DEVICE_CLASS: SensorDeviceClass.MOISTURE, + FanAttributes.VALUE: lambda value: not value & (1 << 8), + CONF_ENTITY_CATEGORY: EntityCategory.DIAGNOSTIC, + }, + PhilipsApi.FUNCTION: { + # test if the water container is available and thus humidification switched on + FanAttributes.ICON_MAP: { + True: PhilipsApi.FUNCTION_MAP["PH"][1], + False: PhilipsApi.FUNCTION_MAP["P"][1], + }, + FanAttributes.LABEL: FanAttributes.HUMIDIFIER, + FanAttributes.VALUE: lambda value: value == "PH", + }, + PhilipsApi.NEW2_MODE_A: { + # test if the water container is available and thus humidification switched on + FanAttributes.ICON_MAP: { + True: PhilipsApi.FUNCTION_MAP["PH"][1], + False: PhilipsApi.FUNCTION_MAP["P"][1], + }, + FanAttributes.LABEL: FanAttributes.HUMIDIFIER, + FanAttributes.VALUE: lambda value: value == 4, + }, +} + FILTER_TYPES: dict[str, FilterDescription] = { PhilipsApi.FILTER_PRE: { FanAttributes.ICON_MAP: {0: ICON.FILTER_REPLACEMENT, 72: "mdi:dots-grid"}, diff --git a/custom_components/philips_airpurifier_coap/philips.py b/custom_components/philips_airpurifier_coap/philips.py index f65eb9e..153b3ee 100644 --- a/custom_components/philips_airpurifier_coap/philips.py +++ b/custom_components/philips_airpurifier_coap/philips.py @@ -293,6 +293,7 @@ class PhilipsGenericCoAPFanBase(PhilipsGenericFan): AVAILABLE_SWITCHES = [] AVAILABLE_LIGHTS = [] AVAILABLE_NUMBERS = [] + AVAILABLE_BINARY_SENSORS = [] KEY_PHILIPS_POWER = PhilipsApi.POWER STATE_POWER_ON = "1" @@ -635,6 +636,7 @@ class PhilipsHumidifierMixin(PhilipsGenericCoAPFanBase): """Mixin for humidifiers.""" AVAILABLE_SELECTS = [PhilipsApi.FUNCTION, PhilipsApi.HUMIDITY_TARGET] + AVAILABLE_BINARY_SENSORS = [PhilipsApi.ERROR_CODE] # similar to the AC1715, the AC0850 seems to be a new class of devices that @@ -1225,6 +1227,7 @@ class PhilipsAC3737(PhilipsNew2GenericCoAPFan): AVAILABLE_LIGHTS = [PhilipsApi.NEW2_DISPLAY_BACKLIGHT2] AVAILABLE_SWITCHES = [PhilipsApi.NEW2_CHILD_LOCK] UNAVAILABLE_SENSORS = [PhilipsApi.NEW2_FAN_SPEED] + AVAILABLE_BINARY_SENSORS = [PhilipsApi.NEW2_ERROR_CODE, PhilipsApi.NEW2_MODE_A] class PhilipsAC3829(PhilipsHumidifierMixin, PhilipsGenericCoAPFan):