Cache homekit_controller supported features (#106702)

This commit is contained in:
J. Nick Koston 2024-01-04 10:31:09 -10:00 committed by GitHub
parent f2514c0bde
commit bc26377c16
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 4588 additions and 192 deletions

View file

@ -2,7 +2,7 @@
from __future__ import annotations from __future__ import annotations
import logging import logging
from typing import Any, Final from typing import TYPE_CHECKING, Any, Final
from aiohomekit.model.characteristics import ( from aiohomekit.model.characteristics import (
ActivationStateValues, ActivationStateValues,
@ -48,6 +48,12 @@ from . import KNOWN_DEVICES
from .connection import HKDevice from .connection import HKDevice
from .entity import HomeKitEntity from .entity import HomeKitEntity
if TYPE_CHECKING:
from functools import cached_property
else:
from homeassistant.backports.functools import cached_property
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
# Map of Homekit operation modes to hass modes # Map of Homekit operation modes to hass modes
@ -134,6 +140,12 @@ class HomeKitBaseClimateEntity(HomeKitEntity, ClimateEntity):
_attr_temperature_unit = UnitOfTemperature.CELSIUS _attr_temperature_unit = UnitOfTemperature.CELSIUS
@callback
def _async_reconfigure(self) -> None:
"""Reconfigure entity."""
self._async_clear_property_cache(("supported_features", "fan_modes"))
super()._async_reconfigure()
def get_characteristic_types(self) -> list[str]: def get_characteristic_types(self) -> list[str]:
"""Define the homekit characteristics the entity cares about.""" """Define the homekit characteristics the entity cares about."""
return [ return [
@ -146,7 +158,7 @@ class HomeKitBaseClimateEntity(HomeKitEntity, ClimateEntity):
"""Return the current temperature.""" """Return the current temperature."""
return self.service.value(CharacteristicsTypes.TEMPERATURE_CURRENT) return self.service.value(CharacteristicsTypes.TEMPERATURE_CURRENT)
@property @cached_property
def fan_modes(self) -> list[str] | None: def fan_modes(self) -> list[str] | None:
"""Return the available fan modes.""" """Return the available fan modes."""
if self.service.has(CharacteristicsTypes.FAN_STATE_TARGET): if self.service.has(CharacteristicsTypes.FAN_STATE_TARGET):
@ -165,7 +177,7 @@ class HomeKitBaseClimateEntity(HomeKitEntity, ClimateEntity):
{CharacteristicsTypes.FAN_STATE_TARGET: int(fan_mode == FAN_AUTO)} {CharacteristicsTypes.FAN_STATE_TARGET: int(fan_mode == FAN_AUTO)}
) )
@property @cached_property
def supported_features(self) -> ClimateEntityFeature: def supported_features(self) -> ClimateEntityFeature:
"""Return the list of supported features.""" """Return the list of supported features."""
features = ClimateEntityFeature(0) features = ClimateEntityFeature(0)
@ -179,6 +191,12 @@ class HomeKitBaseClimateEntity(HomeKitEntity, ClimateEntity):
class HomeKitHeaterCoolerEntity(HomeKitBaseClimateEntity): class HomeKitHeaterCoolerEntity(HomeKitBaseClimateEntity):
"""Representation of a Homekit climate device.""" """Representation of a Homekit climate device."""
@callback
def _async_reconfigure(self) -> None:
"""Reconfigure entity."""
self._async_clear_property_cache(("hvac_modes", "swing_modes"))
super()._async_reconfigure()
def get_characteristic_types(self) -> list[str]: def get_characteristic_types(self) -> list[str]:
"""Define the homekit characteristics the entity cares about.""" """Define the homekit characteristics the entity cares about."""
return super().get_characteristic_types() + [ return super().get_characteristic_types() + [
@ -197,7 +215,7 @@ class HomeKitHeaterCoolerEntity(HomeKitBaseClimateEntity):
rotation_speed.maxValue or 100 rotation_speed.maxValue or 100
) )
@property @cached_property
def fan_modes(self) -> list[str]: def fan_modes(self) -> list[str]:
"""Return the available fan modes.""" """Return the available fan modes."""
return [FAN_OFF, FAN_LOW, FAN_MEDIUM, FAN_HIGH] return [FAN_OFF, FAN_LOW, FAN_MEDIUM, FAN_HIGH]
@ -388,7 +406,7 @@ class HomeKitHeaterCoolerEntity(HomeKitBaseClimateEntity):
value = self.service.value(CharacteristicsTypes.TARGET_HEATER_COOLER_STATE) value = self.service.value(CharacteristicsTypes.TARGET_HEATER_COOLER_STATE)
return TARGET_HEATER_COOLER_STATE_HOMEKIT_TO_HASS[value] return TARGET_HEATER_COOLER_STATE_HOMEKIT_TO_HASS[value]
@property @cached_property
def hvac_modes(self) -> list[HVACMode]: def hvac_modes(self) -> list[HVACMode]:
"""Return the list of available hvac operation modes.""" """Return the list of available hvac operation modes."""
valid_values = clamp_enum_to_char( valid_values = clamp_enum_to_char(
@ -410,7 +428,7 @@ class HomeKitHeaterCoolerEntity(HomeKitBaseClimateEntity):
value = self.service.value(CharacteristicsTypes.SWING_MODE) value = self.service.value(CharacteristicsTypes.SWING_MODE)
return SWING_MODE_HOMEKIT_TO_HASS[value] return SWING_MODE_HOMEKIT_TO_HASS[value]
@property @cached_property
def swing_modes(self) -> list[str]: def swing_modes(self) -> list[str]:
"""Return the list of available swing modes. """Return the list of available swing modes.
@ -428,7 +446,7 @@ class HomeKitHeaterCoolerEntity(HomeKitBaseClimateEntity):
{CharacteristicsTypes.SWING_MODE: SWING_MODE_HASS_TO_HOMEKIT[swing_mode]} {CharacteristicsTypes.SWING_MODE: SWING_MODE_HASS_TO_HOMEKIT[swing_mode]}
) )
@property @cached_property
def supported_features(self) -> ClimateEntityFeature: def supported_features(self) -> ClimateEntityFeature:
"""Return the list of supported features.""" """Return the list of supported features."""
features = super().supported_features features = super().supported_features
@ -451,6 +469,12 @@ class HomeKitHeaterCoolerEntity(HomeKitBaseClimateEntity):
class HomeKitClimateEntity(HomeKitBaseClimateEntity): class HomeKitClimateEntity(HomeKitBaseClimateEntity):
"""Representation of a Homekit climate device.""" """Representation of a Homekit climate device."""
@callback
def _async_reconfigure(self) -> None:
"""Reconfigure entity."""
self._async_clear_property_cache(("hvac_modes",))
super()._async_reconfigure()
def get_characteristic_types(self) -> list[str]: def get_characteristic_types(self) -> list[str]:
"""Define the homekit characteristics the entity cares about.""" """Define the homekit characteristics the entity cares about."""
return super().get_characteristic_types() + [ return super().get_characteristic_types() + [
@ -483,7 +507,7 @@ class HomeKitClimateEntity(HomeKitBaseClimateEntity):
if ( if (
(mode == HVACMode.HEAT_COOL) (mode == HVACMode.HEAT_COOL)
and ( and (
ClimateEntityFeature.TARGET_TEMPERATURE_RANGE & self.supported_features ClimateEntityFeature.TARGET_TEMPERATURE_RANGE in self.supported_features
) )
and heat_temp and heat_temp
and cool_temp and cool_temp
@ -524,9 +548,8 @@ class HomeKitClimateEntity(HomeKitBaseClimateEntity):
value = self.service.value(CharacteristicsTypes.HEATING_COOLING_TARGET) value = self.service.value(CharacteristicsTypes.HEATING_COOLING_TARGET)
if (MODE_HOMEKIT_TO_HASS.get(value) in {HVACMode.HEAT, HVACMode.COOL}) or ( if (MODE_HOMEKIT_TO_HASS.get(value) in {HVACMode.HEAT, HVACMode.COOL}) or (
(MODE_HOMEKIT_TO_HASS.get(value) in {HVACMode.HEAT_COOL}) (MODE_HOMEKIT_TO_HASS.get(value) in {HVACMode.HEAT_COOL})
and not ( and ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
ClimateEntityFeature.TARGET_TEMPERATURE_RANGE & self.supported_features not in self.supported_features
)
): ):
return self.service.value(CharacteristicsTypes.TEMPERATURE_TARGET) return self.service.value(CharacteristicsTypes.TEMPERATURE_TARGET)
return None return None
@ -536,7 +559,7 @@ class HomeKitClimateEntity(HomeKitBaseClimateEntity):
"""Return the highbound target temperature we try to reach.""" """Return the highbound target temperature we try to reach."""
value = self.service.value(CharacteristicsTypes.HEATING_COOLING_TARGET) value = self.service.value(CharacteristicsTypes.HEATING_COOLING_TARGET)
if (MODE_HOMEKIT_TO_HASS.get(value) in {HVACMode.HEAT_COOL}) and ( if (MODE_HOMEKIT_TO_HASS.get(value) in {HVACMode.HEAT_COOL}) and (
ClimateEntityFeature.TARGET_TEMPERATURE_RANGE & self.supported_features ClimateEntityFeature.TARGET_TEMPERATURE_RANGE in self.supported_features
): ):
return self.service.value( return self.service.value(
CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD
@ -548,7 +571,7 @@ class HomeKitClimateEntity(HomeKitBaseClimateEntity):
"""Return the lowbound target temperature we try to reach.""" """Return the lowbound target temperature we try to reach."""
value = self.service.value(CharacteristicsTypes.HEATING_COOLING_TARGET) value = self.service.value(CharacteristicsTypes.HEATING_COOLING_TARGET)
if (MODE_HOMEKIT_TO_HASS.get(value) in {HVACMode.HEAT_COOL}) and ( if (MODE_HOMEKIT_TO_HASS.get(value) in {HVACMode.HEAT_COOL}) and (
ClimateEntityFeature.TARGET_TEMPERATURE_RANGE & self.supported_features ClimateEntityFeature.TARGET_TEMPERATURE_RANGE in self.supported_features
): ):
return self.service.value( return self.service.value(
CharacteristicsTypes.TEMPERATURE_HEATING_THRESHOLD CharacteristicsTypes.TEMPERATURE_HEATING_THRESHOLD
@ -560,7 +583,7 @@ class HomeKitClimateEntity(HomeKitBaseClimateEntity):
"""Return the minimum target temp.""" """Return the minimum target temp."""
value = self.service.value(CharacteristicsTypes.HEATING_COOLING_TARGET) value = self.service.value(CharacteristicsTypes.HEATING_COOLING_TARGET)
if (MODE_HOMEKIT_TO_HASS.get(value) in {HVACMode.HEAT_COOL}) and ( if (MODE_HOMEKIT_TO_HASS.get(value) in {HVACMode.HEAT_COOL}) and (
ClimateEntityFeature.TARGET_TEMPERATURE_RANGE & self.supported_features ClimateEntityFeature.TARGET_TEMPERATURE_RANGE in self.supported_features
): ):
min_temp = self.service[ min_temp = self.service[
CharacteristicsTypes.TEMPERATURE_HEATING_THRESHOLD CharacteristicsTypes.TEMPERATURE_HEATING_THRESHOLD
@ -582,7 +605,7 @@ class HomeKitClimateEntity(HomeKitBaseClimateEntity):
"""Return the maximum target temp.""" """Return the maximum target temp."""
value = self.service.value(CharacteristicsTypes.HEATING_COOLING_TARGET) value = self.service.value(CharacteristicsTypes.HEATING_COOLING_TARGET)
if (MODE_HOMEKIT_TO_HASS.get(value) in {HVACMode.HEAT_COOL}) and ( if (MODE_HOMEKIT_TO_HASS.get(value) in {HVACMode.HEAT_COOL}) and (
ClimateEntityFeature.TARGET_TEMPERATURE_RANGE & self.supported_features ClimateEntityFeature.TARGET_TEMPERATURE_RANGE in self.supported_features
): ):
max_temp = self.service[ max_temp = self.service[
CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD
@ -656,7 +679,7 @@ class HomeKitClimateEntity(HomeKitBaseClimateEntity):
value = self.service.value(CharacteristicsTypes.HEATING_COOLING_TARGET) value = self.service.value(CharacteristicsTypes.HEATING_COOLING_TARGET)
return MODE_HOMEKIT_TO_HASS[value] return MODE_HOMEKIT_TO_HASS[value]
@property @cached_property
def hvac_modes(self) -> list[HVACMode]: def hvac_modes(self) -> list[HVACMode]:
"""Return the list of available hvac operation modes.""" """Return the list of available hvac operation modes."""
valid_values = clamp_enum_to_char( valid_values = clamp_enum_to_char(
@ -665,7 +688,7 @@ class HomeKitClimateEntity(HomeKitBaseClimateEntity):
) )
return [MODE_HOMEKIT_TO_HASS[mode] for mode in valid_values] return [MODE_HOMEKIT_TO_HASS[mode] for mode in valid_values]
@property @cached_property
def supported_features(self) -> ClimateEntityFeature: def supported_features(self) -> ClimateEntityFeature:
"""Return the list of supported features.""" """Return the list of supported features."""
features = super().supported_features features = super().supported_features

View file

@ -1,7 +1,7 @@
"""Support for Homekit covers.""" """Support for Homekit covers."""
from __future__ import annotations from __future__ import annotations
from typing import Any from typing import TYPE_CHECKING, Any
from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.characteristics import CharacteristicsTypes
from aiohomekit.model.services import Service, ServicesTypes from aiohomekit.model.services import Service, ServicesTypes
@ -28,6 +28,12 @@ from . import KNOWN_DEVICES
from .connection import HKDevice from .connection import HKDevice
from .entity import HomeKitEntity from .entity import HomeKitEntity
if TYPE_CHECKING:
from functools import cached_property
else:
from homeassistant.backports.functools import cached_property
STATE_STOPPED = "stopped" STATE_STOPPED = "stopped"
CURRENT_GARAGE_STATE_MAP = { CURRENT_GARAGE_STATE_MAP = {
@ -128,6 +134,12 @@ class HomeKitGarageDoorCover(HomeKitEntity, CoverEntity):
class HomeKitWindowCover(HomeKitEntity, CoverEntity): class HomeKitWindowCover(HomeKitEntity, CoverEntity):
"""Representation of a HomeKit Window or Window Covering.""" """Representation of a HomeKit Window or Window Covering."""
@callback
def _async_reconfigure(self) -> None:
"""Reconfigure entity."""
self._async_clear_property_cache(("supported_features",))
super()._async_reconfigure()
def get_characteristic_types(self) -> list[str]: def get_characteristic_types(self) -> list[str]:
"""Define the homekit characteristics the entity cares about.""" """Define the homekit characteristics the entity cares about."""
return [ return [
@ -142,7 +154,7 @@ class HomeKitWindowCover(HomeKitEntity, CoverEntity):
CharacteristicsTypes.OBSTRUCTION_DETECTED, CharacteristicsTypes.OBSTRUCTION_DETECTED,
] ]
@property @cached_property
def supported_features(self) -> CoverEntityFeature: def supported_features(self) -> CoverEntityFeature:
"""Flag supported features.""" """Flag supported features."""
features = ( features = (

View file

@ -1,6 +1,7 @@
"""Homekit Controller entities.""" """Homekit Controller entities."""
from __future__ import annotations from __future__ import annotations
import contextlib
from typing import Any from typing import Any
from aiohomekit.model.characteristics import ( from aiohomekit.model.characteristics import (
@ -74,6 +75,16 @@ class HomeKitEntity(Entity):
if not self._async_remove_entity_if_accessory_or_service_disappeared(): if not self._async_remove_entity_if_accessory_or_service_disappeared():
self._async_reconfigure() self._async_reconfigure()
@callback
def _async_clear_property_cache(self, properties: tuple[str, ...]) -> None:
"""Clear the cache of properties."""
for prop in properties:
# suppress is slower than try-except-pass, but
# we do not expect to have many properties to clear
# or this to be called often.
with contextlib.suppress(AttributeError):
delattr(self, prop)
@callback @callback
def _async_reconfigure(self) -> None: def _async_reconfigure(self) -> None:
"""Reconfigure the entity.""" """Reconfigure the entity."""

View file

@ -1,7 +1,7 @@
"""Support for Homekit fans.""" """Support for Homekit fans."""
from __future__ import annotations from __future__ import annotations
from typing import Any from typing import TYPE_CHECKING, Any
from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.characteristics import CharacteristicsTypes
from aiohomekit.model.services import Service, ServicesTypes from aiohomekit.model.services import Service, ServicesTypes
@ -25,6 +25,12 @@ from . import KNOWN_DEVICES
from .connection import HKDevice from .connection import HKDevice
from .entity import HomeKitEntity from .entity import HomeKitEntity
if TYPE_CHECKING:
from functools import cached_property
else:
from homeassistant.backports.functools import cached_property
# 0 is clockwise, 1 is counter-clockwise. The match to forward and reverse is so that # 0 is clockwise, 1 is counter-clockwise. The match to forward and reverse is so that
# its consistent with homeassistant.components.homekit. # its consistent with homeassistant.components.homekit.
DIRECTION_TO_HK = { DIRECTION_TO_HK = {
@ -41,6 +47,20 @@ class BaseHomeKitFan(HomeKitEntity, FanEntity):
# that controls whether the fan is on or off. # that controls whether the fan is on or off.
on_characteristic: str on_characteristic: str
@callback
def _async_reconfigure(self) -> None:
"""Reconfigure entity."""
self._async_clear_property_cache(
(
"_speed_range",
"_min_speed",
"_max_speed",
"speed_count",
"supported_features",
)
)
super()._async_reconfigure()
def get_characteristic_types(self) -> list[str]: def get_characteristic_types(self) -> list[str]:
"""Define the homekit characteristics the entity cares about.""" """Define the homekit characteristics the entity cares about."""
return [ return [
@ -55,19 +75,19 @@ class BaseHomeKitFan(HomeKitEntity, FanEntity):
"""Return true if device is on.""" """Return true if device is on."""
return self.service.value(self.on_characteristic) == 1 return self.service.value(self.on_characteristic) == 1
@property @cached_property
def _speed_range(self) -> tuple[int, int]: def _speed_range(self) -> tuple[int, int]:
"""Return the speed range.""" """Return the speed range."""
return (self._min_speed, self._max_speed) return (self._min_speed, self._max_speed)
@property @cached_property
def _min_speed(self) -> int: def _min_speed(self) -> int:
"""Return the minimum speed.""" """Return the minimum speed."""
return ( return (
round(self.service[CharacteristicsTypes.ROTATION_SPEED].minValue or 0) + 1 round(self.service[CharacteristicsTypes.ROTATION_SPEED].minValue or 0) + 1
) )
@property @cached_property
def _max_speed(self) -> int: def _max_speed(self) -> int:
"""Return the minimum speed.""" """Return the minimum speed."""
return round(self.service[CharacteristicsTypes.ROTATION_SPEED].maxValue or 100) return round(self.service[CharacteristicsTypes.ROTATION_SPEED].maxValue or 100)
@ -94,7 +114,7 @@ class BaseHomeKitFan(HomeKitEntity, FanEntity):
oscillating = self.service.value(CharacteristicsTypes.SWING_MODE) oscillating = self.service.value(CharacteristicsTypes.SWING_MODE)
return oscillating == 1 return oscillating == 1
@property @cached_property
def supported_features(self) -> FanEntityFeature: def supported_features(self) -> FanEntityFeature:
"""Flag supported features.""" """Flag supported features."""
features = FanEntityFeature(0) features = FanEntityFeature(0)
@ -110,7 +130,7 @@ class BaseHomeKitFan(HomeKitEntity, FanEntity):
return features return features
@property @cached_property
def speed_count(self) -> int: def speed_count(self) -> int:
"""Speed count for the fan.""" """Speed count for the fan."""
return round( return round(
@ -157,7 +177,7 @@ class BaseHomeKitFan(HomeKitEntity, FanEntity):
if ( if (
percentage is not None percentage is not None
and self.supported_features & FanEntityFeature.SET_SPEED and FanEntityFeature.SET_SPEED in self.supported_features
): ):
characteristics[CharacteristicsTypes.ROTATION_SPEED] = round( characteristics[CharacteristicsTypes.ROTATION_SPEED] = round(
percentage_to_ranged_value(self._speed_range, percentage) percentage_to_ranged_value(self._speed_range, percentage)

View file

@ -1,7 +1,7 @@
"""Support for HomeKit Controller humidifier.""" """Support for HomeKit Controller humidifier."""
from __future__ import annotations from __future__ import annotations
from typing import Any from typing import TYPE_CHECKING, Any
from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.characteristics import CharacteristicsTypes
from aiohomekit.model.services import Service, ServicesTypes from aiohomekit.model.services import Service, ServicesTypes
@ -25,6 +25,12 @@ from . import KNOWN_DEVICES
from .connection import HKDevice from .connection import HKDevice
from .entity import HomeKitEntity from .entity import HomeKitEntity
if TYPE_CHECKING:
from functools import cached_property
else:
from homeassistant.backports.functools import cached_property
HK_MODE_TO_HA = { HK_MODE_TO_HA = {
0: "off", 0: "off",
1: MODE_AUTO, 1: MODE_AUTO,
@ -39,46 +45,25 @@ HA_MODE_TO_HK = {
} }
class HomeKitHumidifier(HomeKitEntity, HumidifierEntity): class HomeKitBaseHumidifier(HomeKitEntity, HumidifierEntity):
"""Representation of a HomeKit Controller Humidifier.""" """Representation of a HomeKit Controller Humidifier."""
_attr_device_class = HumidifierDeviceClass.HUMIDIFIER
_attr_supported_features = HumidifierEntityFeature.MODES _attr_supported_features = HumidifierEntityFeature.MODES
_attr_available_modes = [MODE_NORMAL, MODE_AUTO]
_humidity_char = CharacteristicsTypes.RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD
_on_mode_value = 1
def get_characteristic_types(self) -> list[str]: @callback
"""Define the homekit characteristics the entity cares about.""" def _async_reconfigure(self) -> None:
return [ """Reconfigure entity."""
CharacteristicsTypes.ACTIVE, self._async_clear_property_cache(("max_humidity", "min_humidity"))
CharacteristicsTypes.CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE, super()._async_reconfigure()
CharacteristicsTypes.TARGET_HUMIDIFIER_DEHUMIDIFIER_STATE,
CharacteristicsTypes.RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD,
]
@property @property
def is_on(self) -> bool: def is_on(self) -> bool:
"""Return true if device is on.""" """Return true if device is on."""
return self.service.value(CharacteristicsTypes.ACTIVE) return self.service.value(CharacteristicsTypes.ACTIVE)
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the specified valve on."""
await self.async_put_characteristics({CharacteristicsTypes.ACTIVE: True})
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the specified valve off."""
await self.async_put_characteristics({CharacteristicsTypes.ACTIVE: False})
@property
def target_humidity(self) -> int | None:
"""Return the humidity we try to reach."""
return self.service.value(
CharacteristicsTypes.RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD
)
@property
def current_humidity(self) -> int | None:
"""Return the current humidity."""
return self.service.value(CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT)
@property @property
def mode(self) -> str | None: def mode(self) -> str | None:
"""Return the current mode, e.g., home, auto, baby. """Return the current mode, e.g., home, auto, baby.
@ -91,23 +76,36 @@ class HomeKitHumidifier(HomeKitEntity, HumidifierEntity):
return MODE_AUTO if mode == 1 else MODE_NORMAL return MODE_AUTO if mode == 1 else MODE_NORMAL
@property @property
def available_modes(self) -> list[str] | None: def current_humidity(self) -> int | None:
"""Return a list of available modes. """Return the current humidity."""
return self.service.value(CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT)
Requires HumidifierEntityFeature.MODES. @property
""" def target_humidity(self) -> int | None:
available_modes = [ """Return the humidity we try to reach."""
MODE_NORMAL, return self.service.value(self._humidity_char)
MODE_AUTO,
]
return available_modes @cached_property
def min_humidity(self) -> int:
"""Return the minimum humidity."""
return int(self.service[self._humidity_char].minValue or DEFAULT_MIN_HUMIDITY)
@cached_property
def max_humidity(self) -> int:
"""Return the maximum humidity."""
return int(self.service[self._humidity_char].maxValue or DEFAULT_MAX_HUMIDITY)
async def async_set_humidity(self, humidity: int) -> None: async def async_set_humidity(self, humidity: int) -> None:
"""Set new target humidity.""" """Set new target humidity."""
await self.async_put_characteristics( await self.async_put_characteristics({self._humidity_char: humidity})
{CharacteristicsTypes.RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD: humidity}
) async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the specified valve on."""
await self.async_put_characteristics({CharacteristicsTypes.ACTIVE: True})
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the specified valve off."""
await self.async_put_characteristics({CharacteristicsTypes.ACTIVE: False})
async def async_set_mode(self, mode: str) -> None: async def async_set_mode(self, mode: str) -> None:
"""Set new mode.""" """Set new mode."""
@ -121,37 +119,33 @@ class HomeKitHumidifier(HomeKitEntity, HumidifierEntity):
else: else:
await self.async_put_characteristics( await self.async_put_characteristics(
{ {
CharacteristicsTypes.TARGET_HUMIDIFIER_DEHUMIDIFIER_STATE: 1, CharacteristicsTypes.TARGET_HUMIDIFIER_DEHUMIDIFIER_STATE: self._on_mode_value,
CharacteristicsTypes.ACTIVE: True, CharacteristicsTypes.ACTIVE: True,
} }
) )
@property def get_characteristic_types(self) -> list[str]:
def min_humidity(self) -> int: """Define the homekit characteristics the entity cares about."""
"""Return the minimum humidity.""" return [
return int( CharacteristicsTypes.ACTIVE,
self.service[ CharacteristicsTypes.CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE,
CharacteristicsTypes.RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD CharacteristicsTypes.TARGET_HUMIDIFIER_DEHUMIDIFIER_STATE,
].minValue CharacteristicsTypes.RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD,
or DEFAULT_MIN_HUMIDITY ]
)
@property
def max_humidity(self) -> int:
"""Return the maximum humidity."""
return int(
self.service[
CharacteristicsTypes.RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD
].maxValue
or DEFAULT_MAX_HUMIDITY
)
class HomeKitDehumidifier(HomeKitEntity, HumidifierEntity): class HomeKitHumidifier(HomeKitBaseHumidifier):
"""Representation of a HomeKit Controller Humidifier."""
_attr_device_class = HumidifierDeviceClass.HUMIDIFIER
class HomeKitDehumidifier(HomeKitBaseHumidifier):
"""Representation of a HomeKit Controller Humidifier.""" """Representation of a HomeKit Controller Humidifier."""
_attr_device_class = HumidifierDeviceClass.DEHUMIDIFIER _attr_device_class = HumidifierDeviceClass.DEHUMIDIFIER
_attr_supported_features = HumidifierEntityFeature.MODES _humidity_char = CharacteristicsTypes.RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD
_on_mode_value = 2
def __init__(self, accessory: HKDevice, devinfo: ConfigType) -> None: def __init__(self, accessory: HKDevice, devinfo: ConfigType) -> None:
"""Initialise the dehumidifier.""" """Initialise the dehumidifier."""
@ -160,106 +154,10 @@ class HomeKitDehumidifier(HomeKitEntity, HumidifierEntity):
def get_characteristic_types(self) -> list[str]: def get_characteristic_types(self) -> list[str]:
"""Define the homekit characteristics the entity cares about.""" """Define the homekit characteristics the entity cares about."""
return [ return super().get_characteristic_types() + [
CharacteristicsTypes.ACTIVE,
CharacteristicsTypes.CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE,
CharacteristicsTypes.TARGET_HUMIDIFIER_DEHUMIDIFIER_STATE,
CharacteristicsTypes.RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD,
CharacteristicsTypes.RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD,
]
@property
def is_on(self) -> bool:
"""Return true if device is on."""
return self.service.value(CharacteristicsTypes.ACTIVE)
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the specified valve on."""
await self.async_put_characteristics({CharacteristicsTypes.ACTIVE: True})
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the specified valve off."""
await self.async_put_characteristics({CharacteristicsTypes.ACTIVE: False})
@property
def target_humidity(self) -> int | None:
"""Return the humidity we try to reach."""
return self.service.value(
CharacteristicsTypes.RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD CharacteristicsTypes.RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD
)
@property
def current_humidity(self) -> int | None:
"""Return the current humidity."""
return self.service.value(CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT)
@property
def mode(self) -> str | None:
"""Return the current mode, e.g., home, auto, baby.
Requires HumidifierEntityFeature.MODES.
"""
mode = self.service.value(
CharacteristicsTypes.CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE
)
return MODE_AUTO if mode == 1 else MODE_NORMAL
@property
def available_modes(self) -> list[str] | None:
"""Return a list of available modes.
Requires HumidifierEntityFeature.MODES.
"""
available_modes = [
MODE_NORMAL,
MODE_AUTO,
] ]
return available_modes
async def async_set_humidity(self, humidity: int) -> None:
"""Set new target humidity."""
await self.async_put_characteristics(
{CharacteristicsTypes.RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD: humidity}
)
async def async_set_mode(self, mode: str) -> None:
"""Set new mode."""
if mode == MODE_AUTO:
await self.async_put_characteristics(
{
CharacteristicsTypes.TARGET_HUMIDIFIER_DEHUMIDIFIER_STATE: 0,
CharacteristicsTypes.ACTIVE: True,
}
)
else:
await self.async_put_characteristics(
{
CharacteristicsTypes.TARGET_HUMIDIFIER_DEHUMIDIFIER_STATE: 2,
CharacteristicsTypes.ACTIVE: True,
}
)
@property
def min_humidity(self) -> int:
"""Return the minimum humidity."""
return int(
self.service[
CharacteristicsTypes.RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD
].minValue
or DEFAULT_MIN_HUMIDITY
)
@property
def max_humidity(self) -> int:
"""Return the maximum humidity."""
return int(
self.service[
CharacteristicsTypes.RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD
].maxValue
or DEFAULT_MAX_HUMIDITY
)
@property @property
def old_unique_id(self) -> str: def old_unique_id(self) -> str:
"""Return the old ID of this device.""" """Return the old ID of this device."""

View file

@ -1,7 +1,7 @@
"""Support for Homekit lights.""" """Support for Homekit lights."""
from __future__ import annotations from __future__ import annotations
from typing import Any from typing import TYPE_CHECKING, Any
from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.characteristics import CharacteristicsTypes
from aiohomekit.model.services import Service, ServicesTypes from aiohomekit.model.services import Service, ServicesTypes
@ -22,6 +22,11 @@ from . import KNOWN_DEVICES
from .connection import HKDevice from .connection import HKDevice
from .entity import HomeKitEntity from .entity import HomeKitEntity
if TYPE_CHECKING:
from functools import cached_property
else:
from homeassistant.backports.functools import cached_property
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
@ -50,6 +55,14 @@ async def async_setup_entry(
class HomeKitLight(HomeKitEntity, LightEntity): class HomeKitLight(HomeKitEntity, LightEntity):
"""Representation of a Homekit light.""" """Representation of a Homekit light."""
@callback
def _async_reconfigure(self) -> None:
"""Reconfigure entity."""
self._async_clear_property_cache(
("supported_features", "min_mireds", "max_mireds", "supported_color_modes")
)
super()._async_reconfigure()
def get_characteristic_types(self) -> list[str]: def get_characteristic_types(self) -> list[str]:
"""Define the homekit characteristics the entity cares about.""" """Define the homekit characteristics the entity cares about."""
return [ return [
@ -78,13 +91,13 @@ class HomeKitLight(HomeKitEntity, LightEntity):
self.service.value(CharacteristicsTypes.SATURATION), self.service.value(CharacteristicsTypes.SATURATION),
) )
@property @cached_property
def min_mireds(self) -> int: def min_mireds(self) -> int:
"""Return minimum supported color temperature.""" """Return minimum supported color temperature."""
min_value = self.service[CharacteristicsTypes.COLOR_TEMPERATURE].minValue min_value = self.service[CharacteristicsTypes.COLOR_TEMPERATURE].minValue
return int(min_value) if min_value else super().min_mireds return int(min_value) if min_value else super().min_mireds
@property @cached_property
def max_mireds(self) -> int: def max_mireds(self) -> int:
"""Return the maximum color temperature.""" """Return the maximum color temperature."""
max_value = self.service[CharacteristicsTypes.COLOR_TEMPERATURE].maxValue max_value = self.service[CharacteristicsTypes.COLOR_TEMPERATURE].maxValue
@ -113,7 +126,7 @@ class HomeKitLight(HomeKitEntity, LightEntity):
return ColorMode.ONOFF return ColorMode.ONOFF
@property @cached_property
def supported_color_modes(self) -> set[ColorMode]: def supported_color_modes(self) -> set[ColorMode]:
"""Flag supported color modes.""" """Flag supported color modes."""
color_modes: set[ColorMode] = set() color_modes: set[ColorMode] = set()

View file

@ -0,0 +1,323 @@
[
{
"aid": 1,
"services": [
{
"iid": 1,
"type": "3E",
"characteristics": [
{
"iid": 2,
"type": "14",
"perms": ["pw"],
"format": "bool"
},
{
"iid": 3,
"type": "20",
"perms": ["pr"],
"format": "string",
"value": "Home Assistant"
},
{
"iid": 4,
"type": "21",
"perms": ["pr"],
"format": "string",
"value": "Bridge"
},
{
"iid": 5,
"type": "23",
"perms": ["pr"],
"format": "string",
"value": "HASS Bridge S6"
},
{
"iid": 6,
"type": "30",
"perms": ["pr"],
"format": "string",
"value": "homekit.bridge"
},
{
"iid": 7,
"type": "52",
"perms": ["pr"],
"format": "string",
"value": "2024.2.0"
}
]
},
{
"iid": 8,
"type": "A2",
"characteristics": [
{
"iid": 9,
"type": "37",
"perms": ["pr", "ev"],
"format": "string",
"value": "01.01.00"
}
]
}
]
},
{
"aid": 878448248,
"services": [
{
"iid": 1,
"type": "3E",
"characteristics": [
{
"iid": 2,
"type": "14",
"perms": ["pw"],
"format": "bool"
},
{
"iid": 3,
"type": "20",
"perms": ["pr"],
"format": "string",
"value": "RYSE Inc."
},
{
"iid": 4,
"type": "21",
"perms": ["pr"],
"format": "string",
"value": "RYSE Shade"
},
{
"iid": 5,
"type": "23",
"perms": ["pr"],
"format": "string",
"value": "Kitchen Window"
},
{
"iid": 6,
"type": "30",
"perms": ["pr"],
"format": "string",
"value": "cover.kitchen_window"
},
{
"iid": 7,
"type": "52",
"perms": ["pr"],
"format": "string",
"value": "3.6.2"
},
{
"iid": 8,
"type": "53",
"perms": ["pr"],
"format": "string",
"value": "1.0.0"
}
]
},
{
"iid": 9,
"type": "96",
"characteristics": [
{
"iid": 10,
"type": "68",
"perms": ["pr", "ev"],
"format": "uint8",
"minStep": 1,
"minValue": 0,
"unit": "percentage",
"maxValue": 100,
"value": 100
},
{
"iid": 11,
"type": "8F",
"perms": ["pr", "ev"],
"format": "uint8",
"valid-values": [0, 1, 2],
"value": 2
},
{
"iid": 12,
"type": "79",
"perms": ["pr", "ev"],
"format": "uint8",
"valid-values": [0, 1],
"value": 0
}
]
},
{
"iid": 13,
"type": "8C",
"characteristics": [
{
"iid": 14,
"type": "6D",
"perms": ["pr", "ev"],
"format": "uint8",
"minStep": 1,
"minValue": 0,
"unit": "percentage",
"maxValue": 100,
"value": 100
},
{
"iid": 15,
"type": "7C",
"perms": ["pr", "pw", "ev"],
"format": "uint8",
"minStep": 1,
"minValue": 0,
"unit": "percentage",
"maxValue": 100,
"value": 100
},
{
"iid": 16,
"type": "72",
"perms": ["pr", "ev"],
"format": "uint8",
"valid-values": [0, 1, 2],
"value": 2
}
]
}
]
},
{
"aid": 123016423,
"services": [
{
"iid": 1,
"type": "3E",
"characteristics": [
{
"iid": 155,
"type": "14",
"perms": ["pw"],
"format": "bool"
},
{
"iid": 156,
"type": "20",
"perms": ["pr"],
"format": "string",
"value": "RYSE Inc."
},
{
"iid": 157,
"type": "21",
"perms": ["pr"],
"format": "string",
"value": "RYSE Shade"
},
{
"iid": 158,
"type": "23",
"perms": ["pr"],
"format": "string",
"value": "Family Room North"
},
{
"iid": 159,
"type": "30",
"perms": ["pr"],
"format": "string",
"value": "cover.family_door_north"
},
{
"iid": 160,
"type": "52",
"perms": ["pr"],
"format": "string",
"value": "3.6.2"
},
{
"iid": 161,
"type": "53",
"perms": ["pr"],
"format": "string",
"value": "1.0.0"
}
]
},
{
"iid": 162,
"type": "96",
"characteristics": [
{
"iid": 163,
"type": "68",
"perms": ["pr", "ev"],
"format": "uint8",
"minStep": 1,
"minValue": 0,
"unit": "percentage",
"maxValue": 100,
"value": 100
},
{
"iid": 164,
"type": "8F",
"perms": ["pr", "ev"],
"format": "uint8",
"valid-values": [0, 1, 2],
"value": 2
},
{
"iid": 165,
"type": "79",
"perms": ["pr", "ev"],
"format": "uint8",
"valid-values": [0, 1],
"value": 0
}
]
},
{
"iid": 166,
"type": "8C",
"characteristics": [
{
"iid": 167,
"type": "6D",
"perms": ["pr", "ev"],
"format": "uint8",
"minStep": 1,
"minValue": 0,
"unit": "percentage",
"maxValue": 100,
"value": 98
},
{
"iid": 168,
"type": "7C",
"perms": ["pr", "pw", "ev"],
"format": "uint8",
"minStep": 1,
"minValue": 0,
"unit": "percentage",
"maxValue": 100,
"value": 98
},
{
"iid": 169,
"type": "72",
"perms": ["pr", "ev"],
"format": "uint8",
"valid-values": [0, 1, 2],
"value": 2
}
]
}
]
}
]

View file

@ -0,0 +1,229 @@
[
{
"aid": 1,
"services": [
{
"iid": 1,
"type": "3E",
"characteristics": [
{
"iid": 2,
"type": "14",
"perms": ["pw"],
"format": "bool"
},
{
"iid": 3,
"type": "20",
"perms": ["pr"],
"format": "string",
"value": "Home Assistant"
},
{
"iid": 4,
"type": "21",
"perms": ["pr"],
"format": "string",
"value": "Bridge"
},
{
"iid": 5,
"type": "23",
"perms": ["pr"],
"format": "string",
"value": "HASS Bridge S6"
},
{
"iid": 6,
"type": "30",
"perms": ["pr"],
"format": "string",
"value": "homekit.bridge"
},
{
"iid": 7,
"type": "52",
"perms": ["pr"],
"format": "string",
"value": "2024.2.0"
}
]
},
{
"iid": 8,
"type": "A2",
"characteristics": [
{
"iid": 9,
"type": "37",
"perms": ["pr", "ev"],
"format": "string",
"value": "01.01.00"
}
]
}
]
},
{
"aid": 1233851541,
"services": [
{
"iid": 1,
"type": "3E",
"characteristics": [
{
"iid": 163,
"type": "14",
"perms": ["pw"],
"format": "bool"
},
{
"iid": 164,
"type": "20",
"perms": ["pr"],
"format": "string",
"value": "Lookin"
},
{
"iid": 165,
"type": "21",
"perms": ["pr"],
"format": "string",
"value": "Climate Control"
},
{
"iid": 166,
"type": "23",
"perms": ["pr"],
"format": "string",
"value": "89 Living Room"
},
{
"iid": 167,
"type": "30",
"perms": ["pr"],
"format": "string",
"value": "climate.89_living_room"
},
{
"iid": 168,
"type": "52",
"perms": ["pr"],
"format": "string",
"value": "2024.2.0"
}
],
"primary": false
},
{
"iid": 169,
"type": "BC",
"characteristics": [
{
"iid": 170,
"type": "B2",
"perms": ["pr", "ev"],
"format": "uint8",
"valid-values": [0, 1, 2],
"value": 0
},
{
"iid": 171,
"type": "B1",
"perms": ["pr", "pw", "ev"],
"format": "uint8",
"valid-values": [0, 1, 2, 3],
"value": 2
},
{
"iid": 172,
"type": "11",
"perms": ["pr", "ev"],
"format": "float",
"unit": "celsius",
"minStep": 0.1,
"maxValue": 1000,
"minValue": -273.1,
"value": 22.8
},
{
"iid": 173,
"type": "35",
"perms": ["pr", "pw", "ev"],
"format": "float",
"unit": "celsius",
"minStep": 0.1,
"maxValue": 30.0,
"minValue": 16.0,
"value": 20.0
},
{
"iid": 174,
"type": "36",
"perms": ["pr", "pw", "ev"],
"format": "uint8",
"valid-values": [0, 1],
"value": 1
},
{
"iid": 180,
"type": "10",
"perms": ["pr", "ev"],
"format": "float",
"unit": "percentage",
"minStep": 1,
"maxValue": 100,
"minValue": 0,
"value": 60
}
],
"primary": true,
"linked": [175]
},
{
"iid": 175,
"type": "B7",
"characteristics": [
{
"iid": 176,
"type": "B0",
"perms": ["pr", "pw", "ev"],
"format": "uint8",
"valid-values": [0, 1],
"value": 1
},
{
"iid": 177,
"type": "29",
"perms": ["pr", "pw", "ev"],
"format": "float",
"description": "Fan Mode",
"unit": "percentage",
"minStep": 33.333333333333336,
"maxValue": 100,
"minValue": 0,
"value": 33.33333333333334
},
{
"iid": 178,
"type": "BF",
"perms": ["pr", "pw", "ev"],
"format": "uint8",
"description": "Fan Auto",
"valid-values": [0, 1],
"value": 0
},
{
"iid": 179,
"type": "B6",
"perms": ["pr", "pw", "ev"],
"format": "uint8",
"description": "Swing Mode",
"valid-values": [0, 1],
"value": 0
}
]
}
]
}
]

View file

@ -0,0 +1,183 @@
[
{
"aid": 1,
"services": [
{
"iid": 1,
"type": "3E",
"characteristics": [
{
"iid": 2,
"type": "14",
"perms": ["pw"],
"format": "bool"
},
{
"iid": 3,
"type": "20",
"perms": ["pr"],
"format": "string",
"value": "Home Assistant"
},
{
"iid": 4,
"type": "21",
"perms": ["pr"],
"format": "string",
"value": "Bridge"
},
{
"iid": 5,
"type": "23",
"perms": ["pr"],
"format": "string",
"value": "HASS Bridge S6"
},
{
"iid": 6,
"type": "30",
"perms": ["pr"],
"format": "string",
"value": "homekit.bridge"
},
{
"iid": 7,
"type": "52",
"perms": ["pr"],
"format": "string",
"value": "2024.2.0"
}
]
},
{
"iid": 8,
"type": "A2",
"characteristics": [
{
"iid": 9,
"type": "37",
"perms": ["pr", "ev"],
"format": "string",
"value": "01.01.00"
}
]
}
]
},
{
"aid": 3982136094,
"services": [
{
"iid": 1,
"type": "3E",
"characteristics": [
{
"iid": 597,
"type": "14",
"perms": ["pw"],
"format": "bool"
},
{
"iid": 598,
"type": "20",
"perms": ["pr"],
"format": "string",
"value": "FirstAlert"
},
{
"iid": 599,
"type": "21",
"perms": ["pr"],
"format": "string",
"value": "1039102"
},
{
"iid": 600,
"type": "23",
"perms": ["pr"],
"format": "string",
"value": "Laundry Smoke ED78"
},
{
"iid": 601,
"type": "30",
"perms": ["pr"],
"format": "string",
"value": "light.laundry_smoke_ed78"
},
{
"iid": 602,
"type": "52",
"perms": ["pr"],
"format": "string",
"value": "1.4.84"
},
{
"iid": 603,
"type": "53",
"perms": ["pr"],
"format": "string",
"value": "9.0.0"
}
]
},
{
"iid": 604,
"type": "96",
"characteristics": [
{
"iid": 605,
"type": "68",
"perms": ["pr", "ev"],
"format": "uint8",
"maxValue": 100,
"minStep": 1,
"minValue": 0,
"unit": "percentage",
"value": 100
},
{
"iid": 606,
"type": "8F",
"perms": ["pr", "ev"],
"format": "uint8",
"valid-values": [0, 1, 2],
"value": 2
},
{
"iid": 607,
"type": "79",
"perms": ["pr", "ev"],
"format": "uint8",
"valid-values": [0, 1],
"value": 0
}
]
},
{
"iid": 608,
"type": "43",
"characteristics": [
{
"iid": 609,
"type": "25",
"perms": ["pr", "pw", "ev"],
"format": "bool",
"value": false
},
{
"iid": 610,
"type": "8",
"perms": ["pr", "pw", "ev"],
"format": "int",
"maxValue": 100,
"minStep": 1,
"minValue": 0,
"unit": "percentage",
"value": 100
}
]
}
]
}
]

View file

@ -0,0 +1,330 @@
[
{
"aid": 1,
"services": [
{
"iid": 1,
"type": "3E",
"characteristics": [
{
"iid": 2,
"type": "14",
"perms": ["pw"],
"format": "bool"
},
{
"iid": 3,
"type": "20",
"perms": ["pr"],
"format": "string",
"value": "Home Assistant"
},
{
"iid": 4,
"type": "21",
"perms": ["pr"],
"format": "string",
"value": "Bridge"
},
{
"iid": 5,
"type": "23",
"perms": ["pr"],
"format": "string",
"value": "HASS Bridge S6"
},
{
"iid": 6,
"type": "30",
"perms": ["pr"],
"format": "string",
"value": "homekit.bridge"
},
{
"iid": 7,
"type": "52",
"perms": ["pr"],
"format": "string",
"value": "2024.2.0"
}
]
},
{
"iid": 8,
"type": "A2",
"characteristics": [
{
"iid": 9,
"type": "37",
"perms": ["pr", "ev"],
"format": "string",
"value": "01.01.00"
}
]
}
]
},
{
"aid": 878448248,
"services": [
{
"iid": 1,
"type": "3E",
"characteristics": [
{
"iid": 2,
"type": "14",
"perms": ["pw"],
"format": "bool"
},
{
"iid": 3,
"type": "20",
"perms": ["pr"],
"format": "string",
"value": "RYSE Inc."
},
{
"iid": 4,
"type": "21",
"perms": ["pr"],
"format": "string",
"value": "RYSE Shade"
},
{
"iid": 5,
"type": "23",
"perms": ["pr"],
"format": "string",
"value": "Kitchen Window"
},
{
"iid": 6,
"type": "30",
"perms": ["pr"],
"format": "string",
"value": "cover.kitchen_window"
},
{
"iid": 7,
"type": "52",
"perms": ["pr"],
"format": "string",
"value": "3.6.2"
},
{
"iid": 8,
"type": "53",
"perms": ["pr"],
"format": "string",
"value": "1.0.0"
}
]
},
{
"iid": 9,
"type": "96",
"characteristics": [
{
"iid": 10,
"type": "68",
"perms": ["pr", "ev"],
"format": "uint8",
"minStep": 1,
"minValue": 0,
"unit": "percentage",
"maxValue": 100,
"value": 100
},
{
"iid": 11,
"type": "8F",
"perms": ["pr", "ev"],
"format": "uint8",
"valid-values": [0, 1, 2],
"value": 2
},
{
"iid": 12,
"type": "79",
"perms": ["pr", "ev"],
"format": "uint8",
"valid-values": [0, 1],
"value": 0
}
]
},
{
"iid": 13,
"type": "8C",
"characteristics": [
{
"iid": 14,
"type": "6D",
"perms": ["pr", "ev"],
"format": "uint8",
"minStep": 1,
"minValue": 0,
"unit": "percentage",
"maxValue": 100,
"value": 100
},
{
"iid": 15,
"type": "7C",
"perms": ["pr", "pw", "ev"],
"format": "uint8",
"minStep": 1,
"minValue": 0,
"unit": "percentage",
"maxValue": 100,
"value": 100
},
{
"iid": 16,
"type": "72",
"perms": ["pr", "ev"],
"format": "uint8",
"valid-values": [0, 1, 2],
"value": 2
}
]
}
]
},
{
"aid": 123016423,
"services": [
{
"iid": 1,
"type": "3E",
"characteristics": [
{
"iid": 155,
"type": "14",
"perms": ["pw"],
"format": "bool"
},
{
"iid": 156,
"type": "20",
"perms": ["pr"],
"format": "string",
"value": "RYSE Inc."
},
{
"iid": 157,
"type": "21",
"perms": ["pr"],
"format": "string",
"value": "RYSE Shade"
},
{
"iid": 158,
"type": "23",
"perms": ["pr"],
"format": "string",
"value": "Family Room North"
},
{
"iid": 159,
"type": "30",
"perms": ["pr"],
"format": "string",
"value": "cover.family_door_north"
},
{
"iid": 160,
"type": "52",
"perms": ["pr"],
"format": "string",
"value": "3.6.2"
},
{
"iid": 161,
"type": "53",
"perms": ["pr"],
"format": "string",
"value": "1.0.0"
}
]
},
{
"iid": 162,
"type": "96",
"characteristics": [
{
"iid": 163,
"type": "68",
"perms": ["pr", "ev"],
"format": "uint8",
"minStep": 1,
"minValue": 0,
"unit": "percentage",
"maxValue": 100,
"value": 100
},
{
"iid": 164,
"type": "8F",
"perms": ["pr", "ev"],
"format": "uint8",
"valid-values": [0, 1, 2],
"value": 2
},
{
"iid": 165,
"type": "79",
"perms": ["pr", "ev"],
"format": "uint8",
"valid-values": [0, 1],
"value": 0
}
]
},
{
"iid": 166,
"type": "8C",
"characteristics": [
{
"iid": 167,
"type": "6D",
"perms": ["pr", "ev"],
"format": "uint8",
"minStep": 1,
"minValue": 0,
"unit": "percentage",
"maxValue": 100,
"value": 98
},
{
"iid": 168,
"type": "7C",
"perms": ["pr", "pw", "ev"],
"format": "uint8",
"minStep": 1,
"minValue": 0,
"unit": "percentage",
"maxValue": 100,
"value": 98
},
{
"iid": 169,
"type": "72",
"perms": ["pr", "ev"],
"format": "uint8",
"valid-values": [0, 1, 2],
"value": 2
},
{
"iid": 170,
"type": "6F",
"perms": ["pw", "pr", "ev"],
"format": "bool",
"value": false
}
]
}
]
}
]

View file

@ -0,0 +1,237 @@
[
{
"aid": 1,
"services": [
{
"iid": 1,
"type": "3E",
"characteristics": [
{
"iid": 2,
"type": "14",
"perms": ["pw"],
"format": "bool"
},
{
"iid": 3,
"type": "20",
"perms": ["pr"],
"format": "string",
"value": "Home Assistant"
},
{
"iid": 4,
"type": "21",
"perms": ["pr"],
"format": "string",
"value": "Bridge"
},
{
"iid": 5,
"type": "23",
"perms": ["pr"],
"format": "string",
"value": "HASS Bridge S6"
},
{
"iid": 6,
"type": "30",
"perms": ["pr"],
"format": "string",
"value": "homekit.bridge"
},
{
"iid": 7,
"type": "52",
"perms": ["pr"],
"format": "string",
"value": "2024.2.0"
}
]
},
{
"iid": 8,
"type": "A2",
"characteristics": [
{
"iid": 9,
"type": "37",
"perms": ["pr", "ev"],
"format": "string",
"value": "01.01.00"
}
]
}
]
},
{
"aid": 1233851541,
"services": [
{
"iid": 1,
"type": "3E",
"characteristics": [
{
"iid": 163,
"type": "14",
"perms": ["pw"],
"format": "bool"
},
{
"iid": 164,
"type": "20",
"perms": ["pr"],
"format": "string",
"value": "Lookin"
},
{
"iid": 165,
"type": "21",
"perms": ["pr"],
"format": "string",
"value": "Climate Control"
},
{
"iid": 166,
"type": "23",
"perms": ["pr"],
"format": "string",
"value": "89 Living Room"
},
{
"iid": 167,
"type": "30",
"perms": ["pr"],
"format": "string",
"value": "climate.89_living_room"
},
{
"iid": 168,
"type": "52",
"perms": ["pr"],
"format": "string",
"value": "2024.2.0"
}
],
"primary": false
},
{
"iid": 169,
"type": "BC",
"characteristics": [
{
"iid": 170,
"type": "B2",
"perms": ["pr", "ev"],
"format": "uint8",
"valid-values": [0, 1, 2],
"value": 0
},
{
"iid": 171,
"type": "B1",
"perms": ["pr", "pw", "ev"],
"format": "uint8",
"valid-values": [0, 1, 2, 3],
"value": 2
},
{
"iid": 172,
"type": "11",
"perms": ["pr", "ev"],
"format": "float",
"unit": "celsius",
"minStep": 0.1,
"maxValue": 1000,
"minValue": -273.1,
"value": 22.8
},
{
"iid": 173,
"type": "35",
"perms": ["pr", "pw", "ev"],
"format": "float",
"unit": "celsius",
"minStep": 0.1,
"maxValue": 30.0,
"minValue": 16.0,
"value": 20.0
},
{
"iid": 174,
"type": "36",
"perms": ["pr", "pw", "ev"],
"format": "uint8",
"valid-values": [0, 1],
"value": 1
},
{
"iid": 180,
"type": "10",
"perms": ["pr", "ev"],
"format": "float",
"unit": "percentage",
"minStep": 1,
"maxValue": 100,
"minValue": 0,
"value": 60
},
{
"iid": 290,
"type": "B6",
"perms": ["pr", "pw", "ev"],
"format": "uint8",
"valid-values": [0, 1],
"value": 1
}
],
"primary": true,
"linked": [175]
},
{
"iid": 175,
"type": "B7",
"characteristics": [
{
"iid": 176,
"type": "B0",
"perms": ["pr", "pw", "ev"],
"format": "uint8",
"valid-values": [0, 1],
"value": 1
},
{
"iid": 177,
"type": "29",
"perms": ["pr", "pw", "ev"],
"format": "float",
"description": "Fan Mode",
"unit": "percentage",
"minStep": 33.333333333333336,
"maxValue": 100,
"minValue": 0,
"value": 33.33333333333334
},
{
"iid": 178,
"type": "BF",
"perms": ["pr", "pw", "ev"],
"format": "uint8",
"description": "Fan Auto",
"valid-values": [0, 1],
"value": 0
},
{
"iid": 179,
"type": "B6",
"perms": ["pr", "pw", "ev"],
"format": "uint8",
"description": "Swing Mode",
"valid-values": [0, 1],
"value": 0
}
]
}
]
}
]

View file

@ -0,0 +1,173 @@
[
{
"aid": 1,
"services": [
{
"iid": 1,
"type": "3E",
"characteristics": [
{
"iid": 2,
"type": "14",
"perms": ["pw"],
"format": "bool"
},
{
"iid": 3,
"type": "20",
"perms": ["pr"],
"format": "string",
"value": "Home Assistant"
},
{
"iid": 4,
"type": "21",
"perms": ["pr"],
"format": "string",
"value": "Bridge"
},
{
"iid": 5,
"type": "23",
"perms": ["pr"],
"format": "string",
"value": "HASS Bridge S6"
},
{
"iid": 6,
"type": "30",
"perms": ["pr"],
"format": "string",
"value": "homekit.bridge"
},
{
"iid": 7,
"type": "52",
"perms": ["pr"],
"format": "string",
"value": "2024.2.0"
}
]
},
{
"iid": 8,
"type": "A2",
"characteristics": [
{
"iid": 9,
"type": "37",
"perms": ["pr", "ev"],
"format": "string",
"value": "01.01.00"
}
]
}
]
},
{
"aid": 293334836,
"services": [
{
"iid": 1,
"type": "3E",
"characteristics": [
{
"iid": 2,
"type": "14",
"perms": ["pw"],
"format": "bool"
},
{
"iid": 3,
"type": "20",
"perms": ["pr"],
"format": "string",
"value": "switchbot"
},
{
"iid": 4,
"type": "21",
"perms": ["pr"],
"format": "string",
"value": "WoHumi"
},
{
"iid": 5,
"type": "23",
"perms": ["pr"],
"format": "string",
"value": "Humidifier 182A"
},
{
"iid": 6,
"type": "30",
"perms": ["pr"],
"format": "string",
"value": "humidifier.humidifier_182a"
},
{
"iid": 7,
"type": "52",
"perms": ["pr"],
"format": "string",
"value": "2024.2.0"
}
]
},
{
"iid": 8,
"type": "BD",
"characteristics": [
{
"iid": 9,
"type": "10",
"perms": ["pr", "ev"],
"format": "float",
"unit": "percentage",
"minStep": 1,
"maxValue": 100,
"minValue": 0,
"value": 0
},
{
"iid": 10,
"type": "B3",
"perms": ["pr", "ev"],
"format": "uint8",
"valid-values": [0, 1, 2],
"value": 0
},
{
"iid": 11,
"type": "B4",
"perms": ["pr", "pw", "ev"],
"format": "uint8",
"maxValue": 1,
"minValue": 1,
"valid-values": [1],
"value": 1
},
{
"iid": 12,
"type": "B0",
"perms": ["pr", "pw", "ev"],
"format": "uint8",
"valid-values": [0, 1],
"value": 0
},
{
"iid": 13,
"type": "CA",
"perms": ["pr", "pw", "ev"],
"format": "float",
"unit": "percentage",
"minStep": 1,
"maxValue": 100,
"minValue": 0,
"value": 45
}
]
}
]
}
]

View file

@ -0,0 +1,173 @@
[
{
"aid": 1,
"services": [
{
"iid": 1,
"type": "3E",
"characteristics": [
{
"iid": 2,
"type": "14",
"perms": ["pw"],
"format": "bool"
},
{
"iid": 3,
"type": "20",
"perms": ["pr"],
"format": "string",
"value": "Home Assistant"
},
{
"iid": 4,
"type": "21",
"perms": ["pr"],
"format": "string",
"value": "Bridge"
},
{
"iid": 5,
"type": "23",
"perms": ["pr"],
"format": "string",
"value": "HASS Bridge S6"
},
{
"iid": 6,
"type": "30",
"perms": ["pr"],
"format": "string",
"value": "homekit.bridge"
},
{
"iid": 7,
"type": "52",
"perms": ["pr"],
"format": "string",
"value": "2024.2.0"
}
]
},
{
"iid": 8,
"type": "A2",
"characteristics": [
{
"iid": 9,
"type": "37",
"perms": ["pr", "ev"],
"format": "string",
"value": "01.01.00"
}
]
}
]
},
{
"aid": 293334836,
"services": [
{
"iid": 1,
"type": "3E",
"characteristics": [
{
"iid": 2,
"type": "14",
"perms": ["pw"],
"format": "bool"
},
{
"iid": 3,
"type": "20",
"perms": ["pr"],
"format": "string",
"value": "switchbot"
},
{
"iid": 4,
"type": "21",
"perms": ["pr"],
"format": "string",
"value": "WoHumi"
},
{
"iid": 5,
"type": "23",
"perms": ["pr"],
"format": "string",
"value": "Humidifier 182A"
},
{
"iid": 6,
"type": "30",
"perms": ["pr"],
"format": "string",
"value": "humidifier.humidifier_182a"
},
{
"iid": 7,
"type": "52",
"perms": ["pr"],
"format": "string",
"value": "2024.2.0"
}
]
},
{
"iid": 8,
"type": "BD",
"characteristics": [
{
"iid": 9,
"type": "10",
"perms": ["pr", "ev"],
"format": "float",
"unit": "percentage",
"minStep": 1,
"maxValue": 100,
"minValue": 0,
"value": 0
},
{
"iid": 10,
"type": "B3",
"perms": ["pr", "ev"],
"format": "uint8",
"valid-values": [0, 1, 2],
"value": 0
},
{
"iid": 11,
"type": "B4",
"perms": ["pr", "pw", "ev"],
"format": "uint8",
"maxValue": 1,
"minValue": 1,
"valid-values": [1],
"value": 1
},
{
"iid": 12,
"type": "B0",
"perms": ["pr", "pw", "ev"],
"format": "uint8",
"valid-values": [0, 1],
"value": 0
},
{
"iid": 13,
"type": "CA",
"perms": ["pr", "pw", "ev"],
"format": "float",
"unit": "percentage",
"minStep": 1,
"maxValue": 80,
"minValue": 20,
"value": 45
}
]
}
]
}
]

View file

@ -0,0 +1,205 @@
[
{
"aid": 1,
"services": [
{
"iid": 1,
"type": "3E",
"characteristics": [
{
"iid": 2,
"type": "14",
"perms": ["pw"],
"format": "bool"
},
{
"iid": 3,
"type": "20",
"perms": ["pr"],
"format": "string",
"value": "Home Assistant"
},
{
"iid": 4,
"type": "21",
"perms": ["pr"],
"format": "string",
"value": "Bridge"
},
{
"iid": 5,
"type": "23",
"perms": ["pr"],
"format": "string",
"value": "HASS Bridge S6"
},
{
"iid": 6,
"type": "30",
"perms": ["pr"],
"format": "string",
"value": "homekit.bridge"
},
{
"iid": 7,
"type": "52",
"perms": ["pr"],
"format": "string",
"value": "2024.2.0"
}
]
},
{
"iid": 8,
"type": "A2",
"characteristics": [
{
"iid": 9,
"type": "37",
"perms": ["pr", "ev"],
"format": "string",
"value": "01.01.00"
}
]
}
]
},
{
"aid": 3982136094,
"services": [
{
"iid": 1,
"type": "3E",
"characteristics": [
{
"iid": 597,
"type": "14",
"perms": ["pw"],
"format": "bool"
},
{
"iid": 598,
"type": "20",
"perms": ["pr"],
"format": "string",
"value": "FirstAlert"
},
{
"iid": 599,
"type": "21",
"perms": ["pr"],
"format": "string",
"value": "1039102"
},
{
"iid": 600,
"type": "23",
"perms": ["pr"],
"format": "string",
"value": "Laundry Smoke ED78"
},
{
"iid": 601,
"type": "30",
"perms": ["pr"],
"format": "string",
"value": "light.laundry_smoke_ed78"
},
{
"iid": 602,
"type": "52",
"perms": ["pr"],
"format": "string",
"value": "1.4.84"
},
{
"iid": 603,
"type": "53",
"perms": ["pr"],
"format": "string",
"value": "9.0.0"
}
]
},
{
"iid": 604,
"type": "96",
"characteristics": [
{
"iid": 605,
"type": "68",
"perms": ["pr", "ev"],
"format": "uint8",
"maxValue": 100,
"minStep": 1,
"minValue": 0,
"unit": "percentage",
"value": 100
},
{
"iid": 606,
"type": "8F",
"perms": ["pr", "ev"],
"format": "uint8",
"valid-values": [0, 1, 2],
"value": 2
},
{
"iid": 607,
"type": "79",
"perms": ["pr", "ev"],
"format": "uint8",
"valid-values": [0, 1],
"value": 0
}
]
},
{
"iid": 608,
"type": "43",
"characteristics": [
{
"iid": 609,
"type": "25",
"perms": ["pr", "pw", "ev"],
"format": "bool",
"value": false
},
{
"iid": 610,
"type": "8",
"perms": ["pr", "pw", "ev"],
"format": "int",
"maxValue": 100,
"minStep": 1,
"minValue": 0,
"unit": "percentage",
"value": 100
},
{
"iid": 611,
"type": "13",
"perms": ["pr", "pw", "ev"],
"format": "float",
"maxValue": 360,
"minStep": 1,
"minValue": 0,
"unit": "arcdegrees",
"value": 0
},
{
"iid": 612,
"type": "2F",
"perms": ["pr", "pw", "ev"],
"format": "float",
"maxValue": 100,
"minStep": 1,
"minValue": 0,
"unit": "percentage",
"value": 75
}
]
}
]
}
]

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,54 @@
"""Test for a Home Assistant bridge that changes cover features at runtime."""
from homeassistant.components.cover import CoverEntityFeature
from homeassistant.const import ATTR_SUPPORTED_FEATURES
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from ..common import (
device_config_changed,
setup_accessories_from_file,
setup_test_accessories,
)
async def test_cover_add_feature_at_runtime(
hass: HomeAssistant, entity_registry: er.EntityRegistry
) -> None:
"""Test that new features can be added at runtime."""
# Set up a basic cover that does not support position
accessories = await setup_accessories_from_file(
hass, "home_assistant_bridge_cover.json"
)
await setup_test_accessories(hass, accessories)
cover = entity_registry.async_get("cover.family_room_north")
assert cover.unique_id == "00:00:00:00:00:00_123016423_166"
cover_state = hass.states.get("cover.family_room_north")
assert (
cover_state.attributes[ATTR_SUPPORTED_FEATURES]
is CoverEntityFeature.OPEN
| CoverEntityFeature.STOP
| CoverEntityFeature.CLOSE
| CoverEntityFeature.SET_POSITION
)
cover = entity_registry.async_get("cover.family_room_north")
assert cover.unique_id == "00:00:00:00:00:00_123016423_166"
# Now change the config to remove stop
accessories = await setup_accessories_from_file(
hass, "home_assistant_bridge_basic_cover.json"
)
await device_config_changed(hass, accessories)
cover_state = hass.states.get("cover.family_room_north")
assert (
cover_state.attributes[ATTR_SUPPORTED_FEATURES]
is CoverEntityFeature.OPEN
| CoverEntityFeature.CLOSE
| CoverEntityFeature.SET_POSITION
)

View file

@ -0,0 +1,48 @@
"""Test for a Home Assistant bridge that changes climate features at runtime."""
from homeassistant.components.climate import ATTR_SWING_MODES, ClimateEntityFeature
from homeassistant.const import ATTR_SUPPORTED_FEATURES
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from ..common import (
device_config_changed,
setup_accessories_from_file,
setup_test_accessories,
)
async def test_cover_add_feature_at_runtime(
hass: HomeAssistant, entity_registry: er.EntityRegistry
) -> None:
"""Test that new features can be added at runtime."""
# Set up a basic heater cooler that does not support swing mode
accessories = await setup_accessories_from_file(
hass, "home_assistant_bridge_basic_heater_cooler.json"
)
await setup_test_accessories(hass, accessories)
climate = entity_registry.async_get("climate.89_living_room")
assert climate.unique_id == "00:00:00:00:00:00_1233851541_169"
climate_state = hass.states.get("climate.89_living_room")
assert climate_state.attributes[ATTR_SUPPORTED_FEATURES] is ClimateEntityFeature(0)
assert ATTR_SWING_MODES not in climate_state.attributes
climate = entity_registry.async_get("climate.89_living_room")
assert climate.unique_id == "00:00:00:00:00:00_1233851541_169"
# Now change the config to add swing mode
accessories = await setup_accessories_from_file(
hass, "home_assistant_bridge_heater_cooler.json"
)
await device_config_changed(hass, accessories)
climate_state = hass.states.get("climate.89_living_room")
assert (
climate_state.attributes[ATTR_SUPPORTED_FEATURES]
is ClimateEntityFeature.SWING_MODE
)
assert climate_state.attributes[ATTR_SWING_MODES] == ["off", "vertical"]

View file

@ -0,0 +1,44 @@
"""Test for a Home Assistant bridge that changes humidifier min/max at runtime."""
from homeassistant.components.humidifier import ATTR_MAX_HUMIDITY, ATTR_MIN_HUMIDITY
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from ..common import (
device_config_changed,
setup_accessories_from_file,
setup_test_accessories,
)
async def test_humidifier_change_range_at_runtime(
hass: HomeAssistant, entity_registry: er.EntityRegistry
) -> None:
"""Test that min max can be changed at runtime."""
# Set up a basic humidifier
accessories = await setup_accessories_from_file(
hass, "home_assistant_bridge_humidifier.json"
)
await setup_test_accessories(hass, accessories)
humidifier = entity_registry.async_get("humidifier.humidifier_182a")
assert humidifier.unique_id == "00:00:00:00:00:00_293334836_8"
humidifier_state = hass.states.get("humidifier.humidifier_182a")
assert humidifier_state.attributes[ATTR_MIN_HUMIDITY] == 0
assert humidifier_state.attributes[ATTR_MAX_HUMIDITY] == 100
cover = entity_registry.async_get("humidifier.humidifier_182a")
assert cover.unique_id == "00:00:00:00:00:00_293334836_8"
# Now change min/max values
accessories = await setup_accessories_from_file(
hass, "home_assistant_bridge_humidifier_new_range.json"
)
await device_config_changed(hass, accessories)
humidifier_state = hass.states.get("humidifier.humidifier_182a")
assert humidifier_state.attributes[ATTR_MIN_HUMIDITY] == 20
assert humidifier_state.attributes[ATTR_MAX_HUMIDITY] == 80

View file

@ -0,0 +1,42 @@
"""Test for a Home Assistant bridge that changes light features at runtime."""
from homeassistant.components.light import ATTR_SUPPORTED_COLOR_MODES, ColorMode
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from ..common import (
device_config_changed,
setup_accessories_from_file,
setup_test_accessories,
)
async def test_light_add_feature_at_runtime(
hass: HomeAssistant, entity_registry: er.EntityRegistry
) -> None:
"""Test that new features can be added at runtime."""
# Set up a basic light that does not support color
accessories = await setup_accessories_from_file(
hass, "home_assistant_bridge_basic_light.json"
)
await setup_test_accessories(hass, accessories)
light = entity_registry.async_get("light.laundry_smoke_ed78")
assert light.unique_id == "00:00:00:00:00:00_3982136094_608"
light_state = hass.states.get("light.laundry_smoke_ed78")
assert light_state.attributes[ATTR_SUPPORTED_COLOR_MODES] == [ColorMode.BRIGHTNESS]
light = entity_registry.async_get("light.laundry_smoke_ed78")
assert light.unique_id == "00:00:00:00:00:00_3982136094_608"
# Now add hue and saturation
accessories = await setup_accessories_from_file(
hass, "home_assistant_bridge_light.json"
)
await device_config_changed(hass, accessories)
light_state = hass.states.get("light.laundry_smoke_ed78")
assert light_state.attributes[ATTR_SUPPORTED_COLOR_MODES] == [ColorMode.HS]