Add duration, fault and low battery sensor to homekit (#171205)

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Michal Čihař
2026-08-01 12:04:29 +02:00
committed by GitHub
parent 00db8bfbc6
commit fbb2c10713
7 changed files with 2175 additions and 7 deletions
@@ -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
@@ -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",
@@ -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,
),
}
@@ -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"]
}
]
}
]
}
]
File diff suppressed because it is too large Load Diff
@@ -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,
@@ -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},
)