Enable types from aiohomekit to be used by mypy for homekit_controller (#65433)

This commit is contained in:
Jc2k 2022-02-03 16:18:03 +00:00 committed by GitHub
parent 6c38a6b569
commit 714a952d73
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 200 additions and 90 deletions

View File

@ -87,6 +87,13 @@ homeassistant.components.group.*
homeassistant.components.guardian.*
homeassistant.components.history.*
homeassistant.components.homeassistant.triggers.event
homeassistant.components.homekit_controller
homeassistant.components.homekit_controller.alarm_control_panel
homeassistant.components.homekit_controller.button
homeassistant.components.homekit_controller.const
homeassistant.components.homekit_controller.lock
homeassistant.components.homekit_controller.select
homeassistant.components.homekit_controller.storage
homeassistant.components.homewizard.*
homeassistant.components.http.*
homeassistant.components.huawei_lte.*

View File

@ -136,7 +136,7 @@ class HomeKitEntity(Entity):
@property
def name(self) -> str | None:
"""Return the name of the device if any."""
return self.accessory_info.value(CharacteristicsTypes.NAME)
return self.accessory.name
@property
def available(self) -> bool:

View File

@ -2,7 +2,7 @@
from __future__ import annotations
import logging
from typing import Any
from typing import Any, Final
from aiohomekit.model.characteristics import (
ActivationStateValues,
@ -16,7 +16,11 @@ from aiohomekit.model.characteristics import (
from aiohomekit.model.services import Service, ServicesTypes
from aiohomekit.utils import clamp_enum_to_char
from homeassistant.components.climate import ClimateEntity
from homeassistant.components.climate import (
DEFAULT_MAX_TEMP,
DEFAULT_MIN_TEMP,
ClimateEntity,
)
from homeassistant.components.climate.const import (
ATTR_HVAC_MODE,
ATTR_TARGET_TEMP_HIGH,
@ -86,6 +90,8 @@ TARGET_HEATER_COOLER_STATE_HASS_TO_HOMEKIT = {
SWING_MODE_HASS_TO_HOMEKIT = {v: k for k, v in SWING_MODE_HOMEKIT_TO_HASS.items()}
DEFAULT_MIN_STEP: Final = 1.0
async def async_setup_entry(
hass: HomeAssistant,
@ -185,22 +191,24 @@ class HomeKitHeaterCoolerEntity(HomeKitEntity, ClimateEntity):
return None
@property
def target_temperature_step(self) -> float | None:
def target_temperature_step(self) -> float:
"""Return the supported step of target temperature."""
state = self.service.value(CharacteristicsTypes.TARGET_HEATER_COOLER_STATE)
if state == TargetHeaterCoolerStateValues.COOL and self.service.has(
CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD
):
return self.service[
CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD
].minStep
return (
self.service[CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD].minStep
or DEFAULT_MIN_STEP
)
if state == TargetHeaterCoolerStateValues.HEAT and self.service.has(
CharacteristicsTypes.TEMPERATURE_HEATING_THRESHOLD
):
return self.service[
CharacteristicsTypes.TEMPERATURE_HEATING_THRESHOLD
].minStep
return None
return (
self.service[CharacteristicsTypes.TEMPERATURE_HEATING_THRESHOLD].minStep
or DEFAULT_MIN_STEP
)
return DEFAULT_MIN_STEP
@property
def min_temp(self) -> float:
@ -209,15 +217,21 @@ class HomeKitHeaterCoolerEntity(HomeKitEntity, ClimateEntity):
if state == TargetHeaterCoolerStateValues.COOL and self.service.has(
CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD
):
return self.service[
CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD
].minValue
return (
self.service[
CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD
].minValue
or DEFAULT_MIN_TEMP
)
if state == TargetHeaterCoolerStateValues.HEAT and self.service.has(
CharacteristicsTypes.TEMPERATURE_HEATING_THRESHOLD
):
return self.service[
CharacteristicsTypes.TEMPERATURE_HEATING_THRESHOLD
].minValue
return (
self.service[
CharacteristicsTypes.TEMPERATURE_HEATING_THRESHOLD
].minValue
or DEFAULT_MIN_TEMP
)
return super().min_temp
@property
@ -227,15 +241,21 @@ class HomeKitHeaterCoolerEntity(HomeKitEntity, ClimateEntity):
if state == TargetHeaterCoolerStateValues.COOL and self.service.has(
CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD
):
return self.service[
CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD
].maxValue
return (
self.service[
CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD
].maxValue
or DEFAULT_MAX_TEMP
)
if state == TargetHeaterCoolerStateValues.HEAT and self.service.has(
CharacteristicsTypes.TEMPERATURE_HEATING_THRESHOLD
):
return self.service[
CharacteristicsTypes.TEMPERATURE_HEATING_THRESHOLD
].maxValue
return (
self.service[
CharacteristicsTypes.TEMPERATURE_HEATING_THRESHOLD
].maxValue
or DEFAULT_MAX_TEMP
)
return super().max_temp
@property
@ -345,9 +365,9 @@ class HomeKitClimateEntity(HomeKitEntity, ClimateEntity):
CharacteristicsTypes.RELATIVE_HUMIDITY_TARGET,
]
async def async_set_temperature(self, **kwargs) -> None:
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperature."""
chars = {}
chars: dict[str, Any] = {}
value = self.service.value(CharacteristicsTypes.HEATING_COOLING_TARGET)
mode = MODE_HOMEKIT_TO_HASS[value]
@ -499,7 +519,7 @@ class HomeKitClimateEntity(HomeKitEntity, ClimateEntity):
CharacteristicsTypes.RELATIVE_HUMIDITY_TARGET
].minValue
if min_humidity is not None:
return min_humidity
return int(min_humidity)
return super().min_humidity
@property
@ -509,7 +529,7 @@ class HomeKitClimateEntity(HomeKitEntity, ClimateEntity):
CharacteristicsTypes.RELATIVE_HUMIDITY_TARGET
].maxValue
if max_humidity is not None:
return max_humidity
return int(max_humidity)
return super().max_humidity
@property

View File

@ -13,8 +13,8 @@ from aiohomekit.exceptions import (
EncryptionError,
)
from aiohomekit.model import Accessories, Accessory
from aiohomekit.model.characteristics import Characteristic, CharacteristicsTypes
from aiohomekit.model.services import Service, ServicesTypes
from aiohomekit.model.characteristics import Characteristic
from aiohomekit.model.services import Service
from homeassistant.const import ATTR_VIA_DEVICE
from homeassistant.core import CALLBACK_TYPE, callback
@ -46,7 +46,7 @@ AddServiceCb = Callable[[Service], bool]
AddCharacteristicCb = Callable[[Characteristic], bool]
def valid_serial_number(serial):
def valid_serial_number(serial: str) -> bool:
"""Return if the serial number appears to be valid."""
if not serial:
return False
@ -190,10 +190,6 @@ class HKDevice:
def device_info_for_accessory(self, accessory: Accessory) -> DeviceInfo:
"""Build a DeviceInfo for a given accessory."""
info = accessory.services.first(
service_type=ServicesTypes.ACCESSORY_INFORMATION,
)
identifiers = {
(
IDENTIFIER_ACCESSORY_ID,
@ -202,16 +198,15 @@ class HKDevice:
}
if not self.unreliable_serial_numbers:
serial_number = info.value(CharacteristicsTypes.SERIAL_NUMBER)
identifiers.add((IDENTIFIER_SERIAL_NUMBER, serial_number))
identifiers.add((IDENTIFIER_SERIAL_NUMBER, accessory.serial_number))
device_info = DeviceInfo(
identifiers=identifiers,
name=info.value(CharacteristicsTypes.NAME),
manufacturer=info.value(CharacteristicsTypes.MANUFACTURER, ""),
model=info.value(CharacteristicsTypes.MODEL, ""),
sw_version=info.value(CharacteristicsTypes.FIRMWARE_REVISION, ""),
hw_version=info.value(CharacteristicsTypes.HARDWARE_REVISION, ""),
name=accessory.name,
manufacturer=accessory.manufacturer,
model=accessory.model,
sw_version=accessory.firmware_revision,
hw_version=accessory.hardware_revision,
)
if accessory.aid != 1:
@ -235,10 +230,6 @@ class HKDevice:
device_registry = dr.async_get(self.hass)
for accessory in self.entity_map.accessories:
info = accessory.services.first(
service_type=ServicesTypes.ACCESSORY_INFORMATION,
)
identifiers = {
(
DOMAIN,
@ -252,10 +243,9 @@ class HKDevice:
(DOMAIN, IDENTIFIER_LEGACY_ACCESSORY_ID, self.unique_id)
)
serial_number = info.value(CharacteristicsTypes.SERIAL_NUMBER)
if valid_serial_number(serial_number):
if valid_serial_number(accessory.serial_number):
identifiers.add(
(DOMAIN, IDENTIFIER_LEGACY_SERIAL_NUMBER, serial_number)
(DOMAIN, IDENTIFIER_LEGACY_SERIAL_NUMBER, accessory.serial_number)
)
device = device_registry.async_get_device(identifiers=identifiers) # type: ignore[arg-type]
@ -284,8 +274,7 @@ class HKDevice:
}
if not self.unreliable_serial_numbers:
serial_number = info.value(CharacteristicsTypes.SERIAL_NUMBER)
new_identifiers.add((IDENTIFIER_SERIAL_NUMBER, serial_number))
new_identifiers.add((IDENTIFIER_SERIAL_NUMBER, accessory.serial_number))
else:
_LOGGER.debug(
"Not migrating serial number identifier for %s:aid:%s (it is wrong, not unique or unreliable)",
@ -334,35 +323,29 @@ class HKDevice:
devices = set()
for accessory in self.entity_map.accessories:
info = accessory.services.first(
service_type=ServicesTypes.ACCESSORY_INFORMATION,
)
serial_number = info.value(CharacteristicsTypes.SERIAL_NUMBER)
if not valid_serial_number(serial_number):
if not valid_serial_number(accessory.serial_number):
_LOGGER.debug(
"Serial number %r is not valid, it cannot be used as a unique identifier",
serial_number,
accessory.serial_number,
)
unreliable_serial_numbers = True
elif serial_number in devices:
elif accessory.serial_number in devices:
_LOGGER.debug(
"Serial number %r is duplicated within this pairing, it cannot be used as a unique identifier",
serial_number,
accessory.serial_number,
)
unreliable_serial_numbers = True
elif serial_number == info.value(CharacteristicsTypes.HARDWARE_REVISION):
elif accessory.serial_number == accessory.hardware_revision:
# This is a known bug with some devices (e.g. RYSE SmartShades)
_LOGGER.debug(
"Serial number %r is actually the hardware revision, it cannot be used as a unique identifier",
serial_number,
accessory.serial_number,
)
unreliable_serial_numbers = True
devices.add(serial_number)
devices.add(accessory.serial_number)
self.unreliable_serial_numbers = unreliable_serial_numbers

View File

@ -1,7 +1,7 @@
"""Provides device automations for homekit devices."""
from __future__ import annotations
from typing import Any
from typing import TYPE_CHECKING, Any
from aiohomekit.model.characteristics import CharacteristicsTypes
from aiohomekit.model.characteristics.const import InputEventValues
@ -20,6 +20,9 @@ from homeassistant.helpers.typing import ConfigType
from .const import DOMAIN, KNOWN_DEVICES, TRIGGERS
if TYPE_CHECKING:
from .connection import HKDevice
TRIGGER_TYPES = {
"doorbell",
"button1",
@ -225,7 +228,7 @@ async def async_setup_triggers_for_entry(hass: HomeAssistant, config_entry):
conn.add_listener(async_add_service)
def async_fire_triggers(conn, events):
def async_fire_triggers(conn: HKDevice, events: dict[tuple[int, int], Any]):
"""Process events generated by a HomeKit accessory into automation triggers."""
for (aid, iid), ev in events.items():
if aid in conn.devices:

View File

@ -8,6 +8,8 @@ from aiohomekit.model.services import Service, ServicesTypes
from homeassistant.components.humidifier import HumidifierDeviceClass, HumidifierEntity
from homeassistant.components.humidifier.const import (
DEFAULT_MAX_HUMIDITY,
DEFAULT_MIN_HUMIDITY,
MODE_AUTO,
MODE_NORMAL,
SUPPORT_MODES,
@ -123,16 +125,22 @@ class HomeKitHumidifier(HomeKitEntity, HumidifierEntity):
@property
def min_humidity(self) -> int:
"""Return the minimum humidity."""
return self.service[
CharacteristicsTypes.RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD
].minValue
return int(
self.service[
CharacteristicsTypes.RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD
].minValue
or DEFAULT_MIN_HUMIDITY
)
@property
def max_humidity(self) -> int:
"""Return the maximum humidity."""
return self.service[
CharacteristicsTypes.RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD
].maxValue
return int(
self.service[
CharacteristicsTypes.RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD
].maxValue
or DEFAULT_MAX_HUMIDITY
)
class HomeKitDehumidifier(HomeKitEntity, HumidifierEntity):
@ -225,16 +233,22 @@ class HomeKitDehumidifier(HomeKitEntity, HumidifierEntity):
@property
def min_humidity(self) -> int:
"""Return the minimum humidity."""
return self.service[
CharacteristicsTypes.RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD
].minValue
return int(
self.service[
CharacteristicsTypes.RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD
].minValue
or DEFAULT_MIN_HUMIDITY
)
@property
def max_humidity(self) -> int:
"""Return the maximum humidity."""
return self.service[
CharacteristicsTypes.RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD
].maxValue
return int(
self.service[
CharacteristicsTypes.RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD
].maxValue
or DEFAULT_MAX_HUMIDITY
)
@property
def unique_id(self) -> str:

View File

@ -3,7 +3,7 @@
"name": "HomeKit Controller",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/homekit_controller",
"requirements": ["aiohomekit==0.7.0"],
"requirements": ["aiohomekit==0.7.5"],
"zeroconf": ["_hap._tcp.local."],
"after_dependencies": ["zeroconf"],
"codeowners": ["@Jc2k", "@bdraco"],

View File

@ -121,7 +121,7 @@ class HomeKitTelevision(HomeKitEntity, MediaPlayerEntity):
)
@property
def supported_remote_keys(self) -> set[str]:
def supported_remote_keys(self) -> set[int]:
"""Remote key buttons that are supported."""
if not self.service.has(CharacteristicsTypes.REMOTE_KEY):
return set()

View File

@ -9,6 +9,11 @@ from __future__ import annotations
from aiohomekit.model.characteristics import Characteristic, CharacteristicsTypes
from homeassistant.components.number import NumberEntity, NumberEntityDescription
from homeassistant.components.number.const import (
DEFAULT_MAX_VALUE,
DEFAULT_MIN_VALUE,
DEFAULT_STEP,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity import EntityCategory
@ -137,17 +142,17 @@ class HomeKitNumber(CharacteristicEntity, NumberEntity):
@property
def min_value(self) -> float:
"""Return the minimum value."""
return self._char.minValue
return self._char.minValue or DEFAULT_MIN_VALUE
@property
def max_value(self) -> float:
"""Return the maximum value."""
return self._char.maxValue
return self._char.maxValue or DEFAULT_MAX_VALUE
@property
def step(self) -> float:
"""Return the increment/decrement step."""
return self._char.minStep
return self._char.minStep or DEFAULT_STEP
@property
def value(self) -> float:
@ -181,17 +186,17 @@ class HomeKitEcobeeFanModeNumber(CharacteristicEntity, NumberEntity):
@property
def min_value(self) -> float:
"""Return the minimum value."""
return self._char.minValue
return self._char.minValue or DEFAULT_MIN_VALUE
@property
def max_value(self) -> float:
"""Return the maximum value."""
return self._char.maxValue
return self._char.maxValue or DEFAULT_MAX_VALUE
@property
def step(self) -> float:
"""Return the increment/decrement step."""
return self._char.minStep
return self._char.minStep or DEFAULT_STEP
@property
def value(self) -> float:

View File

@ -51,13 +51,14 @@ class EntityMapStorage:
async def async_initialize(self) -> None:
"""Get the pairing cache data."""
if not (raw_storage := cast(StorageLayout, await self.store.async_load())):
if not (raw_storage := await self.store.async_load()):
# There is no cached data about HomeKit devices yet
return
self.storage_data = raw_storage.get("pairings", {})
storage = cast(StorageLayout, raw_storage)
self.storage_data = storage.get("pairings", {})
def get_map(self, homekit_id) -> Pairing | None:
def get_map(self, homekit_id: str) -> Pairing | None:
"""Get a pairing cache item."""
return self.storage_data.get(homekit_id)

View File

@ -774,6 +774,83 @@ no_implicit_optional = true
warn_return_any = true
warn_unreachable = true
[mypy-homeassistant.components.homekit_controller]
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.alarm_control_panel]
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.button]
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.const]
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.lock]
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.select]
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.storage]
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.homewizard.*]
check_untyped_defs = true
disallow_incomplete_defs = true

View File

@ -184,7 +184,7 @@ aioguardian==2021.11.0
aioharmony==0.2.9
# homeassistant.components.homekit_controller
aiohomekit==0.7.0
aiohomekit==0.7.5
# homeassistant.components.emulated_hue
# homeassistant.components.http

View File

@ -134,7 +134,7 @@ aioguardian==2021.11.0
aioharmony==0.2.9
# homeassistant.components.homekit_controller
aiohomekit==0.7.0
aiohomekit==0.7.5
# homeassistant.components.emulated_hue
# homeassistant.components.http