mirror of
https://github.com/home-assistant/core.git
synced 2026-08-03 20:24:55 +02:00
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:
@@ -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},
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user