diff --git a/.strict-typing b/.strict-typing index cd74910415c..e84e01b1803 100644 --- a/.strict-typing +++ b/.strict-typing @@ -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.* diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index 974cb5e1dfc..eeca98167d0 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -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: diff --git a/homeassistant/components/homekit_controller/climate.py b/homeassistant/components/homekit_controller/climate.py index e8179b0bc6b..53aee8561e4 100644 --- a/homeassistant/components/homekit_controller/climate.py +++ b/homeassistant/components/homekit_controller/climate.py @@ -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 diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py index 8bd5351906c..e4ad06f322a 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -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 diff --git a/homeassistant/components/homekit_controller/device_trigger.py b/homeassistant/components/homekit_controller/device_trigger.py index a69d189ebd5..aa2765d9be5 100644 --- a/homeassistant/components/homekit_controller/device_trigger.py +++ b/homeassistant/components/homekit_controller/device_trigger.py @@ -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: diff --git a/homeassistant/components/homekit_controller/humidifier.py b/homeassistant/components/homekit_controller/humidifier.py index bc922dd4ec1..fcca3e54725 100644 --- a/homeassistant/components/homekit_controller/humidifier.py +++ b/homeassistant/components/homekit_controller/humidifier.py @@ -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: diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index fcb36d3c54a..8cc6ce2b575 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -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"], diff --git a/homeassistant/components/homekit_controller/media_player.py b/homeassistant/components/homekit_controller/media_player.py index 7318343b643..6314efe9dc4 100644 --- a/homeassistant/components/homekit_controller/media_player.py +++ b/homeassistant/components/homekit_controller/media_player.py @@ -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() diff --git a/homeassistant/components/homekit_controller/number.py b/homeassistant/components/homekit_controller/number.py index a473e30fe6d..5fcb5027640 100644 --- a/homeassistant/components/homekit_controller/number.py +++ b/homeassistant/components/homekit_controller/number.py @@ -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: diff --git a/homeassistant/components/homekit_controller/storage.py b/homeassistant/components/homekit_controller/storage.py index fc6970f078a..9372764a88a 100644 --- a/homeassistant/components/homekit_controller/storage.py +++ b/homeassistant/components/homekit_controller/storage.py @@ -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) diff --git a/mypy.ini b/mypy.ini index cc5d9b0e5e4..32ee9352326 100644 --- a/mypy.ini +++ b/mypy.ini @@ -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 diff --git a/requirements_all.txt b/requirements_all.txt index 602eeae6b2c..79407a8df6d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7e4614d127b..d71ccbfadc5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -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