Strict typing for homekit part 1 (#67657)

Co-authored-by: Franck Nijhof <frenck@frenck.nl>
This commit is contained in:
J. Nick Koston 2022-03-29 23:21:07 -10:00 committed by GitHub
parent 496d90bf00
commit af6a62ca79
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 443 additions and 217 deletions

View file

@ -99,6 +99,14 @@ homeassistant.components.group.*
homeassistant.components.guardian.* homeassistant.components.guardian.*
homeassistant.components.history.* homeassistant.components.history.*
homeassistant.components.homeassistant.triggers.event homeassistant.components.homeassistant.triggers.event
homeassistant.components.homekit
homeassistant.components.homekit.accessories
homeassistant.components.homekit.aidmanager
homeassistant.components.homekit.config_flow
homeassistant.components.homekit.diagnostics
homeassistant.components.homekit.logbook
homeassistant.components.homekit.type_triggers
homeassistant.components.homekit.util
homeassistant.components.homekit_controller homeassistant.components.homekit_controller
homeassistant.components.homekit_controller.alarm_control_panel homeassistant.components.homekit_controller.alarm_control_panel
homeassistant.components.homekit_controller.button homeassistant.components.homekit_controller.button

View file

@ -2,14 +2,18 @@
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
from collections.abc import Iterable
from copy import deepcopy from copy import deepcopy
import ipaddress import ipaddress
import logging import logging
import os import os
from typing import Any, cast
from uuid import UUID
from aiohttp import web from aiohttp import web
from pyhap.const import STANDALONE_AID from pyhap.const import STANDALONE_AID
import voluptuous as vol import voluptuous as vol
from zeroconf.asyncio import AsyncZeroconf
from homeassistant.components import device_automation, network, zeroconf from homeassistant.components import device_automation, network, zeroconf
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
@ -39,7 +43,7 @@ from homeassistant.const import (
EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_STOP,
SERVICE_RELOAD, SERVICE_RELOAD,
) )
from homeassistant.core import CoreState, HomeAssistant, ServiceCall, callback from homeassistant.core import CoreState, HomeAssistant, ServiceCall, State, callback
from homeassistant.exceptions import HomeAssistantError, Unauthorized from homeassistant.exceptions import HomeAssistantError, Unauthorized
from homeassistant.helpers import device_registry, entity_registry from homeassistant.helpers import device_registry, entity_registry
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -67,7 +71,7 @@ from . import ( # noqa: F401
type_switches, type_switches,
type_thermostats, type_thermostats,
) )
from .accessories import HomeBridge, HomeDriver, get_accessory from .accessories import HomeAccessory, HomeBridge, HomeDriver, get_accessory
from .aidmanager import AccessoryAidStorage from .aidmanager import AccessoryAidStorage
from .const import ( from .const import (
ATTR_INTEGRATION, ATTR_INTEGRATION,
@ -114,7 +118,7 @@ from .util import (
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
MAX_DEVICES = 150 MAX_DEVICES = 150 # includes the bridge
# #### Driver Status #### # #### Driver Status ####
STATUS_READY = 0 STATUS_READY = 0
@ -129,7 +133,9 @@ _HOMEKIT_CONFIG_UPDATE_TIME = (
) )
def _has_all_unique_names_and_ports(bridges): def _has_all_unique_names_and_ports(
bridges: list[dict[str, Any]]
) -> list[dict[str, Any]]:
"""Validate that each homekit bridge configured has a unique name.""" """Validate that each homekit bridge configured has a unique name."""
names = [bridge[CONF_NAME] for bridge in bridges] names = [bridge[CONF_NAME] for bridge in bridges]
ports = [bridge[CONF_PORT] for bridge in bridges] ports = [bridge[CONF_PORT] for bridge in bridges]
@ -184,7 +190,9 @@ def _async_all_homekit_instances(hass: HomeAssistant) -> list[HomeKit]:
] ]
def _async_get_entries_by_name(current_entries): def _async_get_entries_by_name(
current_entries: list[ConfigEntry],
) -> dict[str, ConfigEntry]:
"""Return a dict of the entries by name.""" """Return a dict of the entries by name."""
# For backwards compat, its possible the first bridge is using the default # For backwards compat, its possible the first bridge is using the default
@ -221,7 +229,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
@callback @callback
def _async_update_config_entry_if_from_yaml(hass, entries_by_name, conf): def _async_update_config_entry_if_from_yaml(
hass: HomeAssistant, entries_by_name: dict[str, ConfigEntry], conf: ConfigType
) -> bool:
"""Update a config entry with the latest yaml. """Update a config entry with the latest yaml.
Returns True if a matching config entry was found Returns True if a matching config entry was found
@ -346,13 +356,15 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Remove a config entry.""" """Remove a config entry."""
return await hass.async_add_executor_job( await hass.async_add_executor_job(
remove_state_files_for_entry_id, hass, entry.entry_id remove_state_files_for_entry_id, hass, entry.entry_id
) )
@callback @callback
def _async_import_options_from_data_if_missing(hass: HomeAssistant, entry: ConfigEntry): def _async_import_options_from_data_if_missing(
hass: HomeAssistant, entry: ConfigEntry
) -> None:
options = deepcopy(dict(entry.options)) options = deepcopy(dict(entry.options))
data = deepcopy(dict(entry.data)) data = deepcopy(dict(entry.data))
modified = False modified = False
@ -367,7 +379,7 @@ def _async_import_options_from_data_if_missing(hass: HomeAssistant, entry: Confi
@callback @callback
def _async_register_events_and_services(hass: HomeAssistant): def _async_register_events_and_services(hass: HomeAssistant) -> None:
"""Register events and services for HomeKit.""" """Register events and services for HomeKit."""
hass.http.register_view(HomeKitPairingQRView) hass.http.register_view(HomeKitPairingQRView)
@ -381,7 +393,7 @@ def _async_register_events_and_services(hass: HomeAssistant):
) )
continue continue
entity_ids = service.data.get("entity_id") entity_ids = cast(list[str], service.data.get("entity_id"))
await homekit.async_reset_accessories(entity_ids) await homekit.async_reset_accessories(entity_ids)
hass.services.async_register( hass.services.async_register(
@ -453,27 +465,29 @@ def _async_register_events_and_services(hass: HomeAssistant):
class HomeKit: class HomeKit:
"""Class to handle all actions between HomeKit and Home Assistant.""" """Class to handle all actions between HomeKit and Home Assistant."""
driver: HomeDriver
def __init__( def __init__(
self, self,
hass, hass: HomeAssistant,
name, name: str,
port, port: int,
ip_address, ip_address: str | None,
entity_filter, entity_filter: EntityFilter,
exclude_accessory_mode, exclude_accessory_mode: bool,
entity_config, entity_config: dict,
homekit_mode, homekit_mode: str,
advertise_ip=None, advertise_ip: str | None,
entry_id=None, entry_id: str,
entry_title=None, entry_title: str,
devices=None, devices: Iterable[str] | None = None,
): ) -> None:
"""Initialize a HomeKit object.""" """Initialize a HomeKit object."""
self.hass = hass self.hass = hass
self._name = name self._name = name
self._port = port self._port = port
self._ip_address = ip_address self._ip_address = ip_address
self._filter: EntityFilter = entity_filter self._filter = entity_filter
self._config = entity_config self._config = entity_config
self._exclude_accessory_mode = exclude_accessory_mode self._exclude_accessory_mode = exclude_accessory_mode
self._advertise_ip = advertise_ip self._advertise_ip = advertise_ip
@ -481,13 +495,12 @@ class HomeKit:
self._entry_title = entry_title self._entry_title = entry_title
self._homekit_mode = homekit_mode self._homekit_mode = homekit_mode
self._devices = devices or [] self._devices = devices or []
self.aid_storage = None self.aid_storage: AccessoryAidStorage | None = None
self.status = STATUS_READY self.status = STATUS_READY
self.bridge = None self.bridge: HomeBridge | None = None
self.driver = None
def setup(self, async_zeroconf_instance, uuid): def setup(self, async_zeroconf_instance: AsyncZeroconf, uuid: UUID) -> None:
"""Set up bridge and accessory driver.""" """Set up bridge and accessory driver."""
persist_file = get_persist_fullpath_for_entry_id(self.hass, self._entry_id) persist_file = get_persist_fullpath_for_entry_id(self.hass, self._entry_id)
@ -510,22 +523,24 @@ class HomeKit:
if os.path.exists(persist_file): if os.path.exists(persist_file):
self.driver.load() self.driver.load()
async def async_reset_accessories(self, entity_ids): async def async_reset_accessories(self, entity_ids: Iterable[str]) -> None:
"""Reset the accessory to load the latest configuration.""" """Reset the accessory to load the latest configuration."""
if not self.bridge: if not self.bridge:
await self.async_reset_accessories_in_accessory_mode(entity_ids) await self.async_reset_accessories_in_accessory_mode(entity_ids)
return return
await self.async_reset_accessories_in_bridge_mode(entity_ids) await self.async_reset_accessories_in_bridge_mode(entity_ids)
async def async_reset_accessories_in_accessory_mode(self, entity_ids): async def async_reset_accessories_in_accessory_mode(
self, entity_ids: Iterable[str]
) -> None:
"""Reset accessories in accessory mode.""" """Reset accessories in accessory mode."""
acc = self.driver.accessory acc = cast(HomeAccessory, self.driver.accessory)
if acc.entity_id not in entity_ids: if acc.entity_id not in entity_ids:
return return
await acc.stop() await acc.stop()
if not (state := self.hass.states.get(acc.entity_id)): if not (state := self.hass.states.get(acc.entity_id)):
_LOGGER.warning( _LOGGER.warning(
"The underlying entity %s disappeared during reset", acc.entity "The underlying entity %s disappeared during reset", acc.entity_id
) )
return return
if new_acc := self._async_create_single_accessory([state]): if new_acc := self._async_create_single_accessory([state]):
@ -533,9 +548,14 @@ class HomeKit:
self.hass.async_add_job(new_acc.run) self.hass.async_add_job(new_acc.run)
await self.async_config_changed() await self.async_config_changed()
async def async_reset_accessories_in_bridge_mode(self, entity_ids): async def async_reset_accessories_in_bridge_mode(
self, entity_ids: Iterable[str]
) -> None:
"""Reset accessories in bridge mode.""" """Reset accessories in bridge mode."""
assert self.aid_storage is not None
assert self.bridge is not None
new = [] new = []
acc: HomeAccessory | None
for entity_id in entity_ids: for entity_id in entity_ids:
aid = self.aid_storage.get_or_allocate_aid_for_entity_id(entity_id) aid = self.aid_storage.get_or_allocate_aid_for_entity_id(entity_id)
if aid not in self.bridge.accessories: if aid not in self.bridge.accessories:
@ -545,12 +565,13 @@ class HomeKit:
self._name, self._name,
entity_id, entity_id,
) )
acc = await self.async_remove_bridge_accessory(aid) if (acc := await self.async_remove_bridge_accessory(aid)) and (
if state := self.hass.states.get(acc.entity_id): state := self.hass.states.get(acc.entity_id)
):
new.append(state) new.append(state)
else: else:
_LOGGER.warning( _LOGGER.warning(
"The underlying entity %s disappeared during reset", acc.entity "The underlying entity %s disappeared during reset", entity_id
) )
if not new: if not new:
@ -560,23 +581,22 @@ class HomeKit:
await self.async_config_changed() await self.async_config_changed()
await asyncio.sleep(_HOMEKIT_CONFIG_UPDATE_TIME) await asyncio.sleep(_HOMEKIT_CONFIG_UPDATE_TIME)
for state in new: for state in new:
acc = self.add_bridge_accessory(state) if acc := self.add_bridge_accessory(state):
if acc:
self.hass.async_add_job(acc.run) self.hass.async_add_job(acc.run)
await self.async_config_changed() await self.async_config_changed()
async def async_config_changed(self): async def async_config_changed(self) -> None:
"""Call config changed which writes out the new config to disk.""" """Call config changed which writes out the new config to disk."""
await self.hass.async_add_executor_job(self.driver.config_changed) await self.hass.async_add_executor_job(self.driver.config_changed)
def add_bridge_accessory(self, state): def add_bridge_accessory(self, state: State) -> HomeAccessory | None:
"""Try adding accessory to bridge if configured beforehand.""" """Try adding accessory to bridge if configured beforehand."""
if self._would_exceed_max_devices(state.entity_id): if self._would_exceed_max_devices(state.entity_id):
return return None
if state_needs_accessory_mode(state): if state_needs_accessory_mode(state):
if self._exclude_accessory_mode: if self._exclude_accessory_mode:
return return None
_LOGGER.warning( _LOGGER.warning(
"The bridge %s has entity %s. For best performance, " "The bridge %s has entity %s. For best performance, "
"and to prevent unexpected unavailability, create and " "and to prevent unexpected unavailability, create and "
@ -586,6 +606,8 @@ class HomeKit:
state.entity_id, state.entity_id,
) )
assert self.aid_storage is not None
assert self.bridge is not None
aid = self.aid_storage.get_or_allocate_aid_for_entity_id(state.entity_id) aid = self.aid_storage.get_or_allocate_aid_for_entity_id(state.entity_id)
conf = self._config.get(state.entity_id, {}).copy() conf = self._config.get(state.entity_id, {}).copy()
# If an accessory cannot be created or added due to an exception # If an accessory cannot be created or added due to an exception
@ -602,9 +624,10 @@ class HomeKit:
) )
return None return None
def _would_exceed_max_devices(self, name): def _would_exceed_max_devices(self, name: str | None) -> bool:
"""Check if adding another devices would reach the limit and log.""" """Check if adding another devices would reach the limit and log."""
# The bridge itself counts as an accessory # The bridge itself counts as an accessory
assert self.bridge is not None
if len(self.bridge.accessories) + 1 >= MAX_DEVICES: if len(self.bridge.accessories) + 1 >= MAX_DEVICES:
_LOGGER.warning( _LOGGER.warning(
"Cannot add %s as this would exceed the %d device limit. Consider using the filter option", "Cannot add %s as this would exceed the %d device limit. Consider using the filter option",
@ -614,16 +637,20 @@ class HomeKit:
return True return True
return False return False
def add_bridge_triggers_accessory(self, device, device_triggers): def add_bridge_triggers_accessory(
self, device: device_registry.DeviceEntry, device_triggers: list[dict[str, Any]]
) -> None:
"""Add device automation triggers to the bridge.""" """Add device automation triggers to the bridge."""
if self._would_exceed_max_devices(device.name): if self._would_exceed_max_devices(device.name):
return return
assert self.aid_storage is not None
assert self.bridge is not None
aid = self.aid_storage.get_or_allocate_aid(device.id, device.id) aid = self.aid_storage.get_or_allocate_aid(device.id, device.id)
# If an accessory cannot be created or added due to an exception # If an accessory cannot be created or added due to an exception
# of any kind (usually in pyhap) it should not prevent # of any kind (usually in pyhap) it should not prevent
# the rest of the accessories from being created # the rest of the accessories from being created
config = {} config: dict[str, Any] = {}
self._fill_config_from_device_registry_entry(device, config) self._fill_config_from_device_registry_entry(device, config)
self.bridge.add_accessory( self.bridge.add_accessory(
DeviceTriggerAccessory( DeviceTriggerAccessory(
@ -638,13 +665,15 @@ class HomeKit:
) )
) )
async def async_remove_bridge_accessory(self, aid): async def async_remove_bridge_accessory(self, aid: int) -> HomeAccessory | None:
"""Try adding accessory to bridge if configured beforehand.""" """Try adding accessory to bridge if configured beforehand."""
assert self.bridge is not None
if acc := self.bridge.accessories.pop(aid, None): if acc := self.bridge.accessories.pop(aid, None):
await acc.stop() await acc.stop()
return acc return cast(HomeAccessory, acc)
return None
async def async_configure_accessories(self): async def async_configure_accessories(self) -> list[State]:
"""Configure accessories for the included states.""" """Configure accessories for the included states."""
dev_reg = device_registry.async_get(self.hass) dev_reg = device_registry.async_get(self.hass)
ent_reg = entity_registry.async_get(self.hass) ent_reg = entity_registry.async_get(self.hass)
@ -680,7 +709,7 @@ class HomeKit:
return entity_states return entity_states
async def async_start(self, *args): async def async_start(self, *args: Any) -> None:
"""Load storage and start.""" """Load storage and start."""
if self.status != STATUS_READY: if self.status != STATUS_READY:
return return
@ -704,7 +733,7 @@ class HomeKit:
self._async_show_setup_message() self._async_show_setup_message()
@callback @callback
def _async_show_setup_message(self): def _async_show_setup_message(self) -> None:
"""Show the pairing setup message.""" """Show the pairing setup message."""
async_show_setup_message( async_show_setup_message(
self.hass, self.hass,
@ -715,7 +744,7 @@ class HomeKit:
) )
@callback @callback
def async_unpair(self): def async_unpair(self) -> None:
"""Remove all pairings for an accessory so it can be repaired.""" """Remove all pairings for an accessory so it can be repaired."""
state = self.driver.state state = self.driver.state
for client_uuid in list(state.paired_clients): for client_uuid in list(state.paired_clients):
@ -730,8 +759,9 @@ class HomeKit:
self._async_show_setup_message() self._async_show_setup_message()
@callback @callback
def _async_register_bridge(self): def _async_register_bridge(self) -> None:
"""Register the bridge as a device so homekit_controller and exclude it from discovery.""" """Register the bridge as a device so homekit_controller and exclude it from discovery."""
assert self.driver is not None
dev_reg = device_registry.async_get(self.hass) dev_reg = device_registry.async_get(self.hass)
formatted_mac = device_registry.format_mac(self.driver.state.mac) formatted_mac = device_registry.format_mac(self.driver.state.mac)
# Connections and identifiers are both used here. # Connections and identifiers are both used here.
@ -753,7 +783,9 @@ class HomeKit:
hk_mode_name = "Accessory" if is_accessory_mode else "Bridge" hk_mode_name = "Accessory" if is_accessory_mode else "Bridge"
dev_reg.async_get_or_create( dev_reg.async_get_or_create(
config_entry_id=self._entry_id, config_entry_id=self._entry_id,
identifiers={identifier}, identifiers={
identifier # type: ignore[arg-type]
}, # this needs to be migrated as a 2 item tuple at some point
connections={connection}, connections={connection},
manufacturer=MANUFACTURER, manufacturer=MANUFACTURER,
name=accessory_friendly_name(self._entry_title, self.driver.accessory), name=accessory_friendly_name(self._entry_title, self.driver.accessory),
@ -762,12 +794,17 @@ class HomeKit:
) )
@callback @callback
def _async_purge_old_bridges(self, dev_reg, identifier, connection): def _async_purge_old_bridges(
self,
dev_reg: device_registry.DeviceRegistry,
identifier: tuple[str, str, str],
connection: tuple[str, str],
) -> None:
"""Purge bridges that exist from failed pairing or manual resets.""" """Purge bridges that exist from failed pairing or manual resets."""
devices_to_purge = [] devices_to_purge = []
for entry in dev_reg.devices.values(): for entry in dev_reg.devices.values():
if self._entry_id in entry.config_entries and ( if self._entry_id in entry.config_entries and (
identifier not in entry.identifiers identifier not in entry.identifiers # type: ignore[comparison-overlap]
or connection not in entry.connections or connection not in entry.connections
): ):
devices_to_purge.append(entry.id) devices_to_purge.append(entry.id)
@ -776,7 +813,9 @@ class HomeKit:
dev_reg.async_remove_device(device_id) dev_reg.async_remove_device(device_id)
@callback @callback
def _async_create_single_accessory(self, entity_states): def _async_create_single_accessory(
self, entity_states: list[State]
) -> HomeAccessory | None:
"""Create a single HomeKit accessory (accessory mode).""" """Create a single HomeKit accessory (accessory mode)."""
if not entity_states: if not entity_states:
_LOGGER.error( _LOGGER.error(
@ -796,7 +835,9 @@ class HomeKit:
) )
return acc return acc
async def _async_create_bridge_accessory(self, entity_states): async def _async_create_bridge_accessory(
self, entity_states: Iterable[State]
) -> HomeAccessory:
"""Create a HomeKit bridge with accessories. (bridge mode).""" """Create a HomeKit bridge with accessories. (bridge mode)."""
self.bridge = HomeBridge(self.hass, self.driver, self._name) self.bridge = HomeBridge(self.hass, self.driver, self._name)
for state in entity_states: for state in entity_states:
@ -820,12 +861,11 @@ class HomeKit:
valid_device_ids, valid_device_ids,
) )
).items(): ).items():
self.add_bridge_triggers_accessory( if device := dev_reg.async_get(device_id):
dev_reg.async_get(device_id), device_triggers self.add_bridge_triggers_accessory(device, device_triggers)
)
return self.bridge return self.bridge
async def _async_create_accessories(self): async def _async_create_accessories(self) -> bool:
"""Create the accessories.""" """Create the accessories."""
entity_states = await self.async_configure_accessories() entity_states = await self.async_configure_accessories()
if self._homekit_mode == HOMEKIT_MODE_ACCESSORY: if self._homekit_mode == HOMEKIT_MODE_ACCESSORY:
@ -839,7 +879,7 @@ class HomeKit:
self.driver.accessory = acc self.driver.accessory = acc
return True return True
async def async_stop(self, *args): async def async_stop(self, *args: Any) -> None:
"""Stop the accessory driver.""" """Stop the accessory driver."""
if self.status != STATUS_RUNNING: if self.status != STATUS_RUNNING:
return return
@ -848,7 +888,12 @@ class HomeKit:
await self.driver.async_stop() await self.driver.async_stop()
@callback @callback
def _async_configure_linked_sensors(self, ent_reg_ent, device_lookup, state): def _async_configure_linked_sensors(
self,
ent_reg_ent: entity_registry.RegistryEntry,
device_lookup: dict[str, dict[tuple[str, str | None], str]],
state: State,
) -> None:
if ( if (
ent_reg_ent is None ent_reg_ent is None
or ent_reg_ent.device_id is None or ent_reg_ent.device_id is None
@ -905,7 +950,12 @@ class HomeKit:
current_humidity_sensor_entity_id, current_humidity_sensor_entity_id,
) )
async def _async_set_device_info_attributes(self, ent_reg_ent, dev_reg, entity_id): async def _async_set_device_info_attributes(
self,
ent_reg_ent: entity_registry.RegistryEntry,
dev_reg: device_registry.DeviceRegistry,
entity_id: str,
) -> None:
"""Set attributes that will be used for homekit device info.""" """Set attributes that will be used for homekit device info."""
ent_cfg = self._config.setdefault(entity_id, {}) ent_cfg = self._config.setdefault(entity_id, {})
if ent_reg_ent.device_id: if ent_reg_ent.device_id:
@ -920,7 +970,9 @@ class HomeKit:
except IntegrationNotFound: except IntegrationNotFound:
ent_cfg[ATTR_INTEGRATION] = ent_reg_ent.platform ent_cfg[ATTR_INTEGRATION] = ent_reg_ent.platform
def _fill_config_from_device_registry_entry(self, device_entry, config): def _fill_config_from_device_registry_entry(
self, device_entry: device_registry.DeviceEntry, config: dict[str, Any]
) -> None:
"""Populate a config dict from the registry.""" """Populate a config dict from the registry."""
if device_entry.manufacturer: if device_entry.manufacturer:
config[ATTR_MANUFACTURER] = device_entry.manufacturer config[ATTR_MANUFACTURER] = device_entry.manufacturer
@ -943,7 +995,7 @@ class HomeKitPairingQRView(HomeAssistantView):
name = "api:homekit:pairingqr" name = "api:homekit:pairingqr"
requires_auth = False requires_auth = False
async def get(self, request): async def get(self, request: web.Request) -> web.Response:
"""Retrieve the pairing QRCode image.""" """Retrieve the pairing QRCode image."""
# pylint: disable=no-self-use # pylint: disable=no-self-use
if not request.query_string: if not request.query_string:

View file

@ -2,6 +2,8 @@
from __future__ import annotations from __future__ import annotations
import logging import logging
from typing import Any, cast
from uuid import UUID
from pyhap.accessory import Accessory, Bridge from pyhap.accessory import Accessory, Bridge
from pyhap.accessory_driver import AccessoryDriver from pyhap.accessory_driver import AccessoryDriver
@ -34,7 +36,15 @@ from homeassistant.const import (
TEMP_FAHRENHEIT, TEMP_FAHRENHEIT,
__version__, __version__,
) )
from homeassistant.core import Context, callback as ha_callback, split_entity_id from homeassistant.core import (
CALLBACK_TYPE,
Context,
Event,
HomeAssistant,
State,
callback as ha_callback,
split_entity_id,
)
from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.event import async_track_state_change_event
from homeassistant.util.decorator import Registry from homeassistant.util.decorator import Registry
@ -95,7 +105,9 @@ SWITCH_TYPES = {
TYPES: Registry[str, type[HomeAccessory]] = Registry() TYPES: Registry[str, type[HomeAccessory]] = Registry()
def get_accessory(hass, driver, state, aid, config): # noqa: C901 def get_accessory( # noqa: C901
hass: HomeAssistant, driver: HomeDriver, state: State, aid: int | None, config: dict
) -> HomeAccessory | None:
"""Take state and return an accessory object if supported.""" """Take state and return an accessory object if supported."""
if not aid: if not aid:
_LOGGER.warning( _LOGGER.warning(
@ -232,22 +244,22 @@ def get_accessory(hass, driver, state, aid, config): # noqa: C901
return TYPES[a_type](hass, driver, name, state.entity_id, aid, config) return TYPES[a_type](hass, driver, name, state.entity_id, aid, config)
class HomeAccessory(Accessory): class HomeAccessory(Accessory): # type: ignore[misc]
"""Adapter class for Accessory.""" """Adapter class for Accessory."""
def __init__( def __init__(
self, self,
hass, hass: HomeAssistant,
driver, driver: HomeDriver,
name, name: str,
entity_id, entity_id: str,
aid, aid: int,
config, config: dict,
*args, *args: Any,
category=CATEGORY_OTHER, category: str = CATEGORY_OTHER,
device_id=None, device_id: str | None = None,
**kwargs, **kwargs: Any,
): ) -> None:
"""Initialize a Accessory object.""" """Initialize a Accessory object."""
super().__init__( super().__init__(
driver=driver, driver=driver,
@ -258,7 +270,7 @@ class HomeAccessory(Accessory):
) )
self.config = config or {} self.config = config or {}
if device_id: if device_id:
self.device_id = device_id self.device_id: str | None = device_id
serial_number = device_id serial_number = device_id
domain = None domain = None
else: else:
@ -285,6 +297,7 @@ class HomeAccessory(Accessory):
sw_version = format_version(self.config[ATTR_SW_VERSION]) sw_version = format_version(self.config[ATTR_SW_VERSION])
if sw_version is None: if sw_version is None:
sw_version = format_version(__version__) sw_version = format_version(__version__)
assert sw_version is not None
hw_version = None hw_version = None
if self.config.get(ATTR_HW_VERSION) is not None: if self.config.get(ATTR_HW_VERSION) is not None:
hw_version = format_version(self.config[ATTR_HW_VERSION]) hw_version = format_version(self.config[ATTR_HW_VERSION])
@ -308,7 +321,7 @@ class HomeAccessory(Accessory):
self.category = category self.category = category
self.entity_id = entity_id self.entity_id = entity_id
self.hass = hass self.hass = hass
self._subscriptions = [] self._subscriptions: list[CALLBACK_TYPE] = []
if device_id: if device_id:
return return
@ -325,7 +338,9 @@ class HomeAccessory(Accessory):
) )
"""Add battery service if available""" """Add battery service if available"""
entity_attributes = self.hass.states.get(self.entity_id).attributes state = self.hass.states.get(self.entity_id)
assert state is not None
entity_attributes = state.attributes
battery_found = entity_attributes.get(ATTR_BATTERY_LEVEL) battery_found = entity_attributes.get(ATTR_BATTERY_LEVEL)
if self.linked_battery_sensor: if self.linked_battery_sensor:
@ -367,15 +382,15 @@ class HomeAccessory(Accessory):
) )
@property @property
def available(self): def available(self) -> bool:
"""Return if accessory is available.""" """Return if accessory is available."""
state = self.hass.states.get(self.entity_id) state = self.hass.states.get(self.entity_id)
return state is not None and state.state != STATE_UNAVAILABLE return state is not None and state.state != STATE_UNAVAILABLE
async def run(self): async def run(self) -> None:
"""Handle accessory driver started event.""" """Handle accessory driver started event."""
state = self.hass.states.get(self.entity_id) if state := self.hass.states.get(self.entity_id):
self.async_update_state_callback(state) self.async_update_state_callback(state)
self._subscriptions.append( self._subscriptions.append(
async_track_state_change_event( async_track_state_change_event(
self.hass, [self.entity_id], self.async_update_event_state_callback self.hass, [self.entity_id], self.async_update_event_state_callback
@ -384,10 +399,11 @@ class HomeAccessory(Accessory):
battery_charging_state = None battery_charging_state = None
battery_state = None battery_state = None
if self.linked_battery_sensor: if self.linked_battery_sensor and (
linked_battery_sensor_state = self.hass.states.get( linked_battery_sensor_state := self.hass.states.get(
self.linked_battery_sensor self.linked_battery_sensor
) )
):
battery_state = linked_battery_sensor_state.state battery_state = linked_battery_sensor_state.state
battery_charging_state = linked_battery_sensor_state.attributes.get( battery_charging_state = linked_battery_sensor_state.attributes.get(
ATTR_BATTERY_CHARGING ATTR_BATTERY_CHARGING
@ -418,12 +434,12 @@ class HomeAccessory(Accessory):
self.async_update_battery(battery_state, battery_charging_state) self.async_update_battery(battery_state, battery_charging_state)
@ha_callback @ha_callback
def async_update_event_state_callback(self, event): def async_update_event_state_callback(self, event: Event) -> None:
"""Handle state change event listener callback.""" """Handle state change event listener callback."""
self.async_update_state_callback(event.data.get("new_state")) self.async_update_state_callback(event.data.get("new_state"))
@ha_callback @ha_callback
def async_update_state_callback(self, new_state): def async_update_state_callback(self, new_state: State | None) -> None:
"""Handle state change listener callback.""" """Handle state change listener callback."""
_LOGGER.debug("New_state: %s", new_state) _LOGGER.debug("New_state: %s", new_state)
if new_state is None: if new_state is None:
@ -445,7 +461,7 @@ class HomeAccessory(Accessory):
self.async_update_state(new_state) self.async_update_state(new_state)
@ha_callback @ha_callback
def async_update_linked_battery_callback(self, event): def async_update_linked_battery_callback(self, event: Event) -> None:
"""Handle linked battery sensor state change listener callback.""" """Handle linked battery sensor state change listener callback."""
if (new_state := event.data.get("new_state")) is None: if (new_state := event.data.get("new_state")) is None:
return return
@ -456,19 +472,19 @@ class HomeAccessory(Accessory):
self.async_update_battery(new_state.state, battery_charging_state) self.async_update_battery(new_state.state, battery_charging_state)
@ha_callback @ha_callback
def async_update_linked_battery_charging_callback(self, event): def async_update_linked_battery_charging_callback(self, event: Event) -> None:
"""Handle linked battery charging sensor state change listener callback.""" """Handle linked battery charging sensor state change listener callback."""
if (new_state := event.data.get("new_state")) is None: if (new_state := event.data.get("new_state")) is None:
return return
self.async_update_battery(None, new_state.state == STATE_ON) self.async_update_battery(None, new_state.state == STATE_ON)
@ha_callback @ha_callback
def async_update_battery(self, battery_level, battery_charging): def async_update_battery(self, battery_level: Any, battery_charging: Any) -> None:
"""Update battery service if available. """Update battery service if available.
Only call this function if self._support_battery_level is True. Only call this function if self._support_battery_level is True.
""" """
if not self._char_battery: if not self._char_battery or not self._char_low_battery:
# Battery appeared after homekit was started # Battery appeared after homekit was started
return return
@ -495,7 +511,7 @@ class HomeAccessory(Accessory):
) )
@ha_callback @ha_callback
def async_update_state(self, new_state): def async_update_state(self, new_state: State) -> None:
"""Handle state change to update HomeKit value. """Handle state change to update HomeKit value.
Overridden by accessory types. Overridden by accessory types.
@ -503,7 +519,13 @@ class HomeAccessory(Accessory):
raise NotImplementedError() raise NotImplementedError()
@ha_callback @ha_callback
def async_call_service(self, domain, service, service_data, value=None): def async_call_service(
self,
domain: str,
service: str,
service_data: dict[str, Any] | None,
value: Any | None = None,
) -> None:
"""Fire event and call service for changes from HomeKit.""" """Fire event and call service for changes from HomeKit."""
event_data = { event_data = {
ATTR_ENTITY_ID: self.entity_id, ATTR_ENTITY_ID: self.entity_id,
@ -521,7 +543,7 @@ class HomeAccessory(Accessory):
) )
@ha_callback @ha_callback
def async_reset(self): def async_reset(self) -> None:
"""Reset and recreate an accessory.""" """Reset and recreate an accessory."""
self.hass.async_create_task( self.hass.async_create_task(
self.hass.services.async_call( self.hass.services.async_call(
@ -531,16 +553,16 @@ class HomeAccessory(Accessory):
) )
) )
async def stop(self): async def stop(self) -> None:
"""Cancel any subscriptions when the bridge is stopped.""" """Cancel any subscriptions when the bridge is stopped."""
while self._subscriptions: while self._subscriptions:
self._subscriptions.pop(0)() self._subscriptions.pop(0)()
class HomeBridge(Bridge): class HomeBridge(Bridge): # type: ignore[misc]
"""Adapter class for Bridge.""" """Adapter class for Bridge."""
def __init__(self, hass, driver, name): def __init__(self, hass: HomeAssistant, driver: HomeDriver, name: str) -> None:
"""Initialize a Bridge object.""" """Initialize a Bridge object."""
super().__init__(driver, name) super().__init__(driver, name)
self.set_info_service( self.set_info_service(
@ -551,10 +573,10 @@ class HomeBridge(Bridge):
) )
self.hass = hass self.hass = hass
def setup_message(self): def setup_message(self) -> None:
"""Prevent print of pyhap setup message to terminal.""" """Prevent print of pyhap setup message to terminal."""
async def async_get_snapshot(self, info): async def async_get_snapshot(self, info: dict) -> bytes:
"""Get snapshot from accessory if supported.""" """Get snapshot from accessory if supported."""
if (acc := self.accessories.get(info["aid"])) is None: if (acc := self.accessories.get(info["aid"])) is None:
raise ValueError("Requested snapshot for missing accessory") raise ValueError("Requested snapshot for missing accessory")
@ -563,13 +585,20 @@ class HomeBridge(Bridge):
"Got a request for snapshot, but the Accessory " "Got a request for snapshot, but the Accessory "
'does not define a "async_get_snapshot" method' 'does not define a "async_get_snapshot" method'
) )
return await acc.async_get_snapshot(info) return cast(bytes, await acc.async_get_snapshot(info))
class HomeDriver(AccessoryDriver): class HomeDriver(AccessoryDriver): # type: ignore[misc]
"""Adapter class for AccessoryDriver.""" """Adapter class for AccessoryDriver."""
def __init__(self, hass, entry_id, bridge_name, entry_title, **kwargs): def __init__(
self,
hass: HomeAssistant,
entry_id: str,
bridge_name: str,
entry_title: str,
**kwargs: Any,
) -> None:
"""Initialize a AccessoryDriver object.""" """Initialize a AccessoryDriver object."""
super().__init__(**kwargs) super().__init__(**kwargs)
self.hass = hass self.hass = hass
@ -577,16 +606,18 @@ class HomeDriver(AccessoryDriver):
self._bridge_name = bridge_name self._bridge_name = bridge_name
self._entry_title = entry_title self._entry_title = entry_title
@pyhap_callback @pyhap_callback # type: ignore[misc]
def pair(self, client_uuid, client_public, client_permissions): def pair(
self, client_uuid: UUID, client_public: str, client_permissions: int
) -> bool:
"""Override super function to dismiss setup message if paired.""" """Override super function to dismiss setup message if paired."""
success = super().pair(client_uuid, client_public, client_permissions) success = super().pair(client_uuid, client_public, client_permissions)
if success: if success:
async_dismiss_setup_message(self.hass, self._entry_id) async_dismiss_setup_message(self.hass, self._entry_id)
return success return cast(bool, success)
@pyhap_callback @pyhap_callback # type: ignore[misc]
def unpair(self, client_uuid): def unpair(self, client_uuid: UUID) -> None:
"""Override super function to show setup message if unpaired.""" """Override super function to show setup message if unpaired."""
super().unpair(client_uuid) super().unpair(client_uuid)

View file

@ -9,13 +9,15 @@ can't change the hash without causing breakages for HA users.
This module generates and stores them in a HA storage. This module generates and stores them in a HA storage.
""" """
from __future__ import annotations
from collections.abc import Generator
import random import random
from fnvhash import fnv1a_32 from fnvhash import fnv1a_32
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_registry import RegistryEntry from homeassistant.helpers.entity_registry import EntityRegistry, RegistryEntry
from homeassistant.helpers.storage import Store from homeassistant.helpers.storage import Store
from .util import get_aid_storage_filename_for_entry_id from .util import get_aid_storage_filename_for_entry_id
@ -32,12 +34,12 @@ AID_MIN = 2
AID_MAX = 18446744073709551615 AID_MAX = 18446744073709551615
def get_system_unique_id(entity: RegistryEntry): def get_system_unique_id(entity: RegistryEntry) -> str:
"""Determine the system wide unique_id for an entity.""" """Determine the system wide unique_id for an entity."""
return f"{entity.platform}.{entity.domain}.{entity.unique_id}" return f"{entity.platform}.{entity.domain}.{entity.unique_id}"
def _generate_aids(unique_id: str, entity_id: str) -> int: def _generate_aids(unique_id: str | None, entity_id: str) -> Generator[int, None, None]:
"""Generate accessory aid.""" """Generate accessory aid."""
if unique_id: if unique_id:
@ -65,39 +67,41 @@ class AccessoryAidStorage:
persist over reboots. persist over reboots.
""" """
def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: def __init__(self, hass: HomeAssistant, entry_id: str) -> None:
"""Create a new entity map store.""" """Create a new entity map store."""
self.hass = hass self.hass = hass
self.allocations = {} self.allocations: dict[str, int] = {}
self.allocated_aids = set() self.allocated_aids: set[int] = set()
self._entry = entry self._entry_id = entry_id
self.store = None self.store: Store | None = None
self._entity_registry = None self._entity_registry: EntityRegistry | None = None
async def async_initialize(self): async def async_initialize(self) -> None:
"""Load the latest AID data.""" """Load the latest AID data."""
self._entity_registry = ( self._entity_registry = (
await self.hass.helpers.entity_registry.async_get_registry() await self.hass.helpers.entity_registry.async_get_registry()
) )
aidstore = get_aid_storage_filename_for_entry_id(self._entry) aidstore = get_aid_storage_filename_for_entry_id(self._entry_id)
self.store = Store(self.hass, AID_MANAGER_STORAGE_VERSION, aidstore) self.store = Store(self.hass, AID_MANAGER_STORAGE_VERSION, aidstore)
if not (raw_storage := await self.store.async_load()): if not (raw_storage := await self.store.async_load()):
# There is no data about aid allocations yet # There is no data about aid allocations yet
return return
assert isinstance(raw_storage, dict)
self.allocations = raw_storage.get(ALLOCATIONS_KEY, {}) self.allocations = raw_storage.get(ALLOCATIONS_KEY, {})
self.allocated_aids = set(self.allocations.values()) self.allocated_aids = set(self.allocations.values())
def get_or_allocate_aid_for_entity_id(self, entity_id: str): def get_or_allocate_aid_for_entity_id(self, entity_id: str) -> int:
"""Generate a stable aid for an entity id.""" """Generate a stable aid for an entity id."""
assert self._entity_registry is not None
if not (entity := self._entity_registry.async_get(entity_id)): if not (entity := self._entity_registry.async_get(entity_id)):
return self.get_or_allocate_aid(None, entity_id) return self.get_or_allocate_aid(None, entity_id)
sys_unique_id = get_system_unique_id(entity) sys_unique_id = get_system_unique_id(entity)
return self.get_or_allocate_aid(sys_unique_id, entity_id) return self.get_or_allocate_aid(sys_unique_id, entity_id)
def get_or_allocate_aid(self, unique_id: str, entity_id: str): def get_or_allocate_aid(self, unique_id: str | None, entity_id: str) -> int:
"""Allocate (and return) a new aid for an accessory.""" """Allocate (and return) a new aid for an accessory."""
if unique_id and unique_id in self.allocations: if unique_id and unique_id in self.allocations:
return self.allocations[unique_id] return self.allocations[unique_id]
@ -119,7 +123,7 @@ class AccessoryAidStorage:
f"Unable to generate unique aid allocation for {entity_id} [{unique_id}]" f"Unable to generate unique aid allocation for {entity_id} [{unique_id}]"
) )
def delete_aid(self, storage_key: str): def delete_aid(self, storage_key: str) -> None:
"""Delete an aid allocation.""" """Delete an aid allocation."""
if storage_key not in self.allocations: if storage_key not in self.allocations:
return return
@ -129,15 +133,17 @@ class AccessoryAidStorage:
self.async_schedule_save() self.async_schedule_save()
@callback @callback
def async_schedule_save(self): def async_schedule_save(self) -> None:
"""Schedule saving the entity map cache.""" """Schedule saving the entity map cache."""
assert self.store is not None
self.store.async_delay_save(self._data_to_save, AID_MANAGER_SAVE_DELAY) self.store.async_delay_save(self._data_to_save, AID_MANAGER_SAVE_DELAY)
async def async_save(self): async def async_save(self) -> None:
"""Save the entity map cache.""" """Save the entity map cache."""
assert self.store is not None
return await self.store.async_save(self._data_to_save()) return await self.store.async_save(self._data_to_save())
@callback @callback
def _data_to_save(self): def _data_to_save(self) -> dict:
"""Return data of entity map to store in a file.""" """Return data of entity map to store in a file."""
return {ALLOCATIONS_KEY: self.allocations} return {ALLOCATIONS_KEY: self.allocations}

View file

@ -2,11 +2,12 @@
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
from collections.abc import Iterable
from copy import deepcopy from copy import deepcopy
import random import random
import re import re
import string import string
from typing import Any, Final from typing import Any
import voluptuous as vol import voluptuous as vol
@ -27,6 +28,7 @@ from homeassistant.const import (
CONF_PORT, CONF_PORT,
) )
from homeassistant.core import HomeAssistant, callback, split_entity_id from homeassistant.core import HomeAssistant, callback, split_entity_id
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers import device_registry, entity_registry from homeassistant.helpers import device_registry, entity_registry
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entityfilter import ( from homeassistant.helpers.entityfilter import (
@ -119,7 +121,7 @@ DEFAULT_DOMAINS = [
"water_heater", "water_heater",
] ]
_EMPTY_ENTITY_FILTER: Final = { _EMPTY_ENTITY_FILTER: dict[str, list[str]] = {
CONF_INCLUDE_DOMAINS: [], CONF_INCLUDE_DOMAINS: [],
CONF_EXCLUDE_DOMAINS: [], CONF_EXCLUDE_DOMAINS: [],
CONF_INCLUDE_ENTITIES: [], CONF_INCLUDE_ENTITIES: [],
@ -151,9 +153,9 @@ def _async_build_entites_filter(
return entity_filter return entity_filter
def _async_cameras_from_entities(entities: list[str]) -> set[str]: def _async_cameras_from_entities(entities: list[str]) -> dict[str, str]:
return { return {
entity_id entity_id: entity_id
for entity_id in entities for entity_id in entities
if entity_id.startswith(CAMERA_ENTITY_PREFIX) if entity_id.startswith(CAMERA_ENTITY_PREFIX)
} }
@ -181,9 +183,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
def __init__(self) -> None: def __init__(self) -> None:
"""Initialize config flow.""" """Initialize config flow."""
self.hk_data = {} self.hk_data: dict[str, Any] = {}
async def async_step_user(self, user_input=None): async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Choose specific domains in bridge mode.""" """Choose specific domains in bridge mode."""
if user_input is not None: if user_input is not None:
entity_filter = deepcopy(_EMPTY_ENTITY_FILTER) entity_filter = deepcopy(_EMPTY_ENTITY_FILTER)
@ -205,7 +209,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
), ),
) )
async def async_step_pairing(self, user_input=None): async def async_step_pairing(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Pairing instructions.""" """Pairing instructions."""
if user_input is not None: if user_input is not None:
port = async_find_next_available_port(self.hass, DEFAULT_CONFIG_FLOW_PORT) port = async_find_next_available_port(self.hass, DEFAULT_CONFIG_FLOW_PORT)
@ -227,7 +233,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
description_placeholders={CONF_NAME: self.hk_data[CONF_NAME]}, description_placeholders={CONF_NAME: self.hk_data[CONF_NAME]},
) )
async def _async_add_entries_for_accessory_mode_entities(self, last_assigned_port): async def _async_add_entries_for_accessory_mode_entities(
self, last_assigned_port: int
) -> None:
"""Generate new flows for entities that need their own instances.""" """Generate new flows for entities that need their own instances."""
accessory_mode_entity_ids = _async_get_entity_ids_for_accessory_mode( accessory_mode_entity_ids = _async_get_entity_ids_for_accessory_mode(
self.hass, self.hk_data[CONF_FILTER][CONF_INCLUDE_DOMAINS] self.hass, self.hk_data[CONF_FILTER][CONF_INCLUDE_DOMAINS]
@ -249,12 +257,13 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
) )
) )
async def async_step_accessory(self, accessory_input): async def async_step_accessory(self, accessory_input: dict) -> FlowResult:
"""Handle creation a single accessory in accessory mode.""" """Handle creation a single accessory in accessory mode."""
entity_id = accessory_input[CONF_ENTITY_ID] entity_id = accessory_input[CONF_ENTITY_ID]
port = accessory_input[CONF_PORT] port = accessory_input[CONF_PORT]
state = self.hass.states.get(entity_id) state = self.hass.states.get(entity_id)
assert state is not None
name = state.attributes.get(ATTR_FRIENDLY_NAME) or state.entity_id name = state.attributes.get(ATTR_FRIENDLY_NAME) or state.entity_id
entity_filter = _EMPTY_ENTITY_FILTER.copy() entity_filter = _EMPTY_ENTITY_FILTER.copy()
entity_filter[CONF_INCLUDE_ENTITIES] = [entity_id] entity_filter[CONF_INCLUDE_ENTITIES] = [entity_id]
@ -274,7 +283,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
title=f"{name}:{entry_data[CONF_PORT]}", data=entry_data title=f"{name}:{entry_data[CONF_PORT]}", data=entry_data
) )
async def async_step_import(self, user_input=None): async def async_step_import(self, user_input: dict) -> FlowResult:
"""Handle import from yaml.""" """Handle import from yaml."""
if not self._async_is_unique_name_port(user_input): if not self._async_is_unique_name_port(user_input):
return self.async_abort(reason="port_name_in_use") return self.async_abort(reason="port_name_in_use")
@ -283,7 +292,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
) )
@callback @callback
def _async_current_names(self): def _async_current_names(self) -> set[str]:
"""Return a set of bridge names.""" """Return a set of bridge names."""
return { return {
entry.data[CONF_NAME] entry.data[CONF_NAME]
@ -292,7 +301,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
} }
@callback @callback
def _async_available_name(self, requested_name): def _async_available_name(self, requested_name: str) -> str:
"""Return an available for the bridge.""" """Return an available for the bridge."""
current_names = self._async_current_names() current_names = self._async_current_names()
valid_mdns_name = re.sub("[^A-Za-z0-9 ]+", " ", requested_name) valid_mdns_name = re.sub("[^A-Za-z0-9 ]+", " ", requested_name)
@ -301,7 +310,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
return valid_mdns_name return valid_mdns_name
acceptable_mdns_chars = string.ascii_uppercase + string.digits acceptable_mdns_chars = string.ascii_uppercase + string.digits
suggested_name = None suggested_name: str | None = None
while not suggested_name or suggested_name in current_names: while not suggested_name or suggested_name in current_names:
trailer = "".join(random.choices(acceptable_mdns_chars, k=2)) trailer = "".join(random.choices(acceptable_mdns_chars, k=2))
suggested_name = f"{valid_mdns_name} {trailer}" suggested_name = f"{valid_mdns_name} {trailer}"
@ -309,7 +318,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
return suggested_name return suggested_name
@callback @callback
def _async_is_unique_name_port(self, user_input): def _async_is_unique_name_port(self, user_input: dict[str, str]) -> bool:
"""Determine is a name or port is already used.""" """Determine is a name or port is already used."""
name = user_input[CONF_NAME] name = user_input[CONF_NAME]
port = user_input[CONF_PORT] port = user_input[CONF_PORT]
@ -320,7 +329,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
@staticmethod @staticmethod
@callback @callback
def async_get_options_flow(config_entry): def async_get_options_flow(
config_entry: config_entries.ConfigEntry,
) -> OptionsFlowHandler:
"""Get the options flow for this handler.""" """Get the options flow for this handler."""
return OptionsFlowHandler(config_entry) return OptionsFlowHandler(config_entry)
@ -331,10 +342,12 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
def __init__(self, config_entry: config_entries.ConfigEntry) -> None: def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
"""Initialize options flow.""" """Initialize options flow."""
self.config_entry = config_entry self.config_entry = config_entry
self.hk_options = {} self.hk_options: dict[str, Any] = {}
self.included_cameras = set() self.included_cameras: dict[str, str] = {}
async def async_step_yaml(self, user_input=None): async def async_step_yaml(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""No options for yaml managed entries.""" """No options for yaml managed entries."""
if user_input is not None: if user_input is not None:
# Apparently not possible to abort an options flow # Apparently not possible to abort an options flow
@ -343,7 +356,9 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
return self.async_show_form(step_id="yaml") return self.async_show_form(step_id="yaml")
async def async_step_advanced(self, user_input=None): async def async_step_advanced(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Choose advanced options.""" """Choose advanced options."""
if ( if (
not self.show_advanced_options not self.show_advanced_options
@ -352,17 +367,16 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
): ):
if user_input: if user_input:
self.hk_options.update(user_input) self.hk_options.update(user_input)
if (
self.show_advanced_options
and self.hk_options[CONF_HOMEKIT_MODE] == HOMEKIT_MODE_BRIDGE
):
self.hk_options[CONF_DEVICES] = user_input[CONF_DEVICES]
for key in (CONF_DOMAINS, CONF_ENTITIES): for key in (CONF_DOMAINS, CONF_ENTITIES):
if key in self.hk_options: if key in self.hk_options:
del self.hk_options[key] del self.hk_options[key]
if (
self.show_advanced_options
and self.hk_options[CONF_HOMEKIT_MODE] == HOMEKIT_MODE_BRIDGE
):
self.hk_options[CONF_DEVICES] = user_input[CONF_DEVICES]
if CONF_INCLUDE_EXCLUDE_MODE in self.hk_options: if CONF_INCLUDE_EXCLUDE_MODE in self.hk_options:
del self.hk_options[CONF_INCLUDE_EXCLUDE_MODE] del self.hk_options[CONF_INCLUDE_EXCLUDE_MODE]
@ -386,7 +400,9 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
), ),
) )
async def async_step_cameras(self, user_input=None): async def async_step_cameras(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Choose camera config.""" """Choose camera config."""
if user_input is not None: if user_input is not None:
entity_config = self.hk_options[CONF_ENTITY_CONFIG] entity_config = self.hk_options[CONF_ENTITY_CONFIG]
@ -433,7 +449,9 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
) )
return self.async_show_form(step_id="cameras", data_schema=data_schema) return self.async_show_form(step_id="cameras", data_schema=data_schema)
async def async_step_accessory(self, user_input=None): async def async_step_accessory(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Choose entity for the accessory.""" """Choose entity for the accessory."""
domains = self.hk_options[CONF_DOMAINS] domains = self.hk_options[CONF_DOMAINS]
@ -470,7 +488,9 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
), ),
) )
async def async_step_include(self, user_input=None): async def async_step_include(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Choose entities to include from the domain on the bridge.""" """Choose entities to include from the domain on the bridge."""
domains = self.hk_options[CONF_DOMAINS] domains = self.hk_options[CONF_DOMAINS]
if user_input is not None: if user_input is not None:
@ -507,7 +527,9 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
), ),
) )
async def async_step_exclude(self, user_input=None): async def async_step_exclude(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Choose entities to exclude from the domain on the bridge.""" """Choose entities to exclude from the domain on the bridge."""
domains = self.hk_options[CONF_DOMAINS] domains = self.hk_options[CONF_DOMAINS]
@ -516,13 +538,13 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
entities = cv.ensure_list(user_input[CONF_ENTITIES]) entities = cv.ensure_list(user_input[CONF_ENTITIES])
entity_filter[CONF_INCLUDE_DOMAINS] = domains entity_filter[CONF_INCLUDE_DOMAINS] = domains
entity_filter[CONF_EXCLUDE_ENTITIES] = entities entity_filter[CONF_EXCLUDE_ENTITIES] = entities
self.included_cameras = set() self.included_cameras = {}
if CAMERA_DOMAIN in entity_filter[CONF_INCLUDE_DOMAINS]: if CAMERA_DOMAIN in entity_filter[CONF_INCLUDE_DOMAINS]:
camera_entities = _async_get_matching_entities( camera_entities = _async_get_matching_entities(
self.hass, [CAMERA_DOMAIN] self.hass, [CAMERA_DOMAIN]
) )
self.included_cameras = { self.included_cameras = {
entity_id entity_id: entity_id
for entity_id in camera_entities for entity_id in camera_entities
if entity_id not in entities if entity_id not in entities
} }
@ -571,7 +593,9 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
), ),
) )
async def async_step_init(self, user_input=None): async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle options flow.""" """Handle options flow."""
if self.config_entry.source == SOURCE_IMPORT: if self.config_entry.source == SOURCE_IMPORT:
return await self.async_step_yaml(user_input) return await self.async_step_yaml(user_input)
@ -615,16 +639,16 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
) )
async def _async_get_supported_devices(hass): async def _async_get_supported_devices(hass: HomeAssistant) -> dict[str, str]:
"""Return all supported devices.""" """Return all supported devices."""
results = await device_automation.async_get_device_automations( results = await device_automation.async_get_device_automations(
hass, device_automation.DeviceAutomationType.TRIGGER hass, device_automation.DeviceAutomationType.TRIGGER
) )
dev_reg = device_registry.async_get(hass) dev_reg = device_registry.async_get(hass)
unsorted = { unsorted: dict[str, str] = {}
device_id: dev_reg.async_get(device_id).name or device_id for device_id in results:
for device_id in results entry = dev_reg.async_get(device_id)
} unsorted[device_id] = entry.name or device_id if entry else device_id
return dict(sorted(unsorted.items(), key=lambda item: item[1])) return dict(sorted(unsorted.items(), key=lambda item: item[1]))
@ -641,13 +665,15 @@ def _async_get_matching_entities(
} }
def _domains_set_from_entities(entity_ids): def _domains_set_from_entities(entity_ids: Iterable[str]) -> set[str]:
"""Build a set of domains for the given entity ids.""" """Build a set of domains for the given entity ids."""
return {split_entity_id(entity_id)[0] for entity_id in entity_ids} return {split_entity_id(entity_id)[0] for entity_id in entity_ids}
@callback @callback
def _async_get_entity_ids_for_accessory_mode(hass, include_domains): def _async_get_entity_ids_for_accessory_mode(
hass: HomeAssistant, include_domains: Iterable[str]
) -> list[str]:
"""Build a list of entities that should be paired in accessory mode.""" """Build a list of entities that should be paired in accessory mode."""
accessory_mode_domains = { accessory_mode_domains = {
domain for domain in include_domains if domain in DOMAINS_NEED_ACCESSORY_MODE domain for domain in include_domains if domain in DOMAINS_NEED_ACCESSORY_MODE
@ -664,7 +690,7 @@ def _async_get_entity_ids_for_accessory_mode(hass, include_domains):
@callback @callback
def _async_entity_ids_with_accessory_mode(hass): def _async_entity_ids_with_accessory_mode(hass: HomeAssistant) -> set[str]:
"""Return a set of entity ids that have config entries in accessory mode.""" """Return a set of entity ids that have config entries in accessory mode."""
entity_ids = set() entity_ids = set()

View file

@ -18,7 +18,6 @@ async def async_get_config_entry_diagnostics(
) -> dict[str, Any]: ) -> dict[str, Any]:
"""Return diagnostics for a config entry.""" """Return diagnostics for a config entry."""
homekit: HomeKit = hass.data[DOMAIN][entry.entry_id][HOMEKIT] homekit: HomeKit = hass.data[DOMAIN][entry.entry_id][HOMEKIT]
driver: AccessoryDriver = homekit.driver
data: dict[str, Any] = { data: dict[str, Any] = {
"status": homekit.status, "status": homekit.status,
"config-entry": { "config-entry": {
@ -28,8 +27,9 @@ async def async_get_config_entry_diagnostics(
"options": dict(entry.options), "options": dict(entry.options),
}, },
} }
if not driver: if not hasattr(homekit, "driver"):
return data return data
driver: AccessoryDriver = homekit.driver
data.update(driver.get_accessories()) data.update(driver.get_accessories())
state: State = driver.state state: State = driver.state
data.update( data.update(

View file

@ -1,16 +1,22 @@
"""Describe logbook events.""" """Describe logbook events."""
from collections.abc import Callable
from typing import Any
from homeassistant.const import ATTR_ENTITY_ID, ATTR_SERVICE from homeassistant.const import ATTR_ENTITY_ID, ATTR_SERVICE
from homeassistant.core import callback from homeassistant.core import Event, HomeAssistant, callback
from .const import ATTR_DISPLAY_NAME, ATTR_VALUE, DOMAIN, EVENT_HOMEKIT_CHANGED from .const import ATTR_DISPLAY_NAME, ATTR_VALUE, DOMAIN, EVENT_HOMEKIT_CHANGED
@callback @callback
def async_describe_events(hass, async_describe_event): def async_describe_events(
hass: HomeAssistant,
async_describe_event: Callable[[str, str, Callable[[Event], dict[str, Any]]], None],
) -> None:
"""Describe logbook events.""" """Describe logbook events."""
@callback @callback
def async_describe_logbook_event(event): def async_describe_logbook_event(event: Event) -> dict[str, Any]:
"""Describe a logbook event.""" """Describe a logbook event."""
data = event.data data = event.data
entity_id = data.get(ATTR_ENTITY_ID) entity_id = data.get(ATTR_ENTITY_ID)

View file

@ -1,8 +1,12 @@
"""Class to hold all sensor accessories.""" """Class to hold all sensor accessories."""
from __future__ import annotations
import logging import logging
from typing import Any
from pyhap.const import CATEGORY_SENSOR from pyhap.const import CATEGORY_SENSOR
from homeassistant.core import CALLBACK_TYPE, Context
from homeassistant.helpers.trigger import async_initialize_triggers from homeassistant.helpers.trigger import async_initialize_triggers
from .accessories import TYPES, HomeAccessory from .accessories import TYPES, HomeAccessory
@ -22,14 +26,21 @@ _LOGGER = logging.getLogger(__name__)
class DeviceTriggerAccessory(HomeAccessory): class DeviceTriggerAccessory(HomeAccessory):
"""Generate a Programmable switch.""" """Generate a Programmable switch."""
def __init__(self, *args, device_triggers=None, device_id=None): def __init__(
self,
*args: Any,
device_triggers: list[dict[str, Any]] | None = None,
device_id: str | None = None,
) -> None:
"""Initialize a Programmable switch accessory object.""" """Initialize a Programmable switch accessory object."""
super().__init__(*args, category=CATEGORY_SENSOR, device_id=device_id) super().__init__(*args, category=CATEGORY_SENSOR, device_id=device_id)
assert device_triggers is not None
self._device_triggers = device_triggers self._device_triggers = device_triggers
self._remove_triggers = None self._remove_triggers: CALLBACK_TYPE | None = None
self.triggers = [] self.triggers = []
assert device_triggers is not None
for idx, trigger in enumerate(device_triggers): for idx, trigger in enumerate(device_triggers):
type_ = trigger.get("type") type_ = trigger["type"]
subtype = trigger.get("subtype") subtype = trigger.get("subtype")
trigger_name = ( trigger_name = (
f"{type_.title()} {subtype.title()}" if subtype else type_.title() f"{type_.title()} {subtype.title()}" if subtype else type_.title()
@ -53,7 +64,12 @@ class DeviceTriggerAccessory(HomeAccessory):
serv_service_label.configure_char(CHAR_SERVICE_LABEL_NAMESPACE, value=1) serv_service_label.configure_char(CHAR_SERVICE_LABEL_NAMESPACE, value=1)
serv_stateless_switch.add_linked_service(serv_service_label) serv_stateless_switch.add_linked_service(serv_service_label)
async def async_trigger(self, run_variables, context=None, skip_condition=False): async def async_trigger(
self,
run_variables: dict,
context: Context | None = None,
skip_condition: bool = False,
) -> None:
"""Trigger button press. """Trigger button press.
This method is a coroutine. This method is a coroutine.
@ -67,7 +83,7 @@ class DeviceTriggerAccessory(HomeAccessory):
# Attach the trigger using the helper in async run # Attach the trigger using the helper in async run
# and detach it in async stop # and detach it in async stop
async def run(self): async def run(self) -> None:
"""Handle accessory driver started event.""" """Handle accessory driver started event."""
self._remove_triggers = await async_initialize_triggers( self._remove_triggers = await async_initialize_triggers(
self.hass, self.hass,
@ -78,12 +94,12 @@ class DeviceTriggerAccessory(HomeAccessory):
_LOGGER.log, _LOGGER.log,
) )
async def stop(self): async def stop(self) -> None:
"""Handle accessory driver stop event.""" """Handle accessory driver stop event."""
if self._remove_triggers: if self._remove_triggers:
self._remove_triggers() self._remove_triggers()
@property @property
def available(self): def available(self) -> bool:
"""Return available.""" """Return available."""
return True return True

View file

@ -8,7 +8,9 @@ import os
import re import re
import secrets import secrets
import socket import socket
from typing import Any, cast
from pyhap.accessory import Accessory
import pyqrcode import pyqrcode
import voluptuous as vol import voluptuous as vol
@ -34,7 +36,7 @@ from homeassistant.const import (
CONF_TYPE, CONF_TYPE,
TEMP_CELSIUS, TEMP_CELSIUS,
) )
from homeassistant.core import HomeAssistant, callback, split_entity_id from homeassistant.core import HomeAssistant, State, callback, split_entity_id
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.storage import STORAGE_DIR from homeassistant.helpers.storage import STORAGE_DIR
import homeassistant.util.temperature as temp_util import homeassistant.util.temperature as temp_util
@ -242,7 +244,7 @@ HOMEKIT_CHAR_TRANSLATIONS = {
} }
def validate_entity_config(values): def validate_entity_config(values: dict) -> dict[str, dict]:
"""Validate config entry for CONF_ENTITY.""" """Validate config entry for CONF_ENTITY."""
if not isinstance(values, dict): if not isinstance(values, dict):
raise vol.Invalid("expected a dictionary") raise vol.Invalid("expected a dictionary")
@ -288,7 +290,7 @@ def validate_entity_config(values):
return entities return entities
def get_media_player_features(state): def get_media_player_features(state: State) -> list[str]:
"""Determine features for media players.""" """Determine features for media players."""
features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
@ -306,7 +308,7 @@ def get_media_player_features(state):
return supported_modes return supported_modes
def validate_media_player_features(state, feature_list): def validate_media_player_features(state: State, feature_list: str) -> bool:
"""Validate features for media players.""" """Validate features for media players."""
if not (supported_modes := get_media_player_features(state)): if not (supported_modes := get_media_player_features(state)):
_LOGGER.error("%s does not support any media_player features", state.entity_id) _LOGGER.error("%s does not support any media_player features", state.entity_id)
@ -329,7 +331,9 @@ def validate_media_player_features(state, feature_list):
return True return True
def async_show_setup_message(hass, entry_id, bridge_name, pincode, uri): def async_show_setup_message(
hass: HomeAssistant, entry_id: str, bridge_name: str, pincode: bytes, uri: str
) -> None:
"""Display persistent notification with setup information.""" """Display persistent notification with setup information."""
pin = pincode.decode() pin = pincode.decode()
_LOGGER.info("Pincode: %s", pin) _LOGGER.info("Pincode: %s", pin)
@ -351,12 +355,12 @@ def async_show_setup_message(hass, entry_id, bridge_name, pincode, uri):
persistent_notification.async_create(hass, message, "HomeKit Pairing", entry_id) persistent_notification.async_create(hass, message, "HomeKit Pairing", entry_id)
def async_dismiss_setup_message(hass, entry_id): def async_dismiss_setup_message(hass: HomeAssistant, entry_id: str) -> None:
"""Dismiss persistent notification and remove QR code.""" """Dismiss persistent notification and remove QR code."""
persistent_notification.async_dismiss(hass, entry_id) persistent_notification.async_dismiss(hass, entry_id)
def convert_to_float(state): def convert_to_float(state: Any) -> float | None:
"""Return float of state, catch errors.""" """Return float of state, catch errors."""
try: try:
return float(state) return float(state)
@ -384,17 +388,17 @@ def cleanup_name_for_homekit(name: str | None) -> str:
return name.translate(HOMEKIT_CHAR_TRANSLATIONS)[:MAX_NAME_LENGTH] return name.translate(HOMEKIT_CHAR_TRANSLATIONS)[:MAX_NAME_LENGTH]
def temperature_to_homekit(temperature, unit): def temperature_to_homekit(temperature: float | int, unit: str) -> float:
"""Convert temperature to Celsius for HomeKit.""" """Convert temperature to Celsius for HomeKit."""
return round(temp_util.convert(temperature, unit, TEMP_CELSIUS), 1) return round(temp_util.convert(temperature, unit, TEMP_CELSIUS), 1)
def temperature_to_states(temperature, unit): def temperature_to_states(temperature: float | int, unit: str) -> float:
"""Convert temperature back from Celsius to Home Assistant unit.""" """Convert temperature back from Celsius to Home Assistant unit."""
return round(temp_util.convert(temperature, TEMP_CELSIUS, unit) * 2) / 2 return round(temp_util.convert(temperature, TEMP_CELSIUS, unit) * 2) / 2
def density_to_air_quality(density): def density_to_air_quality(density: float) -> int:
"""Map PM2.5 density to HomeKit AirQuality level.""" """Map PM2.5 density to HomeKit AirQuality level."""
if density <= 35: if density <= 35:
return 1 return 1
@ -407,7 +411,7 @@ def density_to_air_quality(density):
return 5 return 5
def density_to_air_quality_pm10(density): def density_to_air_quality_pm10(density: float) -> int:
"""Map PM10 density to HomeKit AirQuality level.""" """Map PM10 density to HomeKit AirQuality level."""
if density <= 40: if density <= 40:
return 1 return 1
@ -420,7 +424,7 @@ def density_to_air_quality_pm10(density):
return 5 return 5
def density_to_air_quality_pm25(density): def density_to_air_quality_pm25(density: float) -> int:
"""Map PM2.5 density to HomeKit AirQuality level.""" """Map PM2.5 density to HomeKit AirQuality level."""
if density <= 25: if density <= 25:
return 1 return 1
@ -433,22 +437,22 @@ def density_to_air_quality_pm25(density):
return 5 return 5
def get_persist_filename_for_entry_id(entry_id: str): def get_persist_filename_for_entry_id(entry_id: str) -> str:
"""Determine the filename of the homekit state file.""" """Determine the filename of the homekit state file."""
return f"{DOMAIN}.{entry_id}.state" return f"{DOMAIN}.{entry_id}.state"
def get_aid_storage_filename_for_entry_id(entry_id: str): def get_aid_storage_filename_for_entry_id(entry_id: str) -> str:
"""Determine the ilename of homekit aid storage file.""" """Determine the ilename of homekit aid storage file."""
return f"{DOMAIN}.{entry_id}.aids" return f"{DOMAIN}.{entry_id}.aids"
def get_persist_fullpath_for_entry_id(hass: HomeAssistant, entry_id: str): def get_persist_fullpath_for_entry_id(hass: HomeAssistant, entry_id: str) -> str:
"""Determine the path to the homekit state file.""" """Determine the path to the homekit state file."""
return hass.config.path(STORAGE_DIR, get_persist_filename_for_entry_id(entry_id)) return hass.config.path(STORAGE_DIR, get_persist_filename_for_entry_id(entry_id))
def get_aid_storage_fullpath_for_entry_id(hass: HomeAssistant, entry_id: str): def get_aid_storage_fullpath_for_entry_id(hass: HomeAssistant, entry_id: str) -> str:
"""Determine the path to the homekit aid storage file.""" """Determine the path to the homekit aid storage file."""
return hass.config.path( return hass.config.path(
STORAGE_DIR, get_aid_storage_filename_for_entry_id(entry_id) STORAGE_DIR, get_aid_storage_filename_for_entry_id(entry_id)
@ -459,7 +463,7 @@ def _format_version_part(version_part: str) -> str:
return str(max(0, min(MAX_VERSION_PART, coerce_int(version_part)))) return str(max(0, min(MAX_VERSION_PART, coerce_int(version_part))))
def format_version(version): def format_version(version: str) -> str | None:
"""Extract the version string in a format homekit can consume.""" """Extract the version string in a format homekit can consume."""
split_ver = str(version).replace("-", ".").replace(" ", ".") split_ver = str(version).replace("-", ".").replace(" ", ".")
num_only = NUMBERS_ONLY_RE.sub("", split_ver) num_only = NUMBERS_ONLY_RE.sub("", split_ver)
@ -469,12 +473,12 @@ def format_version(version):
return None if _is_zero_but_true(value) else value return None if _is_zero_but_true(value) else value
def _is_zero_but_true(value): def _is_zero_but_true(value: Any) -> bool:
"""Zero but true values can crash apple watches.""" """Zero but true values can crash apple watches."""
return convert_to_float(value) == 0 return convert_to_float(value) == 0
def remove_state_files_for_entry_id(hass: HomeAssistant, entry_id: str): def remove_state_files_for_entry_id(hass: HomeAssistant, entry_id: str) -> bool:
"""Remove the state files from disk.""" """Remove the state files from disk."""
persist_file_path = get_persist_fullpath_for_entry_id(hass, entry_id) persist_file_path = get_persist_fullpath_for_entry_id(hass, entry_id)
aid_storage_path = get_aid_storage_fullpath_for_entry_id(hass, entry_id) aid_storage_path = get_aid_storage_fullpath_for_entry_id(hass, entry_id)
@ -484,7 +488,7 @@ def remove_state_files_for_entry_id(hass: HomeAssistant, entry_id: str):
return True return True
def _get_test_socket(): def _get_test_socket() -> socket.socket:
"""Create a socket to test binding ports.""" """Create a socket to test binding ports."""
test_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) test_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
test_socket.setblocking(False) test_socket.setblocking(False)
@ -527,9 +531,10 @@ def _async_find_next_available_port(start_port: int, exclude_ports: set) -> int:
if port == MAX_PORT: if port == MAX_PORT:
raise raise
continue continue
raise RuntimeError("unreachable")
def pid_is_alive(pid) -> bool: def pid_is_alive(pid: int) -> bool:
"""Check to see if a process is alive.""" """Check to see if a process is alive."""
try: try:
os.kill(pid, 0) os.kill(pid, 0)
@ -539,14 +544,14 @@ def pid_is_alive(pid) -> bool:
return False return False
def accessory_friendly_name(hass_name, accessory): def accessory_friendly_name(hass_name: str, accessory: Accessory) -> str:
"""Return the combined name for the accessory. """Return the combined name for the accessory.
The mDNS name and the Home Assistant config entry The mDNS name and the Home Assistant config entry
name are usually different which means they need to name are usually different which means they need to
see both to identify the accessory. see both to identify the accessory.
""" """
accessory_mdns_name = accessory.display_name accessory_mdns_name = cast(str, accessory.display_name)
if hass_name.casefold().startswith(accessory_mdns_name.casefold()): if hass_name.casefold().startswith(accessory_mdns_name.casefold()):
return hass_name return hass_name
if accessory_mdns_name.casefold().startswith(hass_name.casefold()): if accessory_mdns_name.casefold().startswith(hass_name.casefold()):
@ -554,7 +559,7 @@ def accessory_friendly_name(hass_name, accessory):
return f"{hass_name} ({accessory_mdns_name})" return f"{hass_name} ({accessory_mdns_name})"
def state_needs_accessory_mode(state): def state_needs_accessory_mode(state: State) -> bool:
"""Return if the entity represented by the state must be paired in accessory mode.""" """Return if the entity represented by the state must be paired in accessory mode."""
if state.domain in (CAMERA_DOMAIN, LOCK_DOMAIN): if state.domain in (CAMERA_DOMAIN, LOCK_DOMAIN):
return True return True

View file

@ -891,6 +891,94 @@ no_implicit_optional = true
warn_return_any = true warn_return_any = true
warn_unreachable = true warn_unreachable = true
[mypy-homeassistant.components.homekit]
check_untyped_defs = true
disallow_incomplete_defs = true
disallow_subclassing_any = true
disallow_untyped_calls = true
disallow_untyped_decorators = true
disallow_untyped_defs = true
no_implicit_optional = true
warn_return_any = true
warn_unreachable = true
[mypy-homeassistant.components.homekit.accessories]
check_untyped_defs = true
disallow_incomplete_defs = true
disallow_subclassing_any = true
disallow_untyped_calls = true
disallow_untyped_decorators = true
disallow_untyped_defs = true
no_implicit_optional = true
warn_return_any = true
warn_unreachable = true
[mypy-homeassistant.components.homekit.aidmanager]
check_untyped_defs = true
disallow_incomplete_defs = true
disallow_subclassing_any = true
disallow_untyped_calls = true
disallow_untyped_decorators = true
disallow_untyped_defs = true
no_implicit_optional = true
warn_return_any = true
warn_unreachable = true
[mypy-homeassistant.components.homekit.config_flow]
check_untyped_defs = true
disallow_incomplete_defs = true
disallow_subclassing_any = true
disallow_untyped_calls = true
disallow_untyped_decorators = true
disallow_untyped_defs = true
no_implicit_optional = true
warn_return_any = true
warn_unreachable = true
[mypy-homeassistant.components.homekit.diagnostics]
check_untyped_defs = true
disallow_incomplete_defs = true
disallow_subclassing_any = true
disallow_untyped_calls = true
disallow_untyped_decorators = true
disallow_untyped_defs = true
no_implicit_optional = true
warn_return_any = true
warn_unreachable = true
[mypy-homeassistant.components.homekit.logbook]
check_untyped_defs = true
disallow_incomplete_defs = true
disallow_subclassing_any = true
disallow_untyped_calls = true
disallow_untyped_decorators = true
disallow_untyped_defs = true
no_implicit_optional = true
warn_return_any = true
warn_unreachable = true
[mypy-homeassistant.components.homekit.type_triggers]
check_untyped_defs = true
disallow_incomplete_defs = true
disallow_subclassing_any = true
disallow_untyped_calls = true
disallow_untyped_decorators = true
disallow_untyped_defs = true
no_implicit_optional = true
warn_return_any = true
warn_unreachable = true
[mypy-homeassistant.components.homekit.util]
check_untyped_defs = true
disallow_incomplete_defs = true
disallow_subclassing_any = true
disallow_untyped_calls = true
disallow_untyped_decorators = true
disallow_untyped_defs = true
no_implicit_optional = true
warn_return_any = true
warn_unreachable = true
[mypy-homeassistant.components.homekit_controller] [mypy-homeassistant.components.homekit_controller]
check_untyped_defs = true check_untyped_defs = true
disallow_incomplete_defs = true disallow_incomplete_defs = true
@ -2566,15 +2654,6 @@ ignore_errors = true
[mypy-homeassistant.components.home_plus_control.api] [mypy-homeassistant.components.home_plus_control.api]
ignore_errors = true ignore_errors = true
[mypy-homeassistant.components.homekit.aidmanager]
ignore_errors = true
[mypy-homeassistant.components.homekit.config_flow]
ignore_errors = true
[mypy-homeassistant.components.homekit.util]
ignore_errors = true
[mypy-homeassistant.components.honeywell.climate] [mypy-homeassistant.components.honeywell.climate]
ignore_errors = true ignore_errors = true

View file

@ -60,9 +60,6 @@ IGNORED_MODULES: Final[list[str]] = [
"homeassistant.components.here_travel_time.sensor", "homeassistant.components.here_travel_time.sensor",
"homeassistant.components.home_plus_control", "homeassistant.components.home_plus_control",
"homeassistant.components.home_plus_control.api", "homeassistant.components.home_plus_control.api",
"homeassistant.components.homekit.aidmanager",
"homeassistant.components.homekit.config_flow",
"homeassistant.components.homekit.util",
"homeassistant.components.honeywell.climate", "homeassistant.components.honeywell.climate",
"homeassistant.components.icloud", "homeassistant.components.icloud",
"homeassistant.components.icloud.account", "homeassistant.components.icloud.account",