Add missing type hints to homekit_controller (#65368)

This commit is contained in:
Jc2k 2022-02-01 19:30:37 +00:00 committed by GitHub
parent aef6f49eff
commit 9f5d77e0df
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 389 additions and 312 deletions

View file

@ -17,7 +17,7 @@ from aiohomekit.model.services import Service, ServicesTypes
from homeassistant.components import zeroconf
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.core import HomeAssistant
from homeassistant.core import Event, HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.entity import DeviceInfo, Entity
from homeassistant.helpers.typing import ConfigType
@ -30,17 +30,12 @@ from .storage import EntityMapStorage
_LOGGER = logging.getLogger(__name__)
def escape_characteristic_name(char_name):
"""Escape any dash or dots in a characteristics name."""
return char_name.replace("-", "_").replace(".", "_")
class HomeKitEntity(Entity):
"""Representation of a Home Assistant HomeKit device."""
_attr_should_poll = False
def __init__(self, accessory: HKDevice, devinfo):
def __init__(self, accessory: HKDevice, devinfo: ConfigType) -> None:
"""Initialise a generic HomeKit device."""
self._accessory = accessory
self._aid = devinfo["aid"]
@ -67,7 +62,7 @@ class HomeKitEntity(Entity):
"""Return a Service model that this entity is attached to."""
return self.accessory.services.iid(self._iid)
async def async_added_to_hass(self):
async def async_added_to_hass(self) -> None:
"""Entity added to hass."""
self.async_on_remove(
self.hass.helpers.dispatcher.async_dispatcher_connect(
@ -78,12 +73,12 @@ class HomeKitEntity(Entity):
self._accessory.add_pollable_characteristics(self.pollable_characteristics)
self._accessory.add_watchable_characteristics(self.watchable_characteristics)
async def async_will_remove_from_hass(self):
async def async_will_remove_from_hass(self) -> None:
"""Prepare to be removed from hass."""
self._accessory.remove_pollable_characteristics(self._aid)
self._accessory.remove_watchable_characteristics(self._aid)
async def async_put_characteristics(self, characteristics: dict[str, Any]):
async def async_put_characteristics(self, characteristics: dict[str, Any]) -> None:
"""
Write characteristics to the device.
@ -101,10 +96,10 @@ class HomeKitEntity(Entity):
payload = self.service.build_update(characteristics)
return await self._accessory.put_characteristics(payload)
def setup(self):
"""Configure an entity baed on its HomeKit characteristics metadata."""
self.pollable_characteristics = []
self.watchable_characteristics = []
def setup(self) -> None:
"""Configure an entity based on its HomeKit characteristics metadata."""
self.pollable_characteristics: list[tuple[int, int]] = []
self.watchable_characteristics: list[tuple[int, int]] = []
char_types = self.get_characteristic_types()
@ -118,7 +113,7 @@ class HomeKitEntity(Entity):
for char in service.characteristics.filter(char_types=char_types):
self._setup_characteristic(char)
def _setup_characteristic(self, char: Characteristic):
def _setup_characteristic(self, char: Characteristic) -> None:
"""Configure an entity based on a HomeKit characteristics metadata."""
# Build up a list of (aid, iid) tuples to poll on update()
if CharacteristicPermissions.paired_read in char.perms:
@ -153,7 +148,7 @@ class HomeKitEntity(Entity):
"""Return the device info."""
return self._accessory.device_info_for_accessory(self.accessory)
def get_characteristic_types(self):
def get_characteristic_types(self) -> list[str]:
"""Define the homekit characteristics the entity cares about."""
raise NotImplementedError
@ -176,7 +171,9 @@ class CharacteristicEntity(HomeKitEntity):
the service entity.
"""
def __init__(self, accessory, devinfo, char):
def __init__(
self, accessory: HKDevice, devinfo: ConfigType, char: Characteristic
) -> None:
"""Initialise a generic single characteristic HomeKit entity."""
self._char = char
super().__init__(accessory, devinfo)
@ -218,7 +215,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
hass.data[KNOWN_DEVICES] = {}
hass.data[TRIGGERS] = {}
async def _async_stop_homekit_controller(event):
async def _async_stop_homekit_controller(event: Event) -> None:
await asyncio.gather(
*(
connection.async_unload()

View file

@ -1,6 +1,10 @@
"""Support for Homekit Alarm Control Panel."""
from __future__ import annotations
from typing import Any
from aiohomekit.model.characteristics import CharacteristicsTypes
from aiohomekit.model.services import ServicesTypes
from aiohomekit.model.services import Service, ServicesTypes
from homeassistant.components.alarm_control_panel import AlarmControlPanelEntity
from homeassistant.components.alarm_control_panel.const import (
@ -50,7 +54,7 @@ async def async_setup_entry(
conn = hass.data[KNOWN_DEVICES][hkid]
@callback
def async_add_service(service):
def async_add_service(service: Service) -> bool:
if service.type != ServicesTypes.SECURITY_SYSTEM:
return False
info = {"aid": service.accessory.aid, "iid": service.iid}
@ -63,7 +67,7 @@ async def async_setup_entry(
class HomeKitAlarmControlPanelEntity(HomeKitEntity, AlarmControlPanelEntity):
"""Representation of a Homekit Alarm Control Panel."""
def get_characteristic_types(self):
def get_characteristic_types(self) -> list[str]:
"""Define the homekit characteristics the entity cares about."""
return [
CharacteristicsTypes.SECURITY_SYSTEM_STATE_CURRENT,
@ -72,12 +76,12 @@ class HomeKitAlarmControlPanelEntity(HomeKitEntity, AlarmControlPanelEntity):
]
@property
def icon(self):
def icon(self) -> str:
"""Return icon."""
return ICON
@property
def state(self):
def state(self) -> str:
"""Return the state of the device."""
return CURRENT_STATE_MAP[
self.service.value(CharacteristicsTypes.SECURITY_SYSTEM_STATE_CURRENT)
@ -88,30 +92,30 @@ class HomeKitAlarmControlPanelEntity(HomeKitEntity, AlarmControlPanelEntity):
"""Return the list of supported features."""
return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_NIGHT
async def async_alarm_disarm(self, code=None):
async def async_alarm_disarm(self, code: str | None = None) -> None:
"""Send disarm command."""
await self.set_alarm_state(STATE_ALARM_DISARMED, code)
async def async_alarm_arm_away(self, code=None):
async def async_alarm_arm_away(self, code: str | None = None) -> None:
"""Send arm command."""
await self.set_alarm_state(STATE_ALARM_ARMED_AWAY, code)
async def async_alarm_arm_home(self, code=None):
async def async_alarm_arm_home(self, code: str | None = None) -> None:
"""Send stay command."""
await self.set_alarm_state(STATE_ALARM_ARMED_HOME, code)
async def async_alarm_arm_night(self, code=None):
async def async_alarm_arm_night(self, code: str | None = None) -> None:
"""Send night command."""
await self.set_alarm_state(STATE_ALARM_ARMED_NIGHT, code)
async def set_alarm_state(self, state, code=None):
async def set_alarm_state(self, state: str, code: str | None = None) -> None:
"""Send state command."""
await self.async_put_characteristics(
{CharacteristicsTypes.SECURITY_SYSTEM_STATE_TARGET: TARGET_STATE_MAP[state]}
)
@property
def extra_state_attributes(self):
def extra_state_attributes(self) -> dict[str, Any] | None:
"""Return the optional state attributes."""
battery_level = self.service.value(CharacteristicsTypes.BATTERY_LEVEL)

View file

@ -1,6 +1,8 @@
"""Support for Homekit motion sensors."""
from __future__ import annotations
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,
@ -18,14 +20,14 @@ class HomeKitMotionSensor(HomeKitEntity, BinarySensorEntity):
_attr_device_class = BinarySensorDeviceClass.MOTION
def get_characteristic_types(self):
def get_characteristic_types(self) -> list[str]:
"""Define the homekit characteristics the entity is tracking."""
return [CharacteristicsTypes.MOTION_DETECTED]
@property
def is_on(self):
def is_on(self) -> bool:
"""Has motion been detected."""
return self.service.value(CharacteristicsTypes.MOTION_DETECTED)
return self.service.value(CharacteristicsTypes.MOTION_DETECTED) is True
class HomeKitContactSensor(HomeKitEntity, BinarySensorEntity):
@ -33,12 +35,12 @@ class HomeKitContactSensor(HomeKitEntity, BinarySensorEntity):
_attr_device_class = BinarySensorDeviceClass.OPENING
def get_characteristic_types(self):
def get_characteristic_types(self) -> list[str]:
"""Define the homekit characteristics the entity is tracking."""
return [CharacteristicsTypes.CONTACT_STATE]
@property
def is_on(self):
def is_on(self) -> bool:
"""Return true if the binary sensor is on/open."""
return self.service.value(CharacteristicsTypes.CONTACT_STATE) == 1
@ -48,12 +50,12 @@ class HomeKitSmokeSensor(HomeKitEntity, BinarySensorEntity):
_attr_device_class = BinarySensorDeviceClass.SMOKE
def get_characteristic_types(self):
def get_characteristic_types(self) -> list[str]:
"""Define the homekit characteristics the entity is tracking."""
return [CharacteristicsTypes.SMOKE_DETECTED]
@property
def is_on(self):
def is_on(self) -> bool:
"""Return true if smoke is currently detected."""
return self.service.value(CharacteristicsTypes.SMOKE_DETECTED) == 1
@ -63,12 +65,12 @@ class HomeKitCarbonMonoxideSensor(HomeKitEntity, BinarySensorEntity):
_attr_device_class = BinarySensorDeviceClass.GAS
def get_characteristic_types(self):
def get_characteristic_types(self) -> list[str]:
"""Define the homekit characteristics the entity is tracking."""
return [CharacteristicsTypes.CARBON_MONOXIDE_DETECTED]
@property
def is_on(self):
def is_on(self) -> bool:
"""Return true if CO is currently detected."""
return self.service.value(CharacteristicsTypes.CARBON_MONOXIDE_DETECTED) == 1
@ -78,12 +80,12 @@ class HomeKitOccupancySensor(HomeKitEntity, BinarySensorEntity):
_attr_device_class = BinarySensorDeviceClass.OCCUPANCY
def get_characteristic_types(self):
def get_characteristic_types(self) -> list[str]:
"""Define the homekit characteristics the entity is tracking."""
return [CharacteristicsTypes.OCCUPANCY_DETECTED]
@property
def is_on(self):
def is_on(self) -> bool:
"""Return true if occupancy is currently detected."""
return self.service.value(CharacteristicsTypes.OCCUPANCY_DETECTED) == 1
@ -93,12 +95,12 @@ class HomeKitLeakSensor(HomeKitEntity, BinarySensorEntity):
_attr_device_class = BinarySensorDeviceClass.MOISTURE
def get_characteristic_types(self):
def get_characteristic_types(self) -> list[str]:
"""Define the homekit characteristics the entity is tracking."""
return [CharacteristicsTypes.LEAK_DETECTED]
@property
def is_on(self):
def is_on(self) -> bool:
"""Return true if a leak is detected from the binary sensor."""
return self.service.value(CharacteristicsTypes.LEAK_DETECTED) == 1
@ -123,7 +125,7 @@ async def async_setup_entry(
conn = hass.data[KNOWN_DEVICES][hkid]
@callback
def async_add_service(service):
def async_add_service(service: Service) -> bool:
if not (entity_class := ENTITY_TYPES.get(service.type)):
return False
info = {"aid": service.accessory.aid, "iid": service.iid}

View file

@ -19,8 +19,10 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType
from . import KNOWN_DEVICES, CharacteristicEntity
from .connection import HKDevice
@dataclass
@ -64,7 +66,7 @@ async def async_setup_entry(
conn = hass.data[KNOWN_DEVICES][hkid]
@callback
def async_add_characteristic(char: Characteristic):
def async_add_characteristic(char: Characteristic) -> bool:
entities = []
info = {"aid": char.service.accessory.aid, "iid": char.service.iid}
@ -88,16 +90,16 @@ class HomeKitButton(CharacteristicEntity, ButtonEntity):
def __init__(
self,
conn,
info,
char,
conn: HKDevice,
info: ConfigType,
char: Characteristic,
description: HomeKitButtonEntityDescription,
):
) -> None:
"""Initialise a HomeKit button control."""
self.entity_description = description
super().__init__(conn, info, char)
def get_characteristic_types(self):
def get_characteristic_types(self) -> list[str]:
"""Define the homekit characteristics the entity is tracking."""
return [self._char.type]
@ -112,13 +114,13 @@ class HomeKitButton(CharacteristicEntity, ButtonEntity):
"""Press the button."""
key = self.entity_description.key
val = self.entity_description.write_value
return await self.async_put_characteristics({key: val})
await self.async_put_characteristics({key: val})
class HomeKitEcobeeClearHoldButton(CharacteristicEntity, ButtonEntity):
"""Representation of a Button control for Ecobee clear hold request."""
def get_characteristic_types(self):
def get_characteristic_types(self) -> list[str]:
"""Define the homekit characteristics the entity is tracking."""
return []

View file

@ -1,6 +1,7 @@
"""Support for Homekit cameras."""
from __future__ import annotations
from aiohomekit.model import Accessory
from aiohomekit.model.services import ServicesTypes
from homeassistant.components.camera import Camera
@ -16,7 +17,7 @@ class HomeKitCamera(AccessoryEntity, Camera):
# content_type = "image/jpeg"
def get_characteristic_types(self):
def get_characteristic_types(self) -> list[str]:
"""Define the homekit characteristics the entity is tracking."""
return []
@ -41,12 +42,12 @@ async def async_setup_entry(
conn = hass.data[KNOWN_DEVICES][hkid]
@callback
def async_add_accessory(accessory):
def async_add_accessory(accessory: Accessory) -> bool:
stream_mgmt = accessory.services.first(
service_type=ServicesTypes.CAMERA_RTP_STREAM_MANAGEMENT
)
if not stream_mgmt:
return
return False
info = {"aid": accessory.aid, "iid": stream_mgmt.iid}
async_add_entities([HomeKitCamera(conn, info)], True)

View file

@ -1,5 +1,8 @@
"""Support for Homekit climate devices."""
from __future__ import annotations
import logging
from typing import Any
from aiohomekit.model.characteristics import (
ActivationStateValues,
@ -10,7 +13,7 @@ from aiohomekit.model.characteristics import (
SwingModeValues,
TargetHeaterCoolerStateValues,
)
from aiohomekit.model.services import ServicesTypes
from aiohomekit.model.services import Service, ServicesTypes
from aiohomekit.utils import clamp_enum_to_char
from homeassistant.components.climate import ClimateEntity
@ -94,7 +97,7 @@ async def async_setup_entry(
conn = hass.data[KNOWN_DEVICES][hkid]
@callback
def async_add_service(service):
def async_add_service(service: Service) -> bool:
if not (entity_class := ENTITY_TYPES.get(service.type)):
return False
info = {"aid": service.accessory.aid, "iid": service.iid}
@ -107,7 +110,7 @@ async def async_setup_entry(
class HomeKitHeaterCoolerEntity(HomeKitEntity, ClimateEntity):
"""Representation of a Homekit climate device."""
def get_characteristic_types(self):
def get_characteristic_types(self) -> list[str]:
"""Define the homekit characteristics the entity cares about."""
return [
CharacteristicsTypes.ACTIVE,
@ -119,7 +122,7 @@ class HomeKitHeaterCoolerEntity(HomeKitEntity, ClimateEntity):
CharacteristicsTypes.TEMPERATURE_CURRENT,
]
async def async_set_temperature(self, **kwargs):
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperature."""
temp = kwargs.get(ATTR_TEMPERATURE)
state = self.service.value(CharacteristicsTypes.TARGET_HEATER_COOLER_STATE)
@ -140,7 +143,7 @@ class HomeKitHeaterCoolerEntity(HomeKitEntity, ClimateEntity):
hvac_mode,
)
async def async_set_hvac_mode(self, hvac_mode):
async def async_set_hvac_mode(self, hvac_mode: str) -> None:
"""Set new target operation mode."""
if hvac_mode == HVAC_MODE_OFF:
await self.async_put_characteristics(
@ -163,12 +166,12 @@ class HomeKitHeaterCoolerEntity(HomeKitEntity, ClimateEntity):
)
@property
def current_temperature(self):
def current_temperature(self) -> float:
"""Return the current temperature."""
return self.service.value(CharacteristicsTypes.TEMPERATURE_CURRENT)
@property
def target_temperature(self):
def target_temperature(self) -> float | None:
"""Return the temperature we try to reach."""
state = self.service.value(CharacteristicsTypes.TARGET_HEATER_COOLER_STATE)
if state == TargetHeaterCoolerStateValues.COOL:
@ -182,7 +185,7 @@ class HomeKitHeaterCoolerEntity(HomeKitEntity, ClimateEntity):
return None
@property
def target_temperature_step(self):
def target_temperature_step(self) -> float | None:
"""Return the supported step of target temperature."""
state = self.service.value(CharacteristicsTypes.TARGET_HEATER_COOLER_STATE)
if state == TargetHeaterCoolerStateValues.COOL and self.service.has(
@ -200,7 +203,7 @@ class HomeKitHeaterCoolerEntity(HomeKitEntity, ClimateEntity):
return None
@property
def min_temp(self):
def min_temp(self) -> float:
"""Return the minimum target temp."""
state = self.service.value(CharacteristicsTypes.TARGET_HEATER_COOLER_STATE)
if state == TargetHeaterCoolerStateValues.COOL and self.service.has(
@ -218,7 +221,7 @@ class HomeKitHeaterCoolerEntity(HomeKitEntity, ClimateEntity):
return super().min_temp
@property
def max_temp(self):
def max_temp(self) -> float:
"""Return the maximum target temp."""
state = self.service.value(CharacteristicsTypes.TARGET_HEATER_COOLER_STATE)
if state == TargetHeaterCoolerStateValues.COOL and self.service.has(
@ -236,7 +239,7 @@ class HomeKitHeaterCoolerEntity(HomeKitEntity, ClimateEntity):
return super().max_temp
@property
def hvac_action(self):
def hvac_action(self) -> str | None:
"""Return the current running hvac operation."""
# This characteristic describes the current mode of a device,
# e.g. a thermostat is "heating" a room to 75 degrees Fahrenheit.
@ -250,7 +253,7 @@ class HomeKitHeaterCoolerEntity(HomeKitEntity, ClimateEntity):
return CURRENT_HEATER_COOLER_STATE_HOMEKIT_TO_HASS.get(value)
@property
def hvac_mode(self):
def hvac_mode(self) -> str:
"""Return hvac operation ie. heat, cool mode."""
# This characteristic describes the target mode
# E.g. should the device start heating a room if the temperature
@ -262,10 +265,10 @@ class HomeKitHeaterCoolerEntity(HomeKitEntity, ClimateEntity):
):
return HVAC_MODE_OFF
value = self.service.value(CharacteristicsTypes.TARGET_HEATER_COOLER_STATE)
return TARGET_HEATER_COOLER_STATE_HOMEKIT_TO_HASS.get(value)
return TARGET_HEATER_COOLER_STATE_HOMEKIT_TO_HASS[value]
@property
def hvac_modes(self):
def hvac_modes(self) -> list[str]:
"""Return the list of available hvac operation modes."""
valid_values = clamp_enum_to_char(
TargetHeaterCoolerStateValues,
@ -278,7 +281,7 @@ class HomeKitHeaterCoolerEntity(HomeKitEntity, ClimateEntity):
return modes
@property
def swing_mode(self):
def swing_mode(self) -> str:
"""Return the swing setting.
Requires SUPPORT_SWING_MODE.
@ -287,7 +290,7 @@ class HomeKitHeaterCoolerEntity(HomeKitEntity, ClimateEntity):
return SWING_MODE_HOMEKIT_TO_HASS[value]
@property
def swing_modes(self):
def swing_modes(self) -> list[str]:
"""Return the list of available swing modes.
Requires SUPPORT_SWING_MODE.
@ -305,7 +308,7 @@ class HomeKitHeaterCoolerEntity(HomeKitEntity, ClimateEntity):
)
@property
def supported_features(self):
def supported_features(self) -> int:
"""Return the list of supported features."""
features = 0
@ -321,7 +324,7 @@ class HomeKitHeaterCoolerEntity(HomeKitEntity, ClimateEntity):
return features
@property
def temperature_unit(self):
def temperature_unit(self) -> str:
"""Return the unit of measurement."""
return TEMP_CELSIUS
@ -329,7 +332,7 @@ class HomeKitHeaterCoolerEntity(HomeKitEntity, ClimateEntity):
class HomeKitClimateEntity(HomeKitEntity, ClimateEntity):
"""Representation of a Homekit climate device."""
def get_characteristic_types(self):
def get_characteristic_types(self) -> list[str]:
"""Define the homekit characteristics the entity cares about."""
return [
CharacteristicsTypes.HEATING_COOLING_CURRENT,
@ -342,12 +345,12 @@ class HomeKitClimateEntity(HomeKitEntity, ClimateEntity):
CharacteristicsTypes.RELATIVE_HUMIDITY_TARGET,
]
async def async_set_temperature(self, **kwargs):
async def async_set_temperature(self, **kwargs) -> None:
"""Set new target temperature."""
chars = {}
value = self.service.value(CharacteristicsTypes.HEATING_COOLING_TARGET)
mode = MODE_HOMEKIT_TO_HASS.get(value)
mode = MODE_HOMEKIT_TO_HASS[value]
if kwargs.get(ATTR_HVAC_MODE, mode) != mode:
mode = kwargs[ATTR_HVAC_MODE]
@ -359,8 +362,11 @@ class HomeKitClimateEntity(HomeKitEntity, ClimateEntity):
heat_temp = kwargs.get(ATTR_TARGET_TEMP_LOW)
cool_temp = kwargs.get(ATTR_TARGET_TEMP_HIGH)
if (mode == HVAC_MODE_HEAT_COOL) and (
SUPPORT_TARGET_TEMPERATURE_RANGE & self.supported_features
if (
(mode == HVAC_MODE_HEAT_COOL)
and (SUPPORT_TARGET_TEMPERATURE_RANGE & self.supported_features)
and heat_temp
and cool_temp
):
if temp is None:
temp = (cool_temp + heat_temp) / 2
@ -376,13 +382,13 @@ class HomeKitClimateEntity(HomeKitEntity, ClimateEntity):
await self.async_put_characteristics(chars)
async def async_set_humidity(self, humidity):
async def async_set_humidity(self, humidity: int) -> None:
"""Set new target humidity."""
await self.async_put_characteristics(
{CharacteristicsTypes.RELATIVE_HUMIDITY_TARGET: humidity}
)
async def async_set_hvac_mode(self, hvac_mode):
async def async_set_hvac_mode(self, hvac_mode: str) -> None:
"""Set new target operation mode."""
await self.async_put_characteristics(
{
@ -393,12 +399,12 @@ class HomeKitClimateEntity(HomeKitEntity, ClimateEntity):
)
@property
def current_temperature(self):
def current_temperature(self) -> float | None:
"""Return the current temperature."""
return self.service.value(CharacteristicsTypes.TEMPERATURE_CURRENT)
@property
def target_temperature(self):
def target_temperature(self) -> float | None:
"""Return the temperature we try to reach."""
value = self.service.value(CharacteristicsTypes.HEATING_COOLING_TARGET)
if (MODE_HOMEKIT_TO_HASS.get(value) in {HVAC_MODE_HEAT, HVAC_MODE_COOL}) or (
@ -409,7 +415,7 @@ class HomeKitClimateEntity(HomeKitEntity, ClimateEntity):
return None
@property
def target_temperature_high(self):
def target_temperature_high(self) -> float | None:
"""Return the highbound target temperature we try to reach."""
value = self.service.value(CharacteristicsTypes.HEATING_COOLING_TARGET)
if (MODE_HOMEKIT_TO_HASS.get(value) in {HVAC_MODE_HEAT_COOL}) and (
@ -421,7 +427,7 @@ class HomeKitClimateEntity(HomeKitEntity, ClimateEntity):
return None
@property
def target_temperature_low(self):
def target_temperature_low(self) -> float | None:
"""Return the lowbound target temperature we try to reach."""
value = self.service.value(CharacteristicsTypes.HEATING_COOLING_TARGET)
if (MODE_HOMEKIT_TO_HASS.get(value) in {HVAC_MODE_HEAT_COOL}) and (
@ -433,7 +439,7 @@ class HomeKitClimateEntity(HomeKitEntity, ClimateEntity):
return None
@property
def min_temp(self):
def min_temp(self) -> float:
"""Return the minimum target temp."""
value = self.service.value(CharacteristicsTypes.HEATING_COOLING_TARGET)
if (MODE_HOMEKIT_TO_HASS.get(value) in {HVAC_MODE_HEAT_COOL}) and (
@ -455,7 +461,7 @@ class HomeKitClimateEntity(HomeKitEntity, ClimateEntity):
return super().min_temp
@property
def max_temp(self):
def max_temp(self) -> float:
"""Return the maximum target temp."""
value = self.service.value(CharacteristicsTypes.HEATING_COOLING_TARGET)
if (MODE_HOMEKIT_TO_HASS.get(value) in {HVAC_MODE_HEAT_COOL}) and (
@ -477,17 +483,17 @@ class HomeKitClimateEntity(HomeKitEntity, ClimateEntity):
return super().max_temp
@property
def current_humidity(self):
def current_humidity(self) -> int:
"""Return the current humidity."""
return self.service.value(CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT)
@property
def target_humidity(self):
def target_humidity(self) -> int:
"""Return the humidity we try to reach."""
return self.service.value(CharacteristicsTypes.RELATIVE_HUMIDITY_TARGET)
@property
def min_humidity(self):
def min_humidity(self) -> int:
"""Return the minimum humidity."""
min_humidity = self.service[
CharacteristicsTypes.RELATIVE_HUMIDITY_TARGET
@ -497,7 +503,7 @@ class HomeKitClimateEntity(HomeKitEntity, ClimateEntity):
return super().min_humidity
@property
def max_humidity(self):
def max_humidity(self) -> int:
"""Return the maximum humidity."""
max_humidity = self.service[
CharacteristicsTypes.RELATIVE_HUMIDITY_TARGET
@ -507,7 +513,7 @@ class HomeKitClimateEntity(HomeKitEntity, ClimateEntity):
return super().max_humidity
@property
def hvac_action(self):
def hvac_action(self) -> str | None:
"""Return the current running hvac operation."""
# This characteristic describes the current mode of a device,
# e.g. a thermostat is "heating" a room to 75 degrees Fahrenheit.
@ -516,17 +522,17 @@ class HomeKitClimateEntity(HomeKitEntity, ClimateEntity):
return CURRENT_MODE_HOMEKIT_TO_HASS.get(value)
@property
def hvac_mode(self):
def hvac_mode(self) -> str:
"""Return hvac operation ie. heat, cool mode."""
# This characteristic describes the target mode
# E.g. should the device start heating a room if the temperature
# falls below the target temperature.
# Can be 0 - 3 (Off, Heat, Cool, Auto)
value = self.service.value(CharacteristicsTypes.HEATING_COOLING_TARGET)
return MODE_HOMEKIT_TO_HASS.get(value)
return MODE_HOMEKIT_TO_HASS[value]
@property
def hvac_modes(self):
def hvac_modes(self) -> list[str]:
"""Return the list of available hvac operation modes."""
valid_values = clamp_enum_to_char(
HeatingCoolingTargetValues,
@ -535,7 +541,7 @@ class HomeKitClimateEntity(HomeKitEntity, ClimateEntity):
return [MODE_HOMEKIT_TO_HASS[mode] for mode in valid_values]
@property
def supported_features(self):
def supported_features(self) -> int:
"""Return the list of supported features."""
features = 0
@ -553,7 +559,7 @@ class HomeKitClimateEntity(HomeKitEntity, ClimateEntity):
return features
@property
def temperature_unit(self):
def temperature_unit(self) -> str:
"""Return the unit of measurement."""
return TEMP_CELSIUS

View file

@ -1,6 +1,9 @@
"""Config flow to configure homekit_controller."""
from __future__ import annotations
import logging
import re
from typing import Any
import aiohomekit
from aiohomekit.exceptions import AuthenticationError
@ -55,20 +58,21 @@ INSECURE_CODES = {
}
def normalize_hkid(hkid):
def normalize_hkid(hkid: str) -> str:
"""Normalize a hkid so that it is safe to compare with other normalized hkids."""
return hkid.lower()
@callback
def find_existing_host(hass, serial):
def find_existing_host(hass, serial: str) -> config_entries.ConfigEntry | None:
"""Return a set of the configured hosts."""
for entry in hass.config_entries.async_entries(DOMAIN):
if entry.data.get("AccessoryPairingID") == serial:
return entry
return None
def ensure_pin_format(pin, allow_insecure_setup_codes=None):
def ensure_pin_format(pin: str, allow_insecure_setup_codes: Any = None) -> str:
"""
Ensure a pin code is correctly formatted.

View file

@ -1,7 +1,11 @@
"""Helpers for managing a pairing with a HomeKit accessory or bridge."""
from __future__ import annotations
import asyncio
from collections.abc import Callable
import datetime
import logging
from typing import Any
from aiohomekit.exceptions import (
AccessoryDisconnectedError,
@ -9,11 +13,11 @@ from aiohomekit.exceptions import (
EncryptionError,
)
from aiohomekit.model import Accessories, Accessory
from aiohomekit.model.characteristics import CharacteristicsTypes
from aiohomekit.model.services import ServicesTypes
from aiohomekit.model.characteristics import Characteristic, CharacteristicsTypes
from aiohomekit.model.services import Service, ServicesTypes
from homeassistant.const import ATTR_VIA_DEVICE
from homeassistant.core import callback
from homeassistant.core import CALLBACK_TYPE, callback
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.event import async_track_time_interval
@ -37,6 +41,10 @@ MAX_POLL_FAILURES_TO_DECLARE_UNAVAILABLE = 3
_LOGGER = logging.getLogger(__name__)
AddAccessoryCb = Callable[[Accessory], bool]
AddServiceCb = Callable[[Service], bool]
AddCharacteristicCb = Callable[[Characteristic], bool]
def valid_serial_number(serial):
"""Return if the serial number appears to be valid."""
@ -51,7 +59,7 @@ def valid_serial_number(serial):
class HKDevice:
"""HomeKit device."""
def __init__(self, hass, config_entry, pairing_data):
def __init__(self, hass, config_entry, pairing_data) -> None:
"""Initialise a generic HomeKit device."""
self.hass = hass
@ -71,28 +79,28 @@ class HKDevice:
self.entity_map = Accessories()
# A list of callbacks that turn HK accessories into entities
self.accessory_factories = []
self.accessory_factories: list[AddAccessoryCb] = []
# A list of callbacks that turn HK service metadata into entities
self.listeners = []
self.listeners: list[AddServiceCb] = []
# A list of callbacks that turn HK characteristics into entities
self.char_factories = []
self.char_factories: list[AddCharacteristicCb] = []
# The platorms we have forwarded the config entry so far. If a new
# accessory is added to a bridge we may have to load additional
# platforms. We don't want to load all platforms up front if its just
# a lightbulb. And we don't want to forward a config entry twice
# (triggers a Config entry already set up error)
self.platforms = set()
self.platforms: set[str] = set()
# This just tracks aid/iid pairs so we know if a HK service has been
# mapped to a HA entity.
self.entities = []
self.entities: list[tuple[int, int | None, int | None]] = []
# A map of aid -> device_id
# Useful when routing events to triggers
self.devices = {}
self.devices: dict[int, str] = {}
self.available = False
@ -100,13 +108,13 @@ class HKDevice:
# Current values of all characteristics homekit_controller is tracking.
# Key is a (accessory_id, characteristic_id) tuple.
self.current_state = {}
self.current_state: dict[tuple[int, int], Any] = {}
self.pollable_characteristics = []
self.pollable_characteristics: list[tuple[int, int]] = []
# If this is set polling is active and can be disabled by calling
# this method.
self._polling_interval_remover = None
self._polling_interval_remover: CALLBACK_TYPE | None = None
# Never allow concurrent polling of the same accessory or bridge
self._polling_lock = asyncio.Lock()
@ -116,33 +124,37 @@ class HKDevice:
# This is set to True if we can't rely on serial numbers to be unique
self.unreliable_serial_numbers = False
self.watchable_characteristics = []
self.watchable_characteristics: list[tuple[int, int]] = []
self.pairing.dispatcher_connect(self.process_new_events)
def add_pollable_characteristics(self, characteristics):
def add_pollable_characteristics(
self, characteristics: list[tuple[int, int]]
) -> None:
"""Add (aid, iid) pairs that we need to poll."""
self.pollable_characteristics.extend(characteristics)
def remove_pollable_characteristics(self, accessory_id):
def remove_pollable_characteristics(self, accessory_id: int) -> None:
"""Remove all pollable characteristics by accessory id."""
self.pollable_characteristics = [
char for char in self.pollable_characteristics if char[0] != accessory_id
]
def add_watchable_characteristics(self, characteristics):
def add_watchable_characteristics(
self, characteristics: list[tuple[int, int]]
) -> None:
"""Add (aid, iid) pairs that we need to poll."""
self.watchable_characteristics.extend(characteristics)
self.hass.async_create_task(self.pairing.subscribe(characteristics))
def remove_watchable_characteristics(self, accessory_id):
def remove_watchable_characteristics(self, accessory_id: int) -> None:
"""Remove all pollable characteristics by accessory id."""
self.watchable_characteristics = [
char for char in self.watchable_characteristics if char[0] != accessory_id
]
@callback
def async_set_available_state(self, available):
def async_set_available_state(self, available: bool) -> None:
"""Mark state of all entities on this connection when it becomes available or unavailable."""
_LOGGER.debug(
"Called async_set_available_state with %s for %s", available, self.unique_id
@ -152,7 +164,7 @@ class HKDevice:
self.available = available
self.hass.helpers.dispatcher.async_dispatcher_send(self.signal_state_updated)
async def async_setup(self):
async def async_setup(self) -> bool:
"""Prepare to use a paired HomeKit device in Home Assistant."""
cache = self.hass.data[ENTITY_MAP].get_map(self.unique_id)
if not cache:
@ -214,7 +226,7 @@ class HKDevice:
return device_info
@callback
def async_migrate_devices(self):
def async_migrate_devices(self) -> None:
"""Migrate legacy device entries from 3-tuples to 2-tuples."""
_LOGGER.debug(
"Migrating device registry entries for pairing %s", self.unique_id
@ -246,7 +258,7 @@ class HKDevice:
(DOMAIN, IDENTIFIER_LEGACY_SERIAL_NUMBER, serial_number)
)
device = device_registry.async_get_device(identifiers=identifiers)
device = device_registry.async_get_device(identifiers=identifiers) # type: ignore[arg-type]
if not device:
continue
@ -286,7 +298,7 @@ class HKDevice:
)
@callback
def async_create_devices(self):
def async_create_devices(self) -> None:
"""
Build device registry entries for all accessories paired with the bridge.
@ -315,7 +327,7 @@ class HKDevice:
self.devices = devices
@callback
def async_detect_workarounds(self):
def async_detect_workarounds(self) -> None:
"""Detect any workarounds that are needed for this pairing."""
unreliable_serial_numbers = False
@ -354,7 +366,7 @@ class HKDevice:
self.unreliable_serial_numbers = unreliable_serial_numbers
async def async_process_entity_map(self):
async def async_process_entity_map(self) -> None:
"""
Process the entity map and load any platforms or entities that need adding.
@ -388,18 +400,18 @@ class HKDevice:
await self.async_update()
async def async_unload(self):
async def async_unload(self) -> None:
"""Stop interacting with device and prepare for removal from hass."""
if self._polling_interval_remover:
self._polling_interval_remover()
await self.pairing.close()
return await self.hass.config_entries.async_unload_platforms(
await self.hass.config_entries.async_unload_platforms(
self.config_entry, self.platforms
)
async def async_refresh_entity_map(self, config_num):
async def async_refresh_entity_map(self, config_num: int) -> bool:
"""Handle setup of a HomeKit accessory."""
try:
self.accessories = await self.pairing.list_accessories_and_characteristics()
@ -419,26 +431,26 @@ class HKDevice:
return True
def add_accessory_factory(self, add_entities_cb):
def add_accessory_factory(self, add_entities_cb) -> None:
"""Add a callback to run when discovering new entities for accessories."""
self.accessory_factories.append(add_entities_cb)
self._add_new_entities_for_accessory([add_entities_cb])
def _add_new_entities_for_accessory(self, handlers):
def _add_new_entities_for_accessory(self, handlers) -> None:
for accessory in self.entity_map.accessories:
for handler in handlers:
if (accessory.aid, None) in self.entities:
if (accessory.aid, None, None) in self.entities:
continue
if handler(accessory):
self.entities.append((accessory.aid, None))
self.entities.append((accessory.aid, None, None))
break
def add_char_factory(self, add_entities_cb):
def add_char_factory(self, add_entities_cb) -> None:
"""Add a callback to run when discovering new entities for accessories."""
self.char_factories.append(add_entities_cb)
self._add_new_entities_for_char([add_entities_cb])
def _add_new_entities_for_char(self, handlers):
def _add_new_entities_for_char(self, handlers) -> None:
for accessory in self.entity_map.accessories:
for service in accessory.services:
for char in service.characteristics:
@ -449,33 +461,33 @@ class HKDevice:
self.entities.append((accessory.aid, service.iid, char.iid))
break
def add_listener(self, add_entities_cb):
def add_listener(self, add_entities_cb) -> None:
"""Add a callback to run when discovering new entities for services."""
self.listeners.append(add_entities_cb)
self._add_new_entities([add_entities_cb])
def add_entities(self):
def add_entities(self) -> None:
"""Process the entity map and create HA entities."""
self._add_new_entities(self.listeners)
self._add_new_entities_for_accessory(self.accessory_factories)
self._add_new_entities_for_char(self.char_factories)
def _add_new_entities(self, callbacks):
def _add_new_entities(self, callbacks) -> None:
for accessory in self.entity_map.accessories:
aid = accessory.aid
for service in accessory.services:
iid = service.iid
if (aid, iid) in self.entities:
if (aid, None, iid) in self.entities:
# Don't add the same entity again
continue
for listener in callbacks:
if listener(service):
self.entities.append((aid, iid))
self.entities.append((aid, None, iid))
break
async def async_load_platform(self, platform):
async def async_load_platform(self, platform: str) -> None:
"""Load a single platform idempotently."""
if platform in self.platforms:
return
@ -489,7 +501,7 @@ class HKDevice:
self.platforms.remove(platform)
raise
async def async_load_platforms(self):
async def async_load_platforms(self) -> None:
"""Load any platforms needed by this HomeKit device."""
tasks = []
for accessory in self.entity_map.accessories:
@ -558,7 +570,7 @@ class HKDevice:
_LOGGER.debug("Finished HomeKit controller update: %s", self.unique_id)
def process_new_events(self, new_values_dict):
def process_new_events(self, new_values_dict) -> None:
"""Process events from accessory into HA state."""
self.async_set_available_state(True)
@ -575,11 +587,11 @@ class HKDevice:
self.hass.helpers.dispatcher.async_dispatcher_send(self.signal_state_updated)
async def get_characteristics(self, *args, **kwargs):
async def get_characteristics(self, *args, **kwargs) -> dict[str, Any]:
"""Read latest state from homekit accessory."""
return await self.pairing.get_characteristics(*args, **kwargs)
async def put_characteristics(self, characteristics):
async def put_characteristics(self, characteristics) -> None:
"""Control a HomeKit device state from Home Assistant."""
results = await self.pairing.put_characteristics(characteristics)
@ -604,7 +616,7 @@ class HKDevice:
self.process_new_events(new_entity_state)
@property
def unique_id(self):
def unique_id(self) -> str:
"""
Return a unique id for this accessory or bridge.

View file

@ -1,10 +1,15 @@
"""Support for Homekit covers."""
from __future__ import annotations
from typing import Any
from aiohomekit.model.characteristics import CharacteristicsTypes
from aiohomekit.model.services import ServicesTypes
from aiohomekit.model.services import Service, ServicesTypes
from homeassistant.components.cover import (
ATTR_POSITION,
ATTR_TILT_POSITION,
DEVICE_CLASS_GARAGE,
SUPPORT_CLOSE,
SUPPORT_CLOSE_TILT,
SUPPORT_OPEN,
@ -46,7 +51,7 @@ async def async_setup_entry(
conn = hass.data[KNOWN_DEVICES][hkid]
@callback
def async_add_service(service):
def async_add_service(service: Service) -> bool:
if not (entity_class := ENTITY_TYPES.get(service.type)):
return False
info = {"aid": service.accessory.aid, "iid": service.iid}
@ -59,12 +64,9 @@ async def async_setup_entry(
class HomeKitGarageDoorCover(HomeKitEntity, CoverEntity):
"""Representation of a HomeKit Garage Door."""
@property
def device_class(self):
"""Define this cover as a garage door."""
return "garage"
_attr_device_class = DEVICE_CLASS_GARAGE
def get_characteristic_types(self):
def get_characteristic_types(self) -> list[str]:
"""Define the homekit characteristics the entity cares about."""
return [
CharacteristicsTypes.DOOR_STATE_CURRENT,
@ -73,47 +75,47 @@ class HomeKitGarageDoorCover(HomeKitEntity, CoverEntity):
]
@property
def supported_features(self):
def supported_features(self) -> int:
"""Flag supported features."""
return SUPPORT_OPEN | SUPPORT_CLOSE
@property
def _state(self):
def _state(self) -> str:
"""Return the current state of the garage door."""
value = self.service.value(CharacteristicsTypes.DOOR_STATE_CURRENT)
return CURRENT_GARAGE_STATE_MAP[value]
@property
def is_closed(self):
def is_closed(self) -> bool:
"""Return true if cover is closed, else False."""
return self._state == STATE_CLOSED
@property
def is_closing(self):
def is_closing(self) -> bool:
"""Return if the cover is closing or not."""
return self._state == STATE_CLOSING
@property
def is_opening(self):
def is_opening(self) -> bool:
"""Return if the cover is opening or not."""
return self._state == STATE_OPENING
async def async_open_cover(self, **kwargs):
async def async_open_cover(self, **kwargs: Any) -> None:
"""Send open command."""
await self.set_door_state(STATE_OPEN)
async def async_close_cover(self, **kwargs):
async def async_close_cover(self, **kwargs: Any) -> None:
"""Send close command."""
await self.set_door_state(STATE_CLOSED)
async def set_door_state(self, state):
async def set_door_state(self, state: str) -> None:
"""Send state command."""
await self.async_put_characteristics(
{CharacteristicsTypes.DOOR_STATE_TARGET: TARGET_GARAGE_STATE_MAP[state]}
)
@property
def extra_state_attributes(self):
def extra_state_attributes(self) -> dict[str, Any]:
"""Return the optional state attributes."""
obstruction_detected = self.service.value(
CharacteristicsTypes.OBSTRUCTION_DETECTED
@ -124,7 +126,7 @@ class HomeKitGarageDoorCover(HomeKitEntity, CoverEntity):
class HomeKitWindowCover(HomeKitEntity, CoverEntity):
"""Representation of a HomeKit Window or Window Covering."""
def get_characteristic_types(self):
def get_characteristic_types(self) -> list[str]:
"""Define the homekit characteristics the entity cares about."""
return [
CharacteristicsTypes.POSITION_STATE,
@ -139,7 +141,7 @@ class HomeKitWindowCover(HomeKitEntity, CoverEntity):
]
@property
def supported_features(self):
def supported_features(self) -> int:
"""Flag supported features."""
features = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION
@ -161,45 +163,45 @@ class HomeKitWindowCover(HomeKitEntity, CoverEntity):
return features
@property
def current_cover_position(self):
def current_cover_position(self) -> int:
"""Return the current position of cover."""
return self.service.value(CharacteristicsTypes.POSITION_CURRENT)
@property
def is_closed(self):
def is_closed(self) -> bool:
"""Return true if cover is closed, else False."""
return self.current_cover_position == 0
@property
def is_closing(self):
def is_closing(self) -> bool:
"""Return if the cover is closing or not."""
value = self.service.value(CharacteristicsTypes.POSITION_STATE)
state = CURRENT_WINDOW_STATE_MAP[value]
return state == STATE_CLOSING
@property
def is_opening(self):
def is_opening(self) -> bool:
"""Return if the cover is opening or not."""
value = self.service.value(CharacteristicsTypes.POSITION_STATE)
state = CURRENT_WINDOW_STATE_MAP[value]
return state == STATE_OPENING
@property
def is_horizontal_tilt(self):
def is_horizontal_tilt(self) -> bool:
"""Return True if the service has a horizontal tilt characteristic."""
return (
self.service.value(CharacteristicsTypes.HORIZONTAL_TILT_CURRENT) is not None
)
@property
def is_vertical_tilt(self):
def is_vertical_tilt(self) -> bool:
"""Return True if the service has a vertical tilt characteristic."""
return (
self.service.value(CharacteristicsTypes.VERTICAL_TILT_CURRENT) is not None
)
@property
def current_cover_tilt_position(self):
def current_cover_tilt_position(self) -> int:
"""Return current position of cover tilt."""
tilt_position = self.service.value(CharacteristicsTypes.VERTICAL_TILT_CURRENT)
if not tilt_position:
@ -208,26 +210,26 @@ class HomeKitWindowCover(HomeKitEntity, CoverEntity):
)
return tilt_position
async def async_stop_cover(self, **kwargs):
async def async_stop_cover(self, **kwargs: Any) -> None:
"""Send hold command."""
await self.async_put_characteristics({CharacteristicsTypes.POSITION_HOLD: 1})
async def async_open_cover(self, **kwargs):
async def async_open_cover(self, **kwargs: Any) -> None:
"""Send open command."""
await self.async_set_cover_position(position=100)
async def async_close_cover(self, **kwargs):
async def async_close_cover(self, **kwargs: Any) -> None:
"""Send close command."""
await self.async_set_cover_position(position=0)
async def async_set_cover_position(self, **kwargs):
async def async_set_cover_position(self, **kwargs: Any) -> None:
"""Send position command."""
position = kwargs[ATTR_POSITION]
await self.async_put_characteristics(
{CharacteristicsTypes.POSITION_TARGET: position}
)
async def async_set_cover_tilt_position(self, **kwargs):
async def async_set_cover_tilt_position(self, **kwargs: Any) -> None:
"""Move the cover tilt to a specific position."""
tilt_position = kwargs[ATTR_TILT_POSITION]
if self.is_vertical_tilt:
@ -240,7 +242,7 @@ class HomeKitWindowCover(HomeKitEntity, CoverEntity):
)
@property
def extra_state_attributes(self):
def extra_state_attributes(self) -> dict[str, Any]:
"""Return the optional state attributes."""
obstruction_detected = self.service.value(
CharacteristicsTypes.OBSTRUCTION_DETECTED

View file

@ -1,6 +1,10 @@
"""Support for Homekit fans."""
from __future__ import annotations
from typing import Any
from aiohomekit.model.characteristics import CharacteristicsTypes
from aiohomekit.model.services import ServicesTypes
from aiohomekit.model.services import Service, ServicesTypes
from homeassistant.components.fan import (
DIRECTION_FORWARD,
@ -30,9 +34,9 @@ class BaseHomeKitFan(HomeKitEntity, FanEntity):
# This must be set in subclasses to the name of a boolean characteristic
# that controls whether the fan is on or off.
on_characteristic = None
on_characteristic: str
def get_characteristic_types(self):
def get_characteristic_types(self) -> list[str]:
"""Define the homekit characteristics the entity cares about."""
return [
CharacteristicsTypes.SWING_MODE,
@ -42,12 +46,12 @@ class BaseHomeKitFan(HomeKitEntity, FanEntity):
]
@property
def is_on(self):
def is_on(self) -> bool:
"""Return true if device is on."""
return self.service.value(self.on_characteristic) == 1
@property
def percentage(self):
def percentage(self) -> int:
"""Return the current speed percentage."""
if not self.is_on:
return 0
@ -55,19 +59,19 @@ class BaseHomeKitFan(HomeKitEntity, FanEntity):
return self.service.value(CharacteristicsTypes.ROTATION_SPEED)
@property
def current_direction(self):
def current_direction(self) -> str:
"""Return the current direction of the fan."""
direction = self.service.value(CharacteristicsTypes.ROTATION_DIRECTION)
return HK_DIRECTION_TO_HA[direction]
@property
def oscillating(self):
def oscillating(self) -> bool:
"""Return whether or not the fan is currently oscillating."""
oscillating = self.service.value(CharacteristicsTypes.SWING_MODE)
return oscillating == 1
@property
def supported_features(self):
def supported_features(self) -> int:
"""Flag supported features."""
features = 0
@ -83,20 +87,20 @@ class BaseHomeKitFan(HomeKitEntity, FanEntity):
return features
@property
def speed_count(self):
def speed_count(self) -> int:
"""Speed count for the fan."""
return round(
min(self.service[CharacteristicsTypes.ROTATION_SPEED].maxValue or 100, 100)
/ max(1, self.service[CharacteristicsTypes.ROTATION_SPEED].minStep or 0)
)
async def async_set_direction(self, direction):
async def async_set_direction(self, direction: str) -> None:
"""Set the direction of the fan."""
await self.async_put_characteristics(
{CharacteristicsTypes.ROTATION_DIRECTION: DIRECTION_TO_HK[direction]}
)
async def async_set_percentage(self, percentage):
async def async_set_percentage(self, percentage: int) -> None:
"""Set the speed of the fan."""
if percentage == 0:
return await self.async_turn_off()
@ -105,17 +109,21 @@ class BaseHomeKitFan(HomeKitEntity, FanEntity):
{CharacteristicsTypes.ROTATION_SPEED: percentage}
)
async def async_oscillate(self, oscillating: bool):
async def async_oscillate(self, oscillating: bool) -> None:
"""Oscillate the fan."""
await self.async_put_characteristics(
{CharacteristicsTypes.SWING_MODE: 1 if oscillating else 0}
)
async def async_turn_on(
self, speed=None, percentage=None, preset_mode=None, **kwargs
):
self,
speed: str | None = None,
percentage: int | None = None,
preset_mode: str | None = None,
**kwargs: Any,
) -> None:
"""Turn the specified fan on."""
characteristics = {}
characteristics: dict[str, Any] = {}
if not self.is_on:
characteristics[self.on_characteristic] = True
@ -126,7 +134,7 @@ class BaseHomeKitFan(HomeKitEntity, FanEntity):
if characteristics:
await self.async_put_characteristics(characteristics)
async def async_turn_off(self, **kwargs):
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the specified fan off."""
await self.async_put_characteristics({self.on_characteristic: False})
@ -159,7 +167,7 @@ async def async_setup_entry(
conn = hass.data[KNOWN_DEVICES][hkid]
@callback
def async_add_service(service):
def async_add_service(service: Service) -> bool:
if not (entity_class := ENTITY_TYPES.get(service.type)):
return False
info = {"aid": service.accessory.aid, "iid": service.iid}

View file

@ -1,8 +1,10 @@
"""Support for HomeKit Controller humidifier."""
from __future__ import annotations
from typing import Any
from aiohomekit.model.characteristics import CharacteristicsTypes
from aiohomekit.model.services import ServicesTypes
from aiohomekit.model.services import Service, ServicesTypes
from homeassistant.components.humidifier import HumidifierDeviceClass, HumidifierEntity
from homeassistant.components.humidifier.const import (
@ -37,7 +39,7 @@ class HomeKitHumidifier(HomeKitEntity, HumidifierEntity):
_attr_device_class = HumidifierDeviceClass.HUMIDIFIER
def get_characteristic_types(self):
def get_characteristic_types(self) -> list[str]:
"""Define the homekit characteristics the entity cares about."""
return [
CharacteristicsTypes.ACTIVE,
@ -47,20 +49,20 @@ class HomeKitHumidifier(HomeKitEntity, HumidifierEntity):
]
@property
def supported_features(self):
def supported_features(self) -> int:
"""Return the list of supported features."""
return SUPPORT_FLAGS | SUPPORT_MODES
@property
def is_on(self):
def is_on(self) -> bool:
"""Return true if device is on."""
return self.service.value(CharacteristicsTypes.ACTIVE)
async def async_turn_on(self, **kwargs):
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):
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the specified valve off."""
await self.async_put_characteristics({CharacteristicsTypes.ACTIVE: False})
@ -138,7 +140,7 @@ class HomeKitDehumidifier(HomeKitEntity, HumidifierEntity):
_attr_device_class = HumidifierDeviceClass.DEHUMIDIFIER
def get_characteristic_types(self):
def get_characteristic_types(self) -> list[str]:
"""Define the homekit characteristics the entity cares about."""
return [
CharacteristicsTypes.ACTIVE,
@ -149,20 +151,20 @@ class HomeKitDehumidifier(HomeKitEntity, HumidifierEntity):
]
@property
def supported_features(self):
def supported_features(self) -> int:
"""Return the list of supported features."""
return SUPPORT_FLAGS | SUPPORT_MODES
@property
def is_on(self):
def is_on(self) -> bool:
"""Return true if device is on."""
return self.service.value(CharacteristicsTypes.ACTIVE)
async def async_turn_on(self, **kwargs):
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):
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the specified valve off."""
await self.async_put_characteristics({CharacteristicsTypes.ACTIVE: False})
@ -251,13 +253,13 @@ async def async_setup_entry(
conn = hass.data[KNOWN_DEVICES][hkid]
@callback
def async_add_service(service):
def async_add_service(service: Service) -> bool:
if service.type != ServicesTypes.HUMIDIFIER_DEHUMIDIFIER:
return False
info = {"aid": service.accessory.aid, "iid": service.iid}
entities = []
entities: list[HumidifierEntity] = []
if service.has(CharacteristicsTypes.RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD):
entities.append(HomeKitHumidifier(conn, info))

View file

@ -1,6 +1,10 @@
"""Support for Homekit lights."""
from __future__ import annotations
from typing import Any
from aiohomekit.model.characteristics import CharacteristicsTypes
from aiohomekit.model.services import ServicesTypes
from aiohomekit.model.services import Service, ServicesTypes
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
@ -28,7 +32,7 @@ async def async_setup_entry(
conn = hass.data[KNOWN_DEVICES][hkid]
@callback
def async_add_service(service):
def async_add_service(service: Service) -> bool:
if service.type != ServicesTypes.LIGHTBULB:
return False
info = {"aid": service.accessory.aid, "iid": service.iid}
@ -41,7 +45,7 @@ async def async_setup_entry(
class HomeKitLight(HomeKitEntity, LightEntity):
"""Representation of a Homekit light."""
def get_characteristic_types(self):
def get_characteristic_types(self) -> list[str]:
"""Define the homekit characteristics the entity cares about."""
return [
CharacteristicsTypes.ON,
@ -52,17 +56,17 @@ class HomeKitLight(HomeKitEntity, LightEntity):
]
@property
def is_on(self):
def is_on(self) -> bool:
"""Return true if device is on."""
return self.service.value(CharacteristicsTypes.ON)
@property
def brightness(self):
def brightness(self) -> int:
"""Return the brightness of this light between 0..255."""
return self.service.value(CharacteristicsTypes.BRIGHTNESS) * 255 / 100
@property
def hs_color(self):
def hs_color(self) -> tuple[float, float]:
"""Return the color property."""
return (
self.service.value(CharacteristicsTypes.HUE),
@ -70,12 +74,12 @@ class HomeKitLight(HomeKitEntity, LightEntity):
)
@property
def color_temp(self):
def color_temp(self) -> int:
"""Return the color temperature."""
return self.service.value(CharacteristicsTypes.COLOR_TEMPERATURE)
@property
def supported_features(self):
def supported_features(self) -> int:
"""Flag supported features."""
features = 0
@ -93,7 +97,7 @@ class HomeKitLight(HomeKitEntity, LightEntity):
return features
async def async_turn_on(self, **kwargs):
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the specified light on."""
hs_color = kwargs.get(ATTR_HS_COLOR)
temperature = kwargs.get(ATTR_COLOR_TEMP)
@ -121,6 +125,6 @@ class HomeKitLight(HomeKitEntity, LightEntity):
await self.async_put_characteristics(characteristics)
async def async_turn_off(self, **kwargs):
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the specified light off."""
await self.async_put_characteristics({CharacteristicsTypes.ON: False})

View file

@ -1,6 +1,10 @@
"""Support for HomeKit Controller locks."""
from __future__ import annotations
from typing import Any
from aiohomekit.model.characteristics import CharacteristicsTypes
from aiohomekit.model.services import ServicesTypes
from aiohomekit.model.services import Service, ServicesTypes
from homeassistant.components.lock import STATE_JAMMED, LockEntity
from homeassistant.config_entries import ConfigEntry
@ -37,7 +41,7 @@ async def async_setup_entry(
conn = hass.data[KNOWN_DEVICES][hkid]
@callback
def async_add_service(service):
def async_add_service(service: Service) -> bool:
if service.type != ServicesTypes.LOCK_MECHANISM:
return False
info = {"aid": service.accessory.aid, "iid": service.iid}
@ -50,7 +54,7 @@ async def async_setup_entry(
class HomeKitLock(HomeKitEntity, LockEntity):
"""Representation of a HomeKit Controller Lock."""
def get_characteristic_types(self):
def get_characteristic_types(self) -> list[str]:
"""Define the homekit characteristics the entity cares about."""
return [
CharacteristicsTypes.LOCK_MECHANISM_CURRENT_STATE,
@ -59,7 +63,7 @@ class HomeKitLock(HomeKitEntity, LockEntity):
]
@property
def is_locked(self):
def is_locked(self) -> bool | None:
"""Return true if device is locked."""
value = self.service.value(CharacteristicsTypes.LOCK_MECHANISM_CURRENT_STATE)
if CURRENT_STATE_MAP[value] == STATE_UNKNOWN:
@ -67,7 +71,7 @@ class HomeKitLock(HomeKitEntity, LockEntity):
return CURRENT_STATE_MAP[value] == STATE_LOCKED
@property
def is_locking(self):
def is_locking(self) -> bool:
"""Return true if device is locking."""
current_value = self.service.value(
CharacteristicsTypes.LOCK_MECHANISM_CURRENT_STATE
@ -81,7 +85,7 @@ class HomeKitLock(HomeKitEntity, LockEntity):
)
@property
def is_unlocking(self):
def is_unlocking(self) -> bool:
"""Return true if device is unlocking."""
current_value = self.service.value(
CharacteristicsTypes.LOCK_MECHANISM_CURRENT_STATE
@ -95,27 +99,27 @@ class HomeKitLock(HomeKitEntity, LockEntity):
)
@property
def is_jammed(self):
def is_jammed(self) -> bool:
"""Return true if device is jammed."""
value = self.service.value(CharacteristicsTypes.LOCK_MECHANISM_CURRENT_STATE)
return CURRENT_STATE_MAP[value] == STATE_JAMMED
async def async_lock(self, **kwargs):
async def async_lock(self, **kwargs: Any) -> None:
"""Lock the device."""
await self._set_lock_state(STATE_LOCKED)
async def async_unlock(self, **kwargs):
async def async_unlock(self, **kwargs: Any) -> None:
"""Unlock the device."""
await self._set_lock_state(STATE_UNLOCKED)
async def _set_lock_state(self, state):
async def _set_lock_state(self, state: str) -> None:
"""Send state command."""
await self.async_put_characteristics(
{CharacteristicsTypes.LOCK_MECHANISM_TARGET_STATE: TARGET_STATE_MAP[state]}
)
@property
def extra_state_attributes(self):
def extra_state_attributes(self) -> dict[str, Any]:
"""Return the optional state attributes."""
attributes = {}

View file

@ -1,4 +1,6 @@
"""Support for HomeKit Controller Televisions."""
from __future__ import annotations
import logging
from aiohomekit.model.characteristics import (
@ -7,7 +9,7 @@ from aiohomekit.model.characteristics import (
RemoteKeyValues,
TargetMediaStateValues,
)
from aiohomekit.model.services import ServicesTypes
from aiohomekit.model.services import Service, ServicesTypes
from aiohomekit.utils import clamp_enum_to_char
from homeassistant.components.media_player import (
@ -53,7 +55,7 @@ async def async_setup_entry(
conn = hass.data[KNOWN_DEVICES][hkid]
@callback
def async_add_service(service):
def async_add_service(service: Service) -> bool:
if service.type != ServicesTypes.TELEVISION:
return False
info = {"aid": service.accessory.aid, "iid": service.iid}
@ -68,7 +70,7 @@ class HomeKitTelevision(HomeKitEntity, MediaPlayerEntity):
_attr_device_class = MediaPlayerDeviceClass.TV
def get_characteristic_types(self):
def get_characteristic_types(self) -> list[str]:
"""Define the homekit characteristics the entity cares about."""
return [
CharacteristicsTypes.ACTIVE,
@ -82,7 +84,7 @@ class HomeKitTelevision(HomeKitEntity, MediaPlayerEntity):
]
@property
def supported_features(self):
def supported_features(self) -> int:
"""Flag media player features that are supported."""
features = 0
@ -108,10 +110,10 @@ class HomeKitTelevision(HomeKitEntity, MediaPlayerEntity):
return features
@property
def supported_media_states(self):
def supported_media_states(self) -> set[TargetMediaStateValues]:
"""Mediate state flags that are supported."""
if not self.service.has(CharacteristicsTypes.TARGET_MEDIA_STATE):
return frozenset()
return set()
return clamp_enum_to_char(
TargetMediaStateValues,
@ -119,17 +121,17 @@ class HomeKitTelevision(HomeKitEntity, MediaPlayerEntity):
)
@property
def supported_remote_keys(self):
def supported_remote_keys(self) -> set[str]:
"""Remote key buttons that are supported."""
if not self.service.has(CharacteristicsTypes.REMOTE_KEY):
return frozenset()
return set()
return clamp_enum_to_char(
RemoteKeyValues, self.service[CharacteristicsTypes.REMOTE_KEY]
)
@property
def source_list(self):
def source_list(self) -> list[str]:
"""List of all input sources for this television."""
sources = []
@ -147,7 +149,7 @@ class HomeKitTelevision(HomeKitEntity, MediaPlayerEntity):
return sources
@property
def source(self):
def source(self) -> str | None:
"""Name of the current input source."""
active_identifier = self.service.value(CharacteristicsTypes.ACTIVE_IDENTIFIER)
if not active_identifier:
@ -165,7 +167,7 @@ class HomeKitTelevision(HomeKitEntity, MediaPlayerEntity):
return char.value
@property
def state(self):
def state(self) -> str:
"""State of the tv."""
active = self.service.value(CharacteristicsTypes.ACTIVE)
if not active:
@ -177,7 +179,7 @@ class HomeKitTelevision(HomeKitEntity, MediaPlayerEntity):
return STATE_OK
async def async_media_play(self):
async def async_media_play(self) -> None:
"""Send play command."""
if self.state == STATE_PLAYING:
_LOGGER.debug("Cannot play while already playing")
@ -192,7 +194,7 @@ class HomeKitTelevision(HomeKitEntity, MediaPlayerEntity):
{CharacteristicsTypes.REMOTE_KEY: RemoteKeyValues.PLAY_PAUSE}
)
async def async_media_pause(self):
async def async_media_pause(self) -> None:
"""Send pause command."""
if self.state == STATE_PAUSED:
_LOGGER.debug("Cannot pause while already paused")
@ -207,7 +209,7 @@ class HomeKitTelevision(HomeKitEntity, MediaPlayerEntity):
{CharacteristicsTypes.REMOTE_KEY: RemoteKeyValues.PLAY_PAUSE}
)
async def async_media_stop(self):
async def async_media_stop(self) -> None:
"""Send stop command."""
if self.state == STATE_IDLE:
_LOGGER.debug("Cannot stop when already idle")
@ -218,7 +220,7 @@ class HomeKitTelevision(HomeKitEntity, MediaPlayerEntity):
{CharacteristicsTypes.TARGET_MEDIA_STATE: TargetMediaStateValues.STOP}
)
async def async_select_source(self, source):
async def async_select_source(self, source: str) -> None:
"""Switch to a different media source."""
this_accessory = self._accessory.entity_map.aid(self._aid)
this_tv = this_accessory.services.iid(self._iid)

View file

@ -13,8 +13,10 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType
from . import KNOWN_DEVICES, CharacteristicEntity
from .connection import HKDevice
NUMBER_ENTITIES: dict[str, NumberEntityDescription] = {
CharacteristicsTypes.VENDOR_VOCOLINC_HUMIDIFIER_SPRAY_LEVEL: NumberEntityDescription(
@ -90,7 +92,7 @@ async def async_setup_entry(
conn = hass.data[KNOWN_DEVICES][hkid]
@callback
def async_add_characteristic(char: Characteristic):
def async_add_characteristic(char: Characteristic) -> bool:
entities = []
info = {"aid": char.service.accessory.aid, "iid": char.service.iid}
@ -112,11 +114,11 @@ class HomeKitNumber(CharacteristicEntity, NumberEntity):
def __init__(
self,
conn,
info,
char,
conn: HKDevice,
info: ConfigType,
char: Characteristic,
description: NumberEntityDescription,
):
) -> None:
"""Initialise a HomeKit number control."""
self.entity_description = description
super().__init__(conn, info, char)
@ -128,7 +130,7 @@ class HomeKitNumber(CharacteristicEntity, NumberEntity):
return f"{prefix} {self.entity_description.name}"
return self.entity_description.name
def get_characteristic_types(self):
def get_characteristic_types(self) -> list[str]:
"""Define the homekit characteristics the entity is tracking."""
return [self._char.type]
@ -152,7 +154,7 @@ class HomeKitNumber(CharacteristicEntity, NumberEntity):
"""Return the current characteristic value."""
return self._char.value
async def async_set_value(self, value: float):
async def async_set_value(self, value: float) -> None:
"""Set the characteristic to this value."""
await self.async_put_characteristics(
{
@ -164,7 +166,7 @@ class HomeKitNumber(CharacteristicEntity, NumberEntity):
class HomeKitEcobeeFanModeNumber(CharacteristicEntity, NumberEntity):
"""Representation of a Number control for Ecobee Fan Mode request."""
def get_characteristic_types(self):
def get_characteristic_types(self) -> list[str]:
"""Define the homekit characteristics the entity is tracking."""
return [self._char.type]
@ -196,7 +198,7 @@ class HomeKitEcobeeFanModeNumber(CharacteristicEntity, NumberEntity):
"""Return the current characteristic value."""
return self._char.value
async def async_set_value(self, value: float):
async def async_set_value(self, value: float) -> None:
"""Set the characteristic to this value."""
# Sending the fan mode request sometimes ends up getting ignored by ecobee

View file

@ -32,7 +32,7 @@ class EcobeeModeSelect(CharacteristicEntity, SelectEntity):
return f"{name} Current Mode"
return "Current Mode"
def get_characteristic_types(self):
def get_characteristic_types(self) -> list[str]:
"""Define the homekit characteristics the entity cares about."""
return [
CharacteristicsTypes.VENDOR_ECOBEE_CURRENT_MODE,
@ -61,7 +61,7 @@ async def async_setup_entry(
conn = hass.data[KNOWN_DEVICES][hkid]
@callback
def async_add_characteristic(char: Characteristic):
def async_add_characteristic(char: Characteristic) -> bool:
if char.type == CharacteristicsTypes.VENDOR_ECOBEE_CURRENT_MODE:
info = {"aid": char.service.accessory.aid, "iid": char.service.iid}
async_add_entities([EcobeeModeSelect(conn, info, char)])

View file

@ -5,7 +5,7 @@ from collections.abc import Callable
from dataclasses import dataclass
from aiohomekit.model.characteristics import Characteristic, CharacteristicsTypes
from aiohomekit.model.services import ServicesTypes
from aiohomekit.model.services import Service, ServicesTypes
from homeassistant.components.sensor import (
SensorDeviceClass,
@ -28,8 +28,10 @@ from homeassistant.const import (
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType
from . import KNOWN_DEVICES, CharacteristicEntity, HomeKitEntity
from .connection import HKDevice
CO2_ICON = "mdi:molecule-co2"
@ -203,17 +205,17 @@ class HomeKitHumiditySensor(HomeKitEntity, SensorEntity):
_attr_device_class = SensorDeviceClass.HUMIDITY
_attr_native_unit_of_measurement = PERCENTAGE
def get_characteristic_types(self):
def get_characteristic_types(self) -> list[str]:
"""Define the homekit characteristics the entity is tracking."""
return [CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT]
@property
def name(self):
def name(self) -> str:
"""Return the name of the device."""
return f"{super().name} Humidity"
@property
def native_value(self):
def native_value(self) -> float:
"""Return the current humidity."""
return self.service.value(CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT)
@ -224,17 +226,17 @@ class HomeKitTemperatureSensor(HomeKitEntity, SensorEntity):
_attr_device_class = SensorDeviceClass.TEMPERATURE
_attr_native_unit_of_measurement = TEMP_CELSIUS
def get_characteristic_types(self):
def get_characteristic_types(self) -> list[str]:
"""Define the homekit characteristics the entity is tracking."""
return [CharacteristicsTypes.TEMPERATURE_CURRENT]
@property
def name(self):
def name(self) -> str:
"""Return the name of the device."""
return f"{super().name} Temperature"
@property
def native_value(self):
def native_value(self) -> float:
"""Return the current temperature in Celsius."""
return self.service.value(CharacteristicsTypes.TEMPERATURE_CURRENT)
@ -245,17 +247,17 @@ class HomeKitLightSensor(HomeKitEntity, SensorEntity):
_attr_device_class = SensorDeviceClass.ILLUMINANCE
_attr_native_unit_of_measurement = LIGHT_LUX
def get_characteristic_types(self):
def get_characteristic_types(self) -> list[str]:
"""Define the homekit characteristics the entity is tracking."""
return [CharacteristicsTypes.LIGHT_LEVEL_CURRENT]
@property
def name(self):
def name(self) -> str:
"""Return the name of the device."""
return f"{super().name} Light Level"
@property
def native_value(self):
def native_value(self) -> int:
"""Return the current light level in lux."""
return self.service.value(CharacteristicsTypes.LIGHT_LEVEL_CURRENT)
@ -266,17 +268,17 @@ class HomeKitCarbonDioxideSensor(HomeKitEntity, SensorEntity):
_attr_icon = CO2_ICON
_attr_native_unit_of_measurement = CONCENTRATION_PARTS_PER_MILLION
def get_characteristic_types(self):
def get_characteristic_types(self) -> list[str]:
"""Define the homekit characteristics the entity is tracking."""
return [CharacteristicsTypes.CARBON_DIOXIDE_LEVEL]
@property
def name(self):
def name(self) -> str:
"""Return the name of the device."""
return f"{super().name} CO2"
@property
def native_value(self):
def native_value(self) -> int:
"""Return the current CO2 level in ppm."""
return self.service.value(CharacteristicsTypes.CARBON_DIOXIDE_LEVEL)
@ -287,7 +289,7 @@ class HomeKitBatterySensor(HomeKitEntity, SensorEntity):
_attr_device_class = SensorDeviceClass.BATTERY
_attr_native_unit_of_measurement = PERCENTAGE
def get_characteristic_types(self):
def get_characteristic_types(self) -> list[str]:
"""Define the homekit characteristics the entity is tracking."""
return [
CharacteristicsTypes.BATTERY_LEVEL,
@ -296,12 +298,12 @@ class HomeKitBatterySensor(HomeKitEntity, SensorEntity):
]
@property
def name(self):
def name(self) -> str:
"""Return the name of the device."""
return f"{super().name} Battery"
@property
def icon(self):
def icon(self) -> str:
"""Return the sensor icon."""
if not self.available or self.state is None:
return "mdi:battery-unknown"
@ -323,12 +325,12 @@ class HomeKitBatterySensor(HomeKitEntity, SensorEntity):
return icon
@property
def is_low_battery(self):
def is_low_battery(self) -> bool:
"""Return true if battery level is low."""
return self.service.value(CharacteristicsTypes.STATUS_LO_BATT) == 1
@property
def is_charging(self):
def is_charging(self) -> bool:
"""Return true if currently charing."""
# 0 = not charging
# 1 = charging
@ -336,7 +338,7 @@ class HomeKitBatterySensor(HomeKitEntity, SensorEntity):
return self.service.value(CharacteristicsTypes.CHARGING_STATE) == 1
@property
def native_value(self):
def native_value(self) -> int:
"""Return the current battery level percentage."""
return self.service.value(CharacteristicsTypes.BATTERY_LEVEL)
@ -356,16 +358,16 @@ class SimpleSensor(CharacteristicEntity, SensorEntity):
def __init__(
self,
conn,
info,
char,
conn: HKDevice,
info: ConfigType,
char: Characteristic,
description: HomeKitSensorEntityDescription,
):
) -> None:
"""Initialise a secondary HomeKit characteristic sensor."""
self.entity_description = description
super().__init__(conn, info, char)
def get_characteristic_types(self):
def get_characteristic_types(self) -> list[str]:
"""Define the homekit characteristics the entity is tracking."""
return [self._char.type]
@ -375,7 +377,7 @@ class SimpleSensor(CharacteristicEntity, SensorEntity):
return f"{super().name} {self.entity_description.name}"
@property
def native_value(self):
def native_value(self) -> str | int | float:
"""Return the current sensor value."""
return self._char.value
@ -399,7 +401,7 @@ async def async_setup_entry(
conn = hass.data[KNOWN_DEVICES][hkid]
@callback
def async_add_service(service):
def async_add_service(service: Service) -> bool:
if not (entity_class := ENTITY_TYPES.get(service.type)):
return False
info = {"aid": service.accessory.aid, "iid": service.iid}
@ -409,7 +411,7 @@ async def async_setup_entry(
conn.add_listener(async_add_service)
@callback
def async_add_characteristic(char: Characteristic):
def async_add_characteristic(char: Characteristic) -> bool:
if not (description := SIMPLE_SENSOR.get(char.type)):
return False
if description.probe and not description.probe(char):

View file

@ -1,6 +1,10 @@
"""Helpers for HomeKit data stored in HA storage."""
from homeassistant.core import callback
from __future__ import annotations
from typing import Any, TypedDict, cast
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.storage import Store
from .const import DOMAIN
@ -10,6 +14,19 @@ ENTITY_MAP_STORAGE_VERSION = 1
ENTITY_MAP_SAVE_DELAY = 10
class Pairing(TypedDict):
"""A versioned map of entity metadata as presented by aiohomekit."""
config_num: int
accessories: list[Any]
class StorageLayout(TypedDict):
"""Cached pairing metadata needed by aiohomekit."""
pairings: dict[str, Pairing]
class EntityMapStorage:
"""
Holds a cache of entity structure data from a paired HomeKit device.
@ -26,34 +43,36 @@ class EntityMapStorage:
very slow for these devices.
"""
def __init__(self, hass):
def __init__(self, hass: HomeAssistant) -> None:
"""Create a new entity map store."""
self.hass = hass
self.store = Store(hass, ENTITY_MAP_STORAGE_VERSION, ENTITY_MAP_STORAGE_KEY)
self.storage_data = {}
self.storage_data: dict[str, Pairing] = {}
async def async_initialize(self):
async def async_initialize(self) -> None:
"""Get the pairing cache data."""
if not (raw_storage := await self.store.async_load()):
if not (raw_storage := cast(StorageLayout, await self.store.async_load())):
# There is no cached data about HomeKit devices yet
return
self.storage_data = raw_storage.get("pairings", {})
def get_map(self, homekit_id):
def get_map(self, homekit_id) -> Pairing | None:
"""Get a pairing cache item."""
return self.storage_data.get(homekit_id)
@callback
def async_create_or_update_map(self, homekit_id, config_num, accessories):
def async_create_or_update_map(
self, homekit_id: str, config_num: int, accessories: list[Any]
) -> Pairing:
"""Create a new pairing cache."""
data = {"config_num": config_num, "accessories": accessories}
data = Pairing(config_num=config_num, accessories=accessories)
self.storage_data[homekit_id] = data
self._async_schedule_save()
return data
@callback
def async_delete_map(self, homekit_id):
def async_delete_map(self, homekit_id: str) -> None:
"""Delete pairing cache."""
if homekit_id not in self.storage_data:
return
@ -62,11 +81,11 @@ class EntityMapStorage:
self._async_schedule_save()
@callback
def _async_schedule_save(self):
def _async_schedule_save(self) -> None:
"""Schedule saving the entity map cache."""
self.store.async_delay_save(self._data_to_save, ENTITY_MAP_SAVE_DELAY)
@callback
def _data_to_save(self):
def _data_to_save(self) -> dict[str, Any]:
"""Return data of entity map to store in a file."""
return {"pairings": self.storage_data}

View file

@ -2,6 +2,7 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Any
from aiohomekit.model.characteristics import (
Characteristic,
@ -9,15 +10,17 @@ from aiohomekit.model.characteristics import (
InUseValues,
IsConfiguredValues,
)
from aiohomekit.model.services import ServicesTypes
from aiohomekit.model.services import Service, ServicesTypes
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType
from . import KNOWN_DEVICES, CharacteristicEntity, HomeKitEntity
from .connection import HKDevice
OUTLET_IN_USE = "outlet_in_use"
@ -53,35 +56,36 @@ SWITCH_ENTITIES: dict[str, DeclarativeSwitchEntityDescription] = {
class HomeKitSwitch(HomeKitEntity, SwitchEntity):
"""Representation of a Homekit switch."""
def get_characteristic_types(self):
def get_characteristic_types(self) -> list[str]:
"""Define the homekit characteristics the entity cares about."""
return [CharacteristicsTypes.ON, CharacteristicsTypes.OUTLET_IN_USE]
@property
def is_on(self):
def is_on(self) -> bool:
"""Return true if device is on."""
return self.service.value(CharacteristicsTypes.ON)
async def async_turn_on(self, **kwargs):
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the specified switch on."""
await self.async_put_characteristics({CharacteristicsTypes.ON: True})
async def async_turn_off(self, **kwargs):
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the specified switch off."""
await self.async_put_characteristics({CharacteristicsTypes.ON: False})
@property
def extra_state_attributes(self):
def extra_state_attributes(self) -> dict[str, Any] | None:
"""Return the optional state attributes."""
outlet_in_use = self.service.value(CharacteristicsTypes.OUTLET_IN_USE)
if outlet_in_use is not None:
return {OUTLET_IN_USE: outlet_in_use}
return None
class HomeKitValve(HomeKitEntity, SwitchEntity):
"""Represents a valve in an irrigation system."""
def get_characteristic_types(self):
def get_characteristic_types(self) -> list[str]:
"""Define the homekit characteristics the entity cares about."""
return [
CharacteristicsTypes.ACTIVE,
@ -90,11 +94,11 @@ class HomeKitValve(HomeKitEntity, SwitchEntity):
CharacteristicsTypes.REMAINING_DURATION,
]
async def async_turn_on(self, **kwargs):
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):
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the specified valve off."""
await self.async_put_characteristics({CharacteristicsTypes.ACTIVE: False})
@ -104,12 +108,12 @@ class HomeKitValve(HomeKitEntity, SwitchEntity):
return "mdi:water"
@property
def is_on(self):
def is_on(self) -> bool:
"""Return true if device is on."""
return self.service.value(CharacteristicsTypes.ACTIVE)
@property
def extra_state_attributes(self):
def extra_state_attributes(self) -> dict[str, Any]:
"""Return the optional state attributes."""
attrs = {}
@ -133,13 +137,13 @@ class DeclarativeCharacteristicSwitch(CharacteristicEntity, SwitchEntity):
def __init__(
self,
conn,
info,
char,
conn: HKDevice,
info: ConfigType,
char: Characteristic,
description: DeclarativeSwitchEntityDescription,
):
) -> None:
"""Initialise a HomeKit switch."""
self.entity_description = description
self.entity_description: DeclarativeSwitchEntityDescription = description
super().__init__(conn, info, char)
@property
@ -149,22 +153,22 @@ class DeclarativeCharacteristicSwitch(CharacteristicEntity, SwitchEntity):
return f"{prefix} {self.entity_description.name}"
return self.entity_description.name
def get_characteristic_types(self):
def get_characteristic_types(self) -> list[str]:
"""Define the homekit characteristics the entity cares about."""
return [self._char.type]
@property
def is_on(self):
def is_on(self) -> bool:
"""Return true if device is on."""
return self._char.value == self.entity_description.true_value
async def async_turn_on(self, **kwargs):
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the specified switch on."""
await self.async_put_characteristics(
{self._char.type: self.entity_description.true_value}
)
async def async_turn_off(self, **kwargs):
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the specified switch off."""
await self.async_put_characteristics(
{self._char.type: self.entity_description.false_value}
@ -188,7 +192,7 @@ async def async_setup_entry(
conn = hass.data[KNOWN_DEVICES][hkid]
@callback
def async_add_service(service):
def async_add_service(service: Service) -> bool:
if not (entity_class := ENTITY_TYPES.get(service.type)):
return False
info = {"aid": service.accessory.aid, "iid": service.iid}
@ -198,7 +202,7 @@ async def async_setup_entry(
conn.add_listener(async_add_service)
@callback
def async_add_characteristic(char: Characteristic):
def async_add_characteristic(char: Characteristic) -> bool:
if not (description := SWITCH_ENTITIES.get(char.type)):
return False