From 1ea9b1a158d1aaf356101edb587260a9ec164fe1 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Wed, 7 Feb 2024 15:19:42 +0000 Subject: [PATCH] Add support for air purifiers to HomeKit Device (#109880) --- .../components/homekit_controller/const.py | 3 + .../components/homekit_controller/fan.py | 1 + .../components/homekit_controller/select.py | 15 +- .../components/homekit_controller/sensor.py | 29 +++- .../homekit_controller/strings.json | 13 ++ .../snapshots/test_init.ambr | 136 ++++++++++++++++++ 6 files changed, 192 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/homekit_controller/const.py b/homeassistant/components/homekit_controller/const.py index 939657eb8a5..aea5a6661ee 100644 --- a/homeassistant/components/homekit_controller/const.py +++ b/homeassistant/components/homekit_controller/const.py @@ -55,6 +55,7 @@ HOMEKIT_ACCESSORY_DISPATCH = { ServicesTypes.DOORBELL: "event", ServicesTypes.STATELESS_PROGRAMMABLE_SWITCH: "event", ServicesTypes.SERVICE_LABEL: "event", + ServicesTypes.AIR_PURIFIER: "fan", } CHARACTERISTIC_PLATFORMS = { @@ -104,6 +105,8 @@ CHARACTERISTIC_PLATFORMS = { CharacteristicsTypes.FILTER_LIFE_LEVEL: "sensor", CharacteristicsTypes.VENDOR_AIRVERSA_SLEEP_MODE: "switch", CharacteristicsTypes.TEMPERATURE_UNITS: "select", + CharacteristicsTypes.AIR_PURIFIER_STATE_CURRENT: "sensor", + CharacteristicsTypes.AIR_PURIFIER_STATE_TARGET: "select", } STARTUP_EXCEPTIONS = ( diff --git a/homeassistant/components/homekit_controller/fan.py b/homeassistant/components/homekit_controller/fan.py index d87b6ab3e39..1b2d572f2b6 100644 --- a/homeassistant/components/homekit_controller/fan.py +++ b/homeassistant/components/homekit_controller/fan.py @@ -206,6 +206,7 @@ class HomeKitFanV2(BaseHomeKitFan): ENTITY_TYPES = { ServicesTypes.FAN: HomeKitFanV1, ServicesTypes.FAN_V2: HomeKitFanV2, + ServicesTypes.AIR_PURIFIER: HomeKitFanV2, } diff --git a/homeassistant/components/homekit_controller/select.py b/homeassistant/components/homekit_controller/select.py index e6eae1c51ca..c3185bcba55 100644 --- a/homeassistant/components/homekit_controller/select.py +++ b/homeassistant/components/homekit_controller/select.py @@ -5,7 +5,10 @@ from dataclasses import dataclass from enum import IntEnum from aiohomekit.model.characteristics import Characteristic, CharacteristicsTypes -from aiohomekit.model.characteristics.const import TemperatureDisplayUnits +from aiohomekit.model.characteristics.const import ( + TargetAirPurifierStateValues, + TemperatureDisplayUnits, +) from homeassistant.components.select import SelectEntity, SelectEntityDescription from homeassistant.config_entries import ConfigEntry @@ -47,6 +50,16 @@ SELECT_ENTITIES: dict[str, HomeKitSelectEntityDescription] = { "fahrenheit": TemperatureDisplayUnits.FAHRENHEIT, }, ), + CharacteristicsTypes.AIR_PURIFIER_STATE_TARGET: HomeKitSelectEntityDescription( + key="air_purifier_state_target", + translation_key="air_purifier_state_target", + name="Air Purifier Mode", + entity_category=EntityCategory.CONFIG, + choices={ + "automatic": TargetAirPurifierStateValues.AUTOMATIC, + "manual": TargetAirPurifierStateValues.MANUAL, + }, + ), } _ECOBEE_MODE_TO_TEXT = { diff --git a/homeassistant/components/homekit_controller/sensor.py b/homeassistant/components/homekit_controller/sensor.py index ebfba110e48..26476417a56 100644 --- a/homeassistant/components/homekit_controller/sensor.py +++ b/homeassistant/components/homekit_controller/sensor.py @@ -3,10 +3,15 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass +from enum import IntEnum from aiohomekit.model import Accessory, Transport from aiohomekit.model.characteristics import Characteristic, CharacteristicsTypes -from aiohomekit.model.characteristics.const import ThreadNodeCapabilities, ThreadStatus +from aiohomekit.model.characteristics.const import ( + CurrentAirPurifierStateValues, + ThreadNodeCapabilities, + ThreadStatus, +) from aiohomekit.model.services import Service, ServicesTypes from homeassistant.components.bluetooth import ( @@ -52,6 +57,7 @@ class HomeKitSensorEntityDescription(SensorEntityDescription): probe: Callable[[Characteristic], bool] | None = None format: Callable[[Characteristic], str] | None = None + enum: dict[IntEnum, str] | None = None def thread_node_capability_to_str(char: Characteristic) -> str: @@ -324,6 +330,18 @@ SIMPLE_SENSOR: dict[str, HomeKitSensorEntityDescription] = { ], translation_key="thread_status", ), + CharacteristicsTypes.AIR_PURIFIER_STATE_CURRENT: HomeKitSensorEntityDescription( + key=CharacteristicsTypes.AIR_PURIFIER_STATE_CURRENT, + name="Air Purifier Status", + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.ENUM, + enum={ + CurrentAirPurifierStateValues.INACTIVE: "inactive", + CurrentAirPurifierStateValues.IDLE: "idle", + CurrentAirPurifierStateValues.ACTIVE: "purifying", + }, + translation_key="air_purifier_state_current", + ), CharacteristicsTypes.VENDOR_NETATMO_NOISE: HomeKitSensorEntityDescription( key=CharacteristicsTypes.VENDOR_NETATMO_NOISE, name="Noise", @@ -535,6 +553,8 @@ class SimpleSensor(CharacteristicEntity, SensorEntity): ) -> None: """Initialise a secondary HomeKit characteristic sensor.""" self.entity_description = description + if self.entity_description.enum: + self._attr_options = list(self.entity_description.enum.values()) super().__init__(conn, info, char) def get_characteristic_types(self) -> list[str]: @@ -551,10 +571,11 @@ class SimpleSensor(CharacteristicEntity, SensorEntity): @property def native_value(self) -> str | int | float: """Return the current sensor value.""" - val = self._char.value + if self.entity_description.enum: + return self.entity_description.enum[self._char.value] if self.entity_description.format: - return self.entity_description.format(val) - return val + return self.entity_description.format(self._char) + return self._char.value ENTITY_TYPES = { diff --git a/homeassistant/components/homekit_controller/strings.json b/homeassistant/components/homekit_controller/strings.json index 998c375aac1..d1205645fd3 100644 --- a/homeassistant/components/homekit_controller/strings.json +++ b/homeassistant/components/homekit_controller/strings.json @@ -108,6 +108,12 @@ "celsius": "Celsius", "fahrenheit": "Fahrenheit" } + }, + "air_purifier_state_target": { + "state": { + "automatic": "Automatic", + "manual": "Manual" + } } }, "sensor": { @@ -131,6 +137,13 @@ "leader": "Leader", "router": "Router" } + }, + "air_purifier_state_current": { + "state": { + "inactive": "Inactive", + "idle": "Idle", + "purifying": "Purifying" + } } } } diff --git a/tests/components/homekit_controller/snapshots/test_init.ambr b/tests/components/homekit_controller/snapshots/test_init.ambr index 29b71d18422..1007bd70370 100644 --- a/tests/components/homekit_controller/snapshots/test_init.ambr +++ b/tests/components/homekit_controller/snapshots/test_init.ambr @@ -101,6 +101,142 @@ 'state': 'unknown', }), }), + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'preset_modes': None, + }), + 'config_entry_id': 'TestData', + 'device_class': None, + 'disabled_by': None, + 'domain': 'fan', + 'entity_category': None, + 'entity_id': 'fan.airversa_ap2_1808_airpurifier', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Airversa AP2 1808 AirPurifier', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_1_32832', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'friendly_name': 'Airversa AP2 1808 AirPurifier', + 'percentage': 0, + 'percentage_step': 20.0, + 'preset_mode': None, + 'preset_modes': None, + 'supported_features': , + }), + 'entity_id': 'fan.airversa_ap2_1808_airpurifier', + 'state': 'off', + }), + }), + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'automatic', + 'manual', + ]), + }), + 'config_entry_id': 'TestData', + 'device_class': None, + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.airversa_ap2_1808_air_purifier_mode', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Airversa AP2 1808 Air Purifier Mode', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'air_purifier_state_target', + 'unique_id': '00:00:00:00:00:00_1_32832_32837', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'friendly_name': 'Airversa AP2 1808 Air Purifier Mode', + 'options': list([ + 'automatic', + 'manual', + ]), + }), + 'entity_id': 'select.airversa_ap2_1808_air_purifier_mode', + 'state': 'automatic', + }), + }), + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'inactive', + 'idle', + 'purifying', + ]), + }), + 'config_entry_id': 'TestData', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.airversa_ap2_1808_air_purifier_status', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Airversa AP2 1808 Air Purifier Status', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'air_purifier_state_current', + 'unique_id': '00:00:00:00:00:00_1_32832_32836', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'enum', + 'friendly_name': 'Airversa AP2 1808 Air Purifier Status', + 'options': list([ + 'inactive', + 'idle', + 'purifying', + ]), + }), + 'entity_id': 'sensor.airversa_ap2_1808_air_purifier_status', + 'state': 'inactive', + }), + }), dict({ 'entry': dict({ 'aliases': list([