From fbb2c10713db6ae686599cb2fa74f5439a5d981c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Sat, 1 Aug 2026 12:04:29 +0200 Subject: [PATCH] Add duration, fault and low battery sensor to homekit (#171205) Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../homekit_controller/binary_sensor.py | 127 +- .../components/homekit_controller/const.py | 3 + .../components/homekit_controller/number.py | 11 +- .../multi_valve_irrigation_control.json | 424 ++++++ .../snapshots/test_init.ambr | 1319 +++++++++++++++++ .../homekit_controller/test_binary_sensor.py | 237 ++- .../homekit_controller/test_number.py | 61 + 7 files changed, 2175 insertions(+), 7 deletions(-) create mode 100644 tests/components/homekit_controller/fixtures/multi_valve_irrigation_control.json diff --git a/homeassistant/components/homekit_controller/binary_sensor.py b/homeassistant/components/homekit_controller/binary_sensor.py index 4a19a271a5a1..91d48e875a70 100644 --- a/homeassistant/components/homekit_controller/binary_sensor.py +++ b/homeassistant/components/homekit_controller/binary_sensor.py @@ -1,22 +1,33 @@ -"""Support for Homekit motion sensors.""" +"""Support for HomeKit binary sensors.""" +from dataclasses import dataclass from typing import override -from aiohomekit.model.characteristics import CharacteristicsTypes +from aiohomekit.model.characteristics import Characteristic, CharacteristicsTypes from aiohomekit.model.services import Service, ServicesTypes from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, BinarySensorEntity, + BinarySensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory, Platform from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback +from homeassistant.helpers.typing import ConfigType from . import KNOWN_DEVICES from .connection import HKDevice -from .entity import HomeKitEntity +from .entity import CharacteristicEntity, HomeKitEntity +from .utils import folded_name + + +@dataclass(frozen=True) +class HomeKitBinarySensorEntityDescription(BinarySensorEntityDescription): + """Describes a HomeKit binary sensor.""" + + on_value: int | bool = 1 class HomeKitMotionSensor(HomeKitEntity, BinarySensorEntity): @@ -167,13 +178,64 @@ REJECT_CHAR_BY_TYPE = { ServicesTypes.BATTERY_SERVICE: CharacteristicsTypes.BATTERY_LEVEL, } +CHARACTERISTIC_BINARY_SENSORS: dict[str, HomeKitBinarySensorEntityDescription] = { + CharacteristicsTypes.STATUS_LO_BATT: HomeKitBinarySensorEntityDescription( + key=CharacteristicsTypes.STATUS_LO_BATT, + name="Low Battery", + device_class=BinarySensorDeviceClass.BATTERY, + entity_category=EntityCategory.DIAGNOSTIC, + ), + CharacteristicsTypes.STATUS_FAULT: HomeKitBinarySensorEntityDescription( + key=CharacteristicsTypes.STATUS_FAULT, + name="Problem", + device_class=BinarySensorDeviceClass.PROBLEM, + entity_category=EntityCategory.DIAGNOSTIC, + ), +} + + +class CharacteristicBinarySensor(CharacteristicEntity, BinarySensorEntity): + """Representation of a HomeKit binary sensor backed by a single characteristic.""" + + entity_description: HomeKitBinarySensorEntityDescription + + def __init__( + self, + conn: HKDevice, + info: ConfigType, + char: Characteristic, + description: HomeKitBinarySensorEntityDescription, + ) -> None: + """Initialise a HomeKit characteristic binary sensor.""" + self.entity_description = description + super().__init__(conn, info, char) + + @property + @override + def name(self) -> str: + """Return the name of the sensor.""" + if name := self.accessory.name: + return f"{name} {self.entity_description.name}" + return f"{self.entity_description.name}" + + @override + def get_characteristic_types(self) -> list[str]: + """Define the homekit characteristics the entity is tracking.""" + return [self._char.type] + + @property + @override + def is_on(self) -> bool: + """Return true if the binary sensor is on.""" + return self._char.value == self.entity_description.on_value + async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: - """Set up Homekit lighting.""" + """Set up HomeKit binary sensors.""" hkid: str = config_entry.data["AccessoryPairingID"] conn: HKDevice = hass.data[KNOWN_DEVICES][hkid] @@ -198,3 +260,60 @@ async def async_setup_entry( return True conn.add_listener(async_add_service) + + @callback + def async_add_characteristic(char: Characteristic) -> bool: + if char.service.type == ServicesTypes.BATTERY_SERVICE: + return False + if not (description := CHARACTERISTIC_BINARY_SENSORS.get(char.type)): + return False + if char.type == CharacteristicsTypes.STATUS_LO_BATT and ( + _should_skip_low_battery_characteristic(char) + ): + return False + + info = {"aid": char.service.accessory.aid, "iid": char.service.iid} + entity = CharacteristicBinarySensor(conn, info, char, description) + conn.async_migrate_unique_id( + entity.old_unique_id, entity.unique_id, Platform.BINARY_SENSOR + ) + async_add_entities([entity]) + return True + + conn.add_char_factory(async_add_characteristic) + + +def _should_skip_low_battery_characteristic(char: Characteristic) -> bool: + """Check if the low battery characteristic should not create an entity.""" + return char.service.accessory.services.first( + service_type=ServicesTypes.BATTERY_SERVICE + ) is not None or _has_earlier_low_battery_characteristic(char) + + +def _has_earlier_low_battery_characteristic(char: Characteristic) -> bool: + """Check if the accessory already exposed the same low battery source. + + Unscoped low battery characteristics are treated as accessory-level duplicates. + """ + source_key = _low_battery_source_key(char.service) + return any( + service.iid < char.service.iid + and service.has(char.type) + and _low_battery_source_key(service) == source_key + for service in char.service.accessory.services + ) + + +def _low_battery_source_key(service: Service) -> str | None: + """Return the low battery source key for the service.""" + if ( + service_label_index := service.value(CharacteristicsTypes.SERVICE_LABEL_INDEX) + ) is not None: + return f"label:{service.type}:{service_label_index}" + + service_name = service.value(CharacteristicsTypes.NAME) + if service_name is not None and folded_name(str(service_name)) != folded_name( + service.accessory.name + ): + return f"name:{folded_name(str(service_name))}" + return None diff --git a/homeassistant/components/homekit_controller/const.py b/homeassistant/components/homekit_controller/const.py index fdd34455486d..83569a98f022 100644 --- a/homeassistant/components/homekit_controller/const.py +++ b/homeassistant/components/homekit_controller/const.py @@ -81,6 +81,9 @@ CHARACTERISTIC_PLATFORMS = { CharacteristicsTypes.VENDOR_EVE_MOTION_DURATION: "number", CharacteristicsTypes.VENDOR_EVE_MOTION_SENSITIVITY: "number", CharacteristicsTypes.VENDOR_EVE_THERMO_VALVE_POSITION: "sensor", + CharacteristicsTypes.SET_DURATION: "number", + CharacteristicsTypes.STATUS_FAULT: "binary_sensor", + CharacteristicsTypes.STATUS_LO_BATT: "binary_sensor", CharacteristicsTypes.VENDOR_HAA_SETUP: "button", CharacteristicsTypes.VENDOR_HAA_UPDATE: "button", CharacteristicsTypes.VENDOR_KOOGEEK_REALTIME_ENERGY: "sensor", diff --git a/homeassistant/components/homekit_controller/number.py b/homeassistant/components/homekit_controller/number.py index e42da9b7393c..0b6c1666c276 100644 --- a/homeassistant/components/homekit_controller/number.py +++ b/homeassistant/components/homekit_controller/number.py @@ -12,11 +12,12 @@ from homeassistant.components.number import ( DEFAULT_MAX_VALUE, DEFAULT_MIN_VALUE, DEFAULT_STEP, + NumberDeviceClass, NumberEntity, NumberEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import EntityCategory, Platform +from homeassistant.const import EntityCategory, Platform, UnitOfTime from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.typing import ConfigType @@ -64,6 +65,14 @@ NUMBER_ENTITIES: dict[str, NumberEntityDescription] = { translation_key="sensitivity", entity_category=EntityCategory.CONFIG, ), + CharacteristicsTypes.SET_DURATION: NumberEntityDescription( + key=CharacteristicsTypes.SET_DURATION, + name="Duration", + device_class=NumberDeviceClass.DURATION, + translation_key="duration", + entity_category=EntityCategory.CONFIG, + native_unit_of_measurement=UnitOfTime.SECONDS, + ), } diff --git a/tests/components/homekit_controller/fixtures/multi_valve_irrigation_control.json b/tests/components/homekit_controller/fixtures/multi_valve_irrigation_control.json new file mode 100644 index 000000000000..c58a2968f216 --- /dev/null +++ b/tests/components/homekit_controller/fixtures/multi_valve_irrigation_control.json @@ -0,0 +1,424 @@ +[ + { + "aid": 1, + "services": [ + { + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 2, + "type": "00000014-0000-1000-8000-0026BB765291", + "format": "bool", + "perms": ["pw"] + }, + { + "iid": 3, + "type": "00000020-0000-1000-8000-0026BB765291", + "description": "Manufacturer", + "format": "string", + "value": "GARDENA", + "perms": ["pr"] + }, + { + "iid": 4, + "type": "00000021-0000-1000-8000-0026BB765291", + "description": "Model", + "format": "string", + "value": "Irrigation Control", + "perms": ["pr"] + }, + { + "iid": 5, + "type": "00000023-0000-1000-8000-0026BB765291", + "description": "Name", + "format": "string", + "value": "Irrigation Control 00000000", + "perms": ["pr"] + }, + { + "iid": 6, + "type": "00000030-0000-1000-8000-0026BB765291", + "description": "Serial Number", + "format": "string", + "value": "**REDACTED**", + "perms": ["pr"] + }, + { + "iid": 7, + "type": "00000052-0000-1000-8000-0026BB765291", + "description": "Firmware Revision", + "format": "string", + "value": "2.5.0", + "perms": ["pr"] + }, + { + "iid": 8, + "type": "00000053-0000-1000-8000-0026BB765291", + "description": "Hardware Revision", + "format": "string", + "value": "0.0.0", + "perms": ["pr"] + } + ] + }, + { + "iid": 256, + "type": "000000CF-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [512, 544, 576], + "characteristics": [ + { + "iid": 258, + "type": "00000023-0000-1000-8000-0026BB765291", + "description": "Name", + "format": "string", + "value": "Irrigation Control 00000000", + "perms": ["pr"] + }, + { + "iid": 259, + "type": "000000B0-0000-1000-8000-0026BB765291", + "description": "Active", + "format": "uint8", + "value": 0, + "minValue": 0, + "maxValue": 1, + "minStep": 1, + "perms": ["pr", "pw", "ev"] + }, + { + "iid": 260, + "type": "000000D1-0000-1000-8000-0026BB765291", + "description": "Program Mode", + "format": "uint8", + "value": 1, + "minValue": 0, + "maxValue": 2, + "minStep": 1, + "perms": ["pr", "ev"] + }, + { + "iid": 261, + "type": "000000D2-0000-1000-8000-0026BB765291", + "description": "In Use", + "format": "uint8", + "value": 0, + "minValue": 0, + "maxValue": 1, + "minStep": 1, + "perms": ["pr", "ev"] + }, + { + "iid": 262, + "type": "000000D4-0000-1000-8000-0026BB765291", + "description": "Remaining Duration", + "format": "uint32", + "value": 0, + "minValue": 0, + "maxValue": 36000, + "minStep": 1, + "perms": ["pr", "ev"] + }, + { + "iid": 263, + "type": "00000077-0000-1000-8000-0026BB765291", + "description": "Status Fault", + "format": "uint8", + "value": 0, + "minValue": 0, + "maxValue": 1, + "minStep": 1, + "perms": ["pr", "ev"] + } + ] + }, + { + "iid": 512, + "type": "000000D0-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 514, + "type": "00000023-0000-1000-8000-0026BB765291", + "description": "Name", + "format": "string", + "value": "Valve 1", + "perms": ["pr"] + }, + { + "iid": 515, + "type": "000000B0-0000-1000-8000-0026BB765291", + "description": "Active", + "format": "uint8", + "value": 0, + "minValue": 0, + "maxValue": 1, + "minStep": 1, + "perms": ["pr", "pw", "ev"] + }, + { + "iid": 516, + "type": "000000D2-0000-1000-8000-0026BB765291", + "description": "In Use", + "format": "uint8", + "value": 0, + "minValue": 0, + "maxValue": 1, + "minStep": 1, + "perms": ["pr", "ev"] + }, + { + "iid": 517, + "type": "000000D5-0000-1000-8000-0026BB765291", + "description": "Valve Type", + "format": "uint8", + "value": 1, + "minValue": 0, + "maxValue": 3, + "minStep": 1, + "perms": ["pr", "ev"] + }, + { + "iid": 518, + "type": "000000D3-0000-1000-8000-0026BB765291", + "description": "Set Duration", + "format": "uint32", + "value": 1200, + "minValue": 30, + "maxValue": 5400, + "minStep": 1, + "perms": ["pr", "pw", "ev"] + }, + { + "iid": 519, + "type": "000000D4-0000-1000-8000-0026BB765291", + "description": "Remaining Duration", + "format": "uint32", + "value": 0, + "minValue": 0, + "maxValue": 36000, + "minStep": 1, + "perms": ["pr", "ev"] + }, + { + "iid": 520, + "type": "000000CB-0000-1000-8000-0026BB765291", + "description": "Service Label Index", + "format": "uint8", + "value": 1, + "minValue": 1, + "maxValue": 6, + "minStep": 1, + "perms": ["pr"] + }, + { + "iid": 521, + "type": "00000077-0000-1000-8000-0026BB765291", + "description": "Status Fault", + "format": "uint8", + "value": 1, + "minValue": 0, + "maxValue": 1, + "minStep": 1, + "perms": ["pr", "ev"] + } + ] + }, + { + "iid": 544, + "type": "000000D0-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 546, + "type": "00000023-0000-1000-8000-0026BB765291", + "description": "Name", + "format": "string", + "value": "Valve 2", + "perms": ["pr"] + }, + { + "iid": 547, + "type": "000000B0-0000-1000-8000-0026BB765291", + "description": "Active", + "format": "uint8", + "value": 1, + "minValue": 0, + "maxValue": 1, + "minStep": 1, + "perms": ["pr", "pw", "ev"] + }, + { + "iid": 548, + "type": "000000D2-0000-1000-8000-0026BB765291", + "description": "In Use", + "format": "uint8", + "value": 1, + "minValue": 0, + "maxValue": 1, + "minStep": 1, + "perms": ["pr", "ev"] + }, + { + "iid": 549, + "type": "000000D5-0000-1000-8000-0026BB765291", + "description": "Valve Type", + "format": "uint8", + "value": 1, + "minValue": 0, + "maxValue": 3, + "minStep": 1, + "perms": ["pr", "ev"] + }, + { + "iid": 550, + "type": "000000D3-0000-1000-8000-0026BB765291", + "description": "Set Duration", + "format": "uint32", + "value": 1200, + "minValue": 30, + "maxValue": 5400, + "minStep": 1, + "perms": ["pr", "pw", "ev"] + }, + { + "iid": 551, + "type": "000000D4-0000-1000-8000-0026BB765291", + "description": "Remaining Duration", + "format": "uint32", + "value": 1163, + "minValue": 0, + "maxValue": 36000, + "minStep": 1, + "perms": ["pr", "ev"] + }, + { + "iid": 552, + "type": "000000CB-0000-1000-8000-0026BB765291", + "description": "Service Label Index", + "format": "uint8", + "value": 2, + "minValue": 1, + "maxValue": 6, + "minStep": 1, + "perms": ["pr"] + }, + { + "iid": 553, + "type": "00000077-0000-1000-8000-0026BB765291", + "description": "Status Fault", + "format": "uint8", + "value": 0, + "minValue": 0, + "maxValue": 1, + "minStep": 1, + "perms": ["pr", "ev"] + } + ] + }, + { + "iid": 576, + "type": "000000D0-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 578, + "type": "00000023-0000-1000-8000-0026BB765291", + "description": "Name", + "format": "string", + "value": "Valve 3", + "perms": ["pr"] + }, + { + "iid": 579, + "type": "000000B0-0000-1000-8000-0026BB765291", + "description": "Active", + "format": "uint8", + "value": 1, + "minValue": 0, + "maxValue": 1, + "minStep": 1, + "perms": ["pr", "pw", "ev"] + }, + { + "iid": 580, + "type": "000000D2-0000-1000-8000-0026BB765291", + "description": "In Use", + "format": "uint8", + "value": 1, + "minValue": 0, + "maxValue": 1, + "minStep": 1, + "perms": ["pr", "ev"] + }, + { + "iid": 581, + "type": "000000D5-0000-1000-8000-0026BB765291", + "description": "Valve Type", + "format": "uint8", + "value": 1, + "minValue": 0, + "maxValue": 3, + "minStep": 1, + "perms": ["pr", "ev"] + }, + { + "iid": 582, + "type": "000000D3-0000-1000-8000-0026BB765291", + "description": "Set Duration", + "format": "uint32", + "value": 1200, + "minValue": 30, + "maxValue": 5400, + "minStep": 1, + "perms": ["pr", "pw", "ev"] + }, + { + "iid": 583, + "type": "000000D4-0000-1000-8000-0026BB765291", + "description": "Remaining Duration", + "format": "uint32", + "value": 1166, + "minValue": 0, + "maxValue": 36000, + "minStep": 1, + "perms": ["pr", "ev"] + }, + { + "iid": 584, + "type": "000000CB-0000-1000-8000-0026BB765291", + "description": "Service Label Index", + "format": "uint8", + "value": 3, + "minValue": 1, + "maxValue": 6, + "minStep": 1, + "perms": ["pr"] + }, + { + "iid": 585, + "type": "00000077-0000-1000-8000-0026BB765291", + "description": "Status Fault", + "format": "uint8", + "value": 0, + "minValue": 0, + "maxValue": 1, + "minStep": 1, + "perms": ["pr", "ev"] + } + ] + } + ] + } +] diff --git a/tests/components/homekit_controller/snapshots/test_init.ambr b/tests/components/homekit_controller/snapshots/test_init.ambr index dac4c4d2a22b..e21e84376994 100644 --- a/tests/components/homekit_controller/snapshots/test_init.ambr +++ b/tests/components/homekit_controller/snapshots/test_init.ambr @@ -3412,6 +3412,51 @@ 'state': 'off', }), }), + dict({ + 'entry': EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.basement_low_battery', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Basement Low Battery', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Basement Low Battery', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_4_55_4113', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + : 'battery', + : 'Basement Low Battery', + }), + 'entity_id': 'binary_sensor.basement_low_battery', + 'state': 'off', + }), + }), dict({ 'entry': EntityRegistryEntrySnapshot({ 'aliases': list([ @@ -4079,6 +4124,51 @@ 'state': 'off', }), }), + dict({ + 'entry': EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.kitchen_low_battery', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Kitchen Low Battery', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Kitchen Low Battery', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_2_55_2065', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + : 'battery', + : 'Kitchen Low Battery', + }), + 'entity_id': 'binary_sensor.kitchen_low_battery', + 'state': 'off', + }), + }), dict({ 'entry': EntityRegistryEntrySnapshot({ 'aliases': list([ @@ -4253,6 +4343,51 @@ 'state': 'off', }), }), + dict({ + 'entry': EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.porch_low_battery', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Porch Low Battery', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Porch Low Battery', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_3_55_3089', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + : 'battery', + : 'Porch Low Battery', + }), + 'entity_id': 'binary_sensor.porch_low_battery', + 'state': 'off', + }), + }), dict({ 'entry': EntityRegistryEntrySnapshot({ 'aliases': list([ @@ -8511,6 +8646,51 @@ 'state': 'off', }), }), + dict({ + 'entry': EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.basement_low_battery', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Basement Low Battery', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Basement Low Battery', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_4_56_4109', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + : 'battery', + : 'Basement Low Battery', + }), + 'entity_id': 'binary_sensor.basement_low_battery', + 'state': 'off', + }), + }), dict({ 'entry': EntityRegistryEntrySnapshot({ 'aliases': list([ @@ -8937,6 +9117,51 @@ 'state': 'off', }), }), + dict({ + 'entry': EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.kitchen_low_battery', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Kitchen Low Battery', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Kitchen Low Battery', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_2_55_2065', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + : 'battery', + : 'Kitchen Low Battery', + }), + 'entity_id': 'binary_sensor.kitchen_low_battery', + 'state': 'off', + }), + }), dict({ 'entry': EntityRegistryEntrySnapshot({ 'aliases': list([ @@ -9111,6 +9336,51 @@ 'state': 'off', }), }), + dict({ + 'entry': EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.porch_low_battery', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Porch Low Battery', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Porch Low Battery', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_3_55_3089', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + : 'battery', + : 'Porch Low Battery', + }), + 'entity_id': 'binary_sensor.porch_low_battery', + 'state': 'off', + }), + }), dict({ 'entry': EntityRegistryEntrySnapshot({ 'aliases': list([ @@ -18944,6 +19214,570 @@ }), ]) # --- +# name: test_snapshots[multi_valve_irrigation_control] + list([ + dict({ + 'device': DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entry_id': , + 'config_subentry_id': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': '0.0.0', + 'id': , + 'identifiers': set({ + tuple( + 'homekit_controller:accessory-id', + '00:00:00:00:00:00:aid:1', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'GARDENA', + 'model': 'Irrigation Control', + 'model_id': None, + 'name': 'Irrigation Control 00000000', + 'name_by_user': None, + 'serial_number': '**REDACTED**', + 'sw_version': '2.5.0', + 'via_device_id': None, + }), + 'entities': list([ + dict({ + 'entry': EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.irrigation_control_00000000_problem', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Irrigation Control 00000000 Problem', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Irrigation Control 00000000 Problem', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_1_256_263', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + : 'problem', + : 'Irrigation Control 00000000 Problem', + }), + 'entity_id': 'binary_sensor.irrigation_control_00000000_problem', + 'state': 'off', + }), + }), + dict({ + 'entry': EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.irrigation_control_00000000_problem_2', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Irrigation Control 00000000 Problem', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Irrigation Control 00000000 Problem', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_1_512_521', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + : 'problem', + : 'Irrigation Control 00000000 Problem', + }), + 'entity_id': 'binary_sensor.irrigation_control_00000000_problem_2', + 'state': 'on', + }), + }), + dict({ + 'entry': EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.irrigation_control_00000000_problem_3', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Irrigation Control 00000000 Problem', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Irrigation Control 00000000 Problem', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_1_544_553', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + : 'problem', + : 'Irrigation Control 00000000 Problem', + }), + 'entity_id': 'binary_sensor.irrigation_control_00000000_problem_3', + 'state': 'off', + }), + }), + dict({ + 'entry': EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.irrigation_control_00000000_problem_4', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Irrigation Control 00000000 Problem', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Irrigation Control 00000000 Problem', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_1_576_585', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + : 'problem', + : 'Irrigation Control 00000000 Problem', + }), + 'entity_id': 'binary_sensor.irrigation_control_00000000_problem_4', + 'state': 'off', + }), + }), + dict({ + 'entry': EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.irrigation_control_00000000_identify', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Irrigation Control 00000000 Identify', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Irrigation Control 00000000 Identify', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_1_1_2', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + : 'identify', + : 'Irrigation Control 00000000 Identify', + }), + 'entity_id': 'button.irrigation_control_00000000_identify', + 'state': 'unknown', + }), + }), + dict({ + 'entry': EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : 5400, + : 30, + : , + : 1, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.irrigation_control_00000000_duration', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Irrigation Control 00000000 Duration', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Irrigation Control 00000000 Duration', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'duration', + 'unique_id': '00:00:00:00:00:00_1_512_518', + 'unit_of_measurement': , + }), + 'state': dict({ + 'attributes': dict({ + : 'duration', + : 'Irrigation Control 00000000 Duration', + : 5400, + : 30, + : , + : 1, + : , + }), + 'entity_id': 'number.irrigation_control_00000000_duration', + 'state': '1200', + }), + }), + dict({ + 'entry': EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : 5400, + : 30, + : , + : 1, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.irrigation_control_00000000_duration_2', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Irrigation Control 00000000 Duration', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Irrigation Control 00000000 Duration', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'duration', + 'unique_id': '00:00:00:00:00:00_1_544_550', + 'unit_of_measurement': , + }), + 'state': dict({ + 'attributes': dict({ + : 'duration', + : 'Irrigation Control 00000000 Duration', + : 5400, + : 30, + : , + : 1, + : , + }), + 'entity_id': 'number.irrigation_control_00000000_duration_2', + 'state': '1200', + }), + }), + dict({ + 'entry': EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : 5400, + : 30, + : , + : 1, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.irrigation_control_00000000_duration_3', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Irrigation Control 00000000 Duration', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Irrigation Control 00000000 Duration', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'duration', + 'unique_id': '00:00:00:00:00:00_1_576_582', + 'unit_of_measurement': , + }), + 'state': dict({ + 'attributes': dict({ + : 'duration', + : 'Irrigation Control 00000000 Duration', + : 5400, + : 30, + : , + : 1, + : , + }), + 'entity_id': 'number.irrigation_control_00000000_duration_3', + 'state': '1200', + }), + }), + dict({ + 'entry': EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.irrigation_control_00000000_valve_1', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Irrigation Control 00000000 Valve 1', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Irrigation Control 00000000 Valve 1', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'valve', + 'unique_id': '00:00:00:00:00:00_1_512', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + : 'Irrigation Control 00000000 Valve 1', + 'in_use': False, + 'remaining_duration': 0, + }), + 'entity_id': 'switch.irrigation_control_00000000_valve_1', + 'state': 'off', + }), + }), + dict({ + 'entry': EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.irrigation_control_00000000_valve_2', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Irrigation Control 00000000 Valve 2', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Irrigation Control 00000000 Valve 2', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'valve', + 'unique_id': '00:00:00:00:00:00_1_544', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + : 'Irrigation Control 00000000 Valve 2', + 'in_use': True, + 'remaining_duration': 1163, + }), + 'entity_id': 'switch.irrigation_control_00000000_valve_2', + 'state': 'on', + }), + }), + dict({ + 'entry': EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.irrigation_control_00000000_valve_3', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Irrigation Control 00000000 Valve 3', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Irrigation Control 00000000 Valve 3', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'valve', + 'unique_id': '00:00:00:00:00:00_1_576', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + : 'Irrigation Control 00000000 Valve 3', + 'in_use': True, + 'remaining_duration': 1166, + }), + 'entity_id': 'switch.irrigation_control_00000000_valve_3', + 'state': 'on', + }), + }), + ]), + }), + ]) +# --- # name: test_snapshots[mysa_living] list([ dict({ @@ -20018,6 +20852,51 @@ 'state': 'off', }), }), + dict({ + 'entry': EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.smart_co_alarm_problem', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Smart CO Alarm Problem', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Smart CO Alarm Problem', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_1_22_231', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + : 'problem', + : 'Smart CO Alarm Problem', + }), + 'entity_id': 'binary_sensor.smart_co_alarm_problem', + 'state': 'off', + }), + }), dict({ 'entry': EntityRegistryEntrySnapshot({ 'aliases': list([ @@ -20475,6 +21354,446 @@ 'state': 'unknown', }), }), + dict({ + 'entry': EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : 86400, + : 0.0, + : , + : 1, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.rainmachine_00ce4a_duration', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'RainMachine-00ce4a Duration', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'RainMachine-00ce4a Duration', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'duration', + 'unique_id': '00:00:00:00:00:00_1_512_624', + 'unit_of_measurement': , + }), + 'state': dict({ + 'attributes': dict({ + : 'duration', + : 'RainMachine-00ce4a Duration', + : 86400, + : 0.0, + : , + : 1, + : , + }), + 'entity_id': 'number.rainmachine_00ce4a_duration', + 'state': '300', + }), + }), + dict({ + 'entry': EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : 86400, + : 0.0, + : , + : 1, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.rainmachine_00ce4a_duration_2', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'RainMachine-00ce4a Duration', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'RainMachine-00ce4a Duration', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'duration', + 'unique_id': '00:00:00:00:00:00_1_768_880', + 'unit_of_measurement': , + }), + 'state': dict({ + 'attributes': dict({ + : 'duration', + : 'RainMachine-00ce4a Duration', + : 86400, + : 0.0, + : , + : 1, + : , + }), + 'entity_id': 'number.rainmachine_00ce4a_duration_2', + 'state': '300', + }), + }), + dict({ + 'entry': EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : 86400, + : 0.0, + : , + : 1, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.rainmachine_00ce4a_duration_3', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'RainMachine-00ce4a Duration', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'RainMachine-00ce4a Duration', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'duration', + 'unique_id': '00:00:00:00:00:00_1_1024_1136', + 'unit_of_measurement': , + }), + 'state': dict({ + 'attributes': dict({ + : 'duration', + : 'RainMachine-00ce4a Duration', + : 86400, + : 0.0, + : , + : 1, + : , + }), + 'entity_id': 'number.rainmachine_00ce4a_duration_3', + 'state': '300', + }), + }), + dict({ + 'entry': EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : 86400, + : 0.0, + : , + : 1, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.rainmachine_00ce4a_duration_4', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'RainMachine-00ce4a Duration', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'RainMachine-00ce4a Duration', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'duration', + 'unique_id': '00:00:00:00:00:00_1_1280_1392', + 'unit_of_measurement': , + }), + 'state': dict({ + 'attributes': dict({ + : 'duration', + : 'RainMachine-00ce4a Duration', + : 86400, + : 0.0, + : , + : 1, + : , + }), + 'entity_id': 'number.rainmachine_00ce4a_duration_4', + 'state': '300', + }), + }), + dict({ + 'entry': EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : 86400, + : 0.0, + : , + : 1, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.rainmachine_00ce4a_duration_5', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'RainMachine-00ce4a Duration', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'RainMachine-00ce4a Duration', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'duration', + 'unique_id': '00:00:00:00:00:00_1_1536_1648', + 'unit_of_measurement': , + }), + 'state': dict({ + 'attributes': dict({ + : 'duration', + : 'RainMachine-00ce4a Duration', + : 86400, + : 0.0, + : , + : 1, + : , + }), + 'entity_id': 'number.rainmachine_00ce4a_duration_5', + 'state': '300', + }), + }), + dict({ + 'entry': EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : 86400, + : 0.0, + : , + : 1, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.rainmachine_00ce4a_duration_6', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'RainMachine-00ce4a Duration', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'RainMachine-00ce4a Duration', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'duration', + 'unique_id': '00:00:00:00:00:00_1_1792_1904', + 'unit_of_measurement': , + }), + 'state': dict({ + 'attributes': dict({ + : 'duration', + : 'RainMachine-00ce4a Duration', + : 86400, + : 0.0, + : , + : 1, + : , + }), + 'entity_id': 'number.rainmachine_00ce4a_duration_6', + 'state': '300', + }), + }), + dict({ + 'entry': EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : 86400, + : 0.0, + : , + : 1, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.rainmachine_00ce4a_duration_7', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'RainMachine-00ce4a Duration', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'RainMachine-00ce4a Duration', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'duration', + 'unique_id': '00:00:00:00:00:00_1_2048_2160', + 'unit_of_measurement': , + }), + 'state': dict({ + 'attributes': dict({ + : 'duration', + : 'RainMachine-00ce4a Duration', + : 86400, + : 0.0, + : , + : 1, + : , + }), + 'entity_id': 'number.rainmachine_00ce4a_duration_7', + 'state': '300', + }), + }), + dict({ + 'entry': EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : 86400, + : 0.0, + : , + : 1, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.rainmachine_00ce4a_duration_8', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'RainMachine-00ce4a Duration', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'RainMachine-00ce4a Duration', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'duration', + 'unique_id': '00:00:00:00:00:00_1_2304_2416', + 'unit_of_measurement': , + }), + 'state': dict({ + 'attributes': dict({ + : 'duration', + : 'RainMachine-00ce4a Duration', + : 86400, + : 0.0, + : , + : 1, + : , + }), + 'entity_id': 'number.rainmachine_00ce4a_duration_8', + 'state': '300', + }), + }), dict({ 'entry': EntityRegistryEntrySnapshot({ 'aliases': list([ diff --git a/tests/components/homekit_controller/test_binary_sensor.py b/tests/components/homekit_controller/test_binary_sensor.py index a46d5eca2f5a..3686823361c3 100644 --- a/tests/components/homekit_controller/test_binary_sensor.py +++ b/tests/components/homekit_controller/test_binary_sensor.py @@ -4,13 +4,13 @@ from collections.abc import Callable from aiohomekit.model import Accessory from aiohomekit.model.characteristics import CharacteristicsTypes -from aiohomekit.model.services import ServicesTypes +from aiohomekit.model.services import Service, ServicesTypes from homeassistant.components.binary_sensor import BinarySensorDeviceClass from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from .common import setup_test_component +from .common import Helper, setup_test_accessories, setup_test_component def create_motion_sensor_service(accessory: Accessory) -> None: @@ -178,6 +178,112 @@ def create_leak_sensor_service(accessory: Accessory) -> None: cur_state.value = 0 +def create_valve_with_status_characteristics(accessory: Accessory) -> Service: + """Define valve characteristics with status binary sensors.""" + service = accessory.add_service(ServicesTypes.VALVE, name="TestDevice") + + active = service.add_char(CharacteristicsTypes.ACTIVE) + active.value = False + + low_battery = service.add_char(CharacteristicsTypes.STATUS_LO_BATT) + low_battery.value = 0 + + fault = service.add_char(CharacteristicsTypes.STATUS_FAULT) + fault.value = 0 + + return service + + +def create_sensor_with_duplicate_low_battery_characteristics( + accessory: Accessory, +) -> None: + """Define sensor services that repeat the same low battery status.""" + for service_type, characteristic_type in ( + (ServicesTypes.TEMPERATURE_SENSOR, CharacteristicsTypes.TEMPERATURE_CURRENT), + ( + ServicesTypes.HUMIDITY_SENSOR, + CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT, + ), + ): + service = accessory.add_service(service_type, name="Shared Sensor") + + current = service.add_char(characteristic_type) + current.value = 0 + + low_battery = service.add_char(CharacteristicsTypes.STATUS_LO_BATT) + low_battery.value = 0 + + +def create_sensor_with_unnamed_low_battery_characteristics( + accessory: Accessory, +) -> None: + """Define unnamed sensor services that repeat the same low battery status.""" + for service_type, characteristic_type in ( + (ServicesTypes.TEMPERATURE_SENSOR, CharacteristicsTypes.TEMPERATURE_CURRENT), + ( + ServicesTypes.HUMIDITY_SENSOR, + CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT, + ), + ): + service = accessory.add_service(service_type) + + current = service.add_char(characteristic_type) + current.value = 0 + + low_battery = service.add_char(CharacteristicsTypes.STATUS_LO_BATT) + low_battery.value = 0 + + +def create_sensor_with_named_low_battery_characteristic(accessory: Accessory) -> None: + """Define a named sensor service with low battery status.""" + service = accessory.add_service( + ServicesTypes.TEMPERATURE_SENSOR, name="Temperature" + ) + + current = service.add_char(CharacteristicsTypes.TEMPERATURE_CURRENT) + current.value = 0 + + low_battery = service.add_char(CharacteristicsTypes.STATUS_LO_BATT) + low_battery.value = 0 + + +def create_labeled_valves_with_low_battery_characteristics( + accessory: Accessory, +) -> None: + """Define labeled valve services with low battery status.""" + for label_index in (1.0, 2.0): + service = accessory.add_service(ServicesTypes.VALVE, name="Valve") + + service_label_index = service.add_char(CharacteristicsTypes.SERVICE_LABEL_INDEX) + service_label_index.value = label_index + + active = service.add_char(CharacteristicsTypes.ACTIVE) + active.value = False + + low_battery = service.add_char(CharacteristicsTypes.STATUS_LO_BATT) + low_battery.value = 0 + + +def create_sensor_with_battery_service(accessory: Accessory) -> None: + """Define a sensor with its own battery service.""" + service = accessory.add_service( + ServicesTypes.TEMPERATURE_SENSOR, name="Temperature" + ) + + current = service.add_char(CharacteristicsTypes.TEMPERATURE_CURRENT) + current.value = 0 + + low_battery = service.add_char(CharacteristicsTypes.STATUS_LO_BATT) + low_battery.value = 0 + + battery = accessory.add_service(ServicesTypes.BATTERY_SERVICE, name="Battery") + battery_level = battery.add_char(CharacteristicsTypes.BATTERY_LEVEL) + battery_level.value = 100 + + battery_low = battery.add_char(CharacteristicsTypes.STATUS_LO_BATT) + battery_low.value = 0 + + async def test_leak_sensor_read_state( hass: HomeAssistant, get_next_aid: Callable[[], int] ) -> None: @@ -201,6 +307,133 @@ async def test_leak_sensor_read_state( assert state.attributes["device_class"] == BinarySensorDeviceClass.MOISTURE +async def test_valve_status_binary_sensors( + hass: HomeAssistant, + get_next_aid: Callable[[], int], +) -> None: + """Test valve status characteristics are exposed as binary sensors.""" + helper = await setup_test_component( + hass, get_next_aid(), create_valve_with_status_characteristics + ) + + low_battery = Helper( + hass, + "binary_sensor.testdevice_low_battery", + helper.pairing, + helper.accessory, + helper.config_entry, + ) + fault = Helper( + hass, + "binary_sensor.testdevice_problem", + helper.pairing, + helper.accessory, + helper.config_entry, + ) + + state = await low_battery.poll_and_get_state() + assert state.state == "off" + assert state.attributes["device_class"] == BinarySensorDeviceClass.BATTERY + + state = await low_battery.async_update( + ServicesTypes.VALVE, + {CharacteristicsTypes.STATUS_LO_BATT: 1}, + ) + assert state.state == "on" + + state = await fault.poll_and_get_state() + assert state.state == "off" + assert state.attributes["device_class"] == BinarySensorDeviceClass.PROBLEM + + state = await fault.async_update( + ServicesTypes.VALVE, + {CharacteristicsTypes.STATUS_FAULT: 1}, + ) + assert state.state == "on" + + +async def test_duplicate_low_battery_characteristics_create_single_binary_sensor( + hass: HomeAssistant, + get_next_aid: Callable[[], int], +) -> None: + """Test repeated low battery characteristics on one sensor create one entity.""" + accessory = Accessory.create_with_info( + get_next_aid(), "Shared Sensor", "example.com", "Test", "0001", "0.1" + ) + create_sensor_with_duplicate_low_battery_characteristics(accessory) + + await setup_test_accessories(hass, [accessory]) + + low_battery = hass.states.get("binary_sensor.shared_sensor_low_battery") + assert low_battery + assert hass.states.get("binary_sensor.shared_sensor_low_battery_2") is None + + +async def test_unnamed_low_battery_characteristics_create_single_binary_sensor( + hass: HomeAssistant, + get_next_aid: Callable[[], int], +) -> None: + """Test unnamed low battery characteristics on one sensor create one entity.""" + accessory = Accessory.create_with_info( + get_next_aid(), "Unnamed Sensor", "example.com", "Test", "0001", "0.1" + ) + create_sensor_with_unnamed_low_battery_characteristics(accessory) + + await setup_test_accessories(hass, [accessory]) + + low_battery = hass.states.get("binary_sensor.unnamed_sensor_low_battery") + assert low_battery + assert hass.states.get("binary_sensor.unnamed_sensor_low_battery_2") is None + + +async def test_named_low_battery_characteristic_creates_binary_sensor( + hass: HomeAssistant, + get_next_aid: Callable[[], int], +) -> None: + """Test low battery characteristics on named services create an entity.""" + accessory = Accessory.create_with_info( + get_next_aid(), "Outdoor Sensor", "example.com", "Test", "0001", "0.1" + ) + create_sensor_with_named_low_battery_characteristic(accessory) + + await setup_test_accessories(hass, [accessory]) + + low_battery = hass.states.get("binary_sensor.outdoor_sensor_low_battery") + assert low_battery + + +async def test_labeled_low_battery_characteristics_create_binary_sensors( + hass: HomeAssistant, + get_next_aid: Callable[[], int], +) -> None: + """Test low battery characteristics on labeled services create entities.""" + await setup_test_component( + hass, get_next_aid(), create_labeled_valves_with_low_battery_characteristics + ) + + valve_1 = hass.states.get("binary_sensor.testdevice_low_battery") + assert valve_1 + + valve_2 = hass.states.get("binary_sensor.testdevice_low_battery_2") + assert valve_2 + + +async def test_low_battery_characteristic_ignored_with_battery_service( + hass: HomeAssistant, get_next_aid: Callable[[], int] +) -> None: + """Test low battery characteristics are ignored when a battery service exists.""" + accessory = Accessory.create_with_info( + get_next_aid(), "Outdoor Sensor", "example.com", "Test", "0001", "0.1" + ) + create_sensor_with_battery_service(accessory) + + await setup_test_accessories(hass, [accessory]) + + assert hass.states.get("sensor.outdoor_sensor_battery") + assert hass.states.get("binary_sensor.outdoor_sensor_battery") is None + assert hass.states.get("binary_sensor.outdoor_sensor_low_battery") is None + + async def test_migrate_unique_id( hass: HomeAssistant, entity_registry: er.EntityRegistry, diff --git a/tests/components/homekit_controller/test_number.py b/tests/components/homekit_controller/test_number.py index b476b4f294c7..f507e2bf4b6c 100644 --- a/tests/components/homekit_controller/test_number.py +++ b/tests/components/homekit_controller/test_number.py @@ -6,6 +6,8 @@ from aiohomekit.model import Accessory from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import Service, ServicesTypes +from homeassistant.components.number import NumberDeviceClass +from homeassistant.const import UnitOfTime from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -33,6 +35,22 @@ def create_switch_with_spray_level(accessory: Accessory) -> Service: return service +def create_valve_with_set_duration(accessory: Accessory) -> Service: + """Define valve characteristics with a set duration.""" + service = accessory.add_service(ServicesTypes.VALVE) + + active = service.add_char(CharacteristicsTypes.ACTIVE) + active.value = False + + set_duration = service.add_char(CharacteristicsTypes.SET_DURATION) + set_duration.value = 1200 + set_duration.minValue = 0 + set_duration.maxValue = 5400 + set_duration.minStep = 60 + + return service + + async def test_migrate_unique_id( hass: HomeAssistant, entity_registry: er.EntityRegistry, @@ -124,3 +142,46 @@ async def test_write_number( ServicesTypes.OUTLET, {CharacteristicsTypes.VENDOR_VOCOLINC_HUMIDIFIER_SPRAY_LEVEL: 3}, ) + + +async def test_valve_set_duration_number( + hass: HomeAssistant, + get_next_aid: Callable[[], int], +) -> None: + """Test a valve service set duration characteristic is correctly handled.""" + helper = await setup_test_component( + hass, get_next_aid(), create_valve_with_set_duration + ) + + set_duration = Helper( + hass, + "number.testdevice_duration", + helper.pairing, + helper.accessory, + helper.config_entry, + ) + + state = await set_duration.poll_and_get_state() + assert state.state == "1200" + assert state.attributes["device_class"] == NumberDeviceClass.DURATION + assert state.attributes["unit_of_measurement"] == UnitOfTime.SECONDS + assert state.attributes["step"] == 60 + assert state.attributes["min"] == 0 + assert state.attributes["max"] == 5400 + + state = await set_duration.async_update( + ServicesTypes.VALVE, + {CharacteristicsTypes.SET_DURATION: 1800}, + ) + assert state.state == "1800" + + await hass.services.async_call( + "number", + "set_value", + {"entity_id": "number.testdevice_duration", "value": 600}, + blocking=True, + ) + set_duration.async_assert_service_values( + ServicesTypes.VALVE, + {CharacteristicsTypes.SET_DURATION: 600}, + )