Add strict typing to fritzbox (#50486)

* enable strict typing

* apply suggestions

* set defaults for FritzboxConfigFlow

* improvements and suggestions

* another suggestion

* tweaks

* tweaks
This commit is contained in:
Michael 2021-05-15 07:54:11 +02:00 committed by GitHub
parent d37a3cded0
commit 25b2fd0cee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 195 additions and 107 deletions

View File

@ -13,6 +13,7 @@ homeassistant.components.camera.*
homeassistant.components.cover.* homeassistant.components.cover.*
homeassistant.components.device_automation.* homeassistant.components.device_automation.*
homeassistant.components.elgato.* homeassistant.components.elgato.*
homeassistant.components.fritzbox.*
homeassistant.components.frontend.* homeassistant.components.frontend.*
homeassistant.components.geo_location.* homeassistant.components.geo_location.*
homeassistant.components.gios.* homeassistant.components.gios.*

View File

@ -17,14 +17,16 @@ from homeassistant.const import (
CONF_USERNAME, CONF_USERNAME,
EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_STOP,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import Event, HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.update_coordinator import ( from homeassistant.helpers.update_coordinator import (
CoordinatorEntity, CoordinatorEntity,
DataUpdateCoordinator, DataUpdateCoordinator,
) )
from .const import CONF_CONNECTIONS, CONF_COORDINATOR, DOMAIN, LOGGER, PLATFORMS from .const import CONF_CONNECTIONS, CONF_COORDINATOR, DOMAIN, LOGGER, PLATFORMS
from .model import EntityInfo
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
@ -63,7 +65,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
data[device.ain] = device data[device.ain] = device
return data return data
async def async_update_coordinator(): async def async_update_coordinator() -> dict[str, FritzhomeDevice]:
"""Fetch all device data.""" """Fetch all device data."""
return await hass.async_add_executor_job(_update_fritz_devices) return await hass.async_add_executor_job(_update_fritz_devices)
@ -81,7 +83,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass.config_entries.async_setup_platforms(entry, PLATFORMS) hass.config_entries.async_setup_platforms(entry, PLATFORMS)
def logout_fritzbox(event): def logout_fritzbox(event: Event) -> None:
"""Close connections to this fritzbox.""" """Close connections to this fritzbox."""
fritz.logout() fritz.logout()
@ -109,10 +111,10 @@ class FritzBoxEntity(CoordinatorEntity):
def __init__( def __init__(
self, self,
entity_info: dict[str, str], entity_info: EntityInfo,
coordinator: DataUpdateCoordinator, coordinator: DataUpdateCoordinator[dict[str, FritzhomeDevice]],
ain: str, ain: str,
): ) -> None:
"""Initialize the FritzBox entity.""" """Initialize the FritzBox entity."""
super().__init__(coordinator) super().__init__(coordinator)
@ -128,7 +130,7 @@ class FritzBoxEntity(CoordinatorEntity):
return self.coordinator.data[self.ain] return self.coordinator.data[self.ain]
@property @property
def device_info(self): def device_info(self) -> DeviceInfo:
"""Return device specific attributes.""" """Return device specific attributes."""
return { return {
"name": self.device.name, "name": self.device.name,
@ -139,21 +141,21 @@ class FritzBoxEntity(CoordinatorEntity):
} }
@property @property
def unique_id(self): def unique_id(self) -> str:
"""Return the unique ID of the device.""" """Return the unique ID of the device."""
return self._unique_id return self._unique_id
@property @property
def name(self): def name(self) -> str:
"""Return the name of the device.""" """Return the name of the device."""
return self._name return self._name
@property @property
def unit_of_measurement(self): def unit_of_measurement(self) -> str | None:
"""Return the unit of measurement.""" """Return the unit of measurement."""
return self._unit_of_measurement return self._unit_of_measurement
@property @property
def device_class(self): def device_class(self) -> str | None:
"""Return the device class.""" """Return the device class."""
return self._device_class return self._device_class

View File

@ -1,4 +1,6 @@
"""Support for Fritzbox binary sensors.""" """Support for Fritzbox binary sensors."""
from __future__ import annotations
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
DEVICE_CLASS_WINDOW, DEVICE_CLASS_WINDOW,
BinarySensorEntity, BinarySensorEntity,
@ -21,7 +23,7 @@ async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None: ) -> None:
"""Set up the FRITZ!SmartHome binary sensor from ConfigEntry.""" """Set up the FRITZ!SmartHome binary sensor from ConfigEntry."""
entities = [] entities: list[FritzboxBinarySensor] = []
coordinator = hass.data[FRITZBOX_DOMAIN][entry.entry_id][CONF_COORDINATOR] coordinator = hass.data[FRITZBOX_DOMAIN][entry.entry_id][CONF_COORDINATOR]
for ain, device in coordinator.data.items(): for ain, device in coordinator.data.items():
@ -48,8 +50,8 @@ class FritzboxBinarySensor(FritzBoxEntity, BinarySensorEntity):
"""Representation of a binary FRITZ!SmartHome device.""" """Representation of a binary FRITZ!SmartHome device."""
@property @property
def is_on(self): def is_on(self) -> bool:
"""Return true if sensor is on.""" """Return true if sensor is on."""
if not self.device.present: if not self.device.present:
return False return False
return self.device.alert_state return self.device.alert_state # type: ignore [no-any-return]

View File

@ -1,4 +1,8 @@
"""Support for AVM FRITZ!SmartHome thermostate devices.""" """Support for AVM FRITZ!SmartHome thermostate devices."""
from __future__ import annotations
from typing import Any
from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate import ClimateEntity
from homeassistant.components.climate.const import ( from homeassistant.components.climate.const import (
ATTR_HVAC_MODE, ATTR_HVAC_MODE,
@ -34,6 +38,7 @@ from .const import (
CONF_COORDINATOR, CONF_COORDINATOR,
DOMAIN as FRITZBOX_DOMAIN, DOMAIN as FRITZBOX_DOMAIN,
) )
from .model import ClimateExtraAttributes
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE
@ -55,7 +60,7 @@ async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None: ) -> None:
"""Set up the FRITZ!SmartHome thermostat from ConfigEntry.""" """Set up the FRITZ!SmartHome thermostat from ConfigEntry."""
entities = [] entities: list[FritzboxThermostat] = []
coordinator = hass.data[FRITZBOX_DOMAIN][entry.entry_id][CONF_COORDINATOR] coordinator = hass.data[FRITZBOX_DOMAIN][entry.entry_id][CONF_COORDINATOR]
for ain, device in coordinator.data.items(): for ain, device in coordinator.data.items():
@ -82,53 +87,53 @@ class FritzboxThermostat(FritzBoxEntity, ClimateEntity):
"""The thermostat class for FRITZ!SmartHome thermostates.""" """The thermostat class for FRITZ!SmartHome thermostates."""
@property @property
def supported_features(self): def supported_features(self) -> int:
"""Return the list of supported features.""" """Return the list of supported features."""
return SUPPORT_FLAGS return SUPPORT_FLAGS
@property @property
def available(self): def available(self) -> bool:
"""Return if thermostat is available.""" """Return if thermostat is available."""
return self.device.present return self.device.present # type: ignore [no-any-return]
@property @property
def temperature_unit(self): def temperature_unit(self) -> str:
"""Return the unit of measurement that is used.""" """Return the unit of measurement that is used."""
return TEMP_CELSIUS return TEMP_CELSIUS
@property @property
def precision(self): def precision(self) -> float:
"""Return precision 0.5.""" """Return precision 0.5."""
return PRECISION_HALVES return PRECISION_HALVES
@property @property
def current_temperature(self): def current_temperature(self) -> float:
"""Return the current temperature.""" """Return the current temperature."""
return self.device.actual_temperature return self.device.actual_temperature # type: ignore [no-any-return]
@property @property
def target_temperature(self): def target_temperature(self) -> float:
"""Return the temperature we try to reach.""" """Return the temperature we try to reach."""
if self.device.target_temperature == ON_API_TEMPERATURE: if self.device.target_temperature == ON_API_TEMPERATURE:
return ON_REPORT_SET_TEMPERATURE return ON_REPORT_SET_TEMPERATURE
if self.device.target_temperature == OFF_API_TEMPERATURE: if self.device.target_temperature == OFF_API_TEMPERATURE:
return OFF_REPORT_SET_TEMPERATURE return OFF_REPORT_SET_TEMPERATURE
return self.device.target_temperature return self.device.target_temperature # type: ignore [no-any-return]
async def async_set_temperature(self, **kwargs): async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperature.""" """Set new target temperature."""
if ATTR_HVAC_MODE in kwargs: if kwargs.get(ATTR_HVAC_MODE) is not None:
hvac_mode = kwargs.get(ATTR_HVAC_MODE) hvac_mode = kwargs[ATTR_HVAC_MODE]
await self.async_set_hvac_mode(hvac_mode) await self.async_set_hvac_mode(hvac_mode)
elif ATTR_TEMPERATURE in kwargs: elif kwargs.get(ATTR_TEMPERATURE) is not None:
temperature = kwargs.get(ATTR_TEMPERATURE) temperature = kwargs[ATTR_TEMPERATURE]
await self.hass.async_add_executor_job( await self.hass.async_add_executor_job(
self.device.set_target_temperature, temperature self.device.set_target_temperature, temperature
) )
await self.coordinator.async_refresh() await self.coordinator.async_refresh()
@property @property
def hvac_mode(self): def hvac_mode(self) -> str:
"""Return the current operation mode.""" """Return the current operation mode."""
if ( if (
self.device.target_temperature == OFF_REPORT_SET_TEMPERATURE self.device.target_temperature == OFF_REPORT_SET_TEMPERATURE
@ -139,11 +144,11 @@ class FritzboxThermostat(FritzBoxEntity, ClimateEntity):
return HVAC_MODE_HEAT return HVAC_MODE_HEAT
@property @property
def hvac_modes(self): def hvac_modes(self) -> list[str]:
"""Return the list of available operation modes.""" """Return the list of available operation modes."""
return OPERATION_LIST return OPERATION_LIST
async def async_set_hvac_mode(self, hvac_mode): async def async_set_hvac_mode(self, hvac_mode: str) -> None:
"""Set new operation mode.""" """Set new operation mode."""
if hvac_mode == HVAC_MODE_OFF: if hvac_mode == HVAC_MODE_OFF:
await self.async_set_temperature(temperature=OFF_REPORT_SET_TEMPERATURE) await self.async_set_temperature(temperature=OFF_REPORT_SET_TEMPERATURE)
@ -153,7 +158,7 @@ class FritzboxThermostat(FritzBoxEntity, ClimateEntity):
) )
@property @property
def preset_mode(self): def preset_mode(self) -> str | None:
"""Return current preset mode.""" """Return current preset mode."""
if self.device.target_temperature == self.device.comfort_temperature: if self.device.target_temperature == self.device.comfort_temperature:
return PRESET_COMFORT return PRESET_COMFORT
@ -162,11 +167,11 @@ class FritzboxThermostat(FritzBoxEntity, ClimateEntity):
return None return None
@property @property
def preset_modes(self): def preset_modes(self) -> list[str]:
"""Return supported preset modes.""" """Return supported preset modes."""
return [PRESET_ECO, PRESET_COMFORT] return [PRESET_ECO, PRESET_COMFORT]
async def async_set_preset_mode(self, preset_mode): async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set preset mode.""" """Set preset mode."""
if preset_mode == PRESET_COMFORT: if preset_mode == PRESET_COMFORT:
await self.async_set_temperature( await self.async_set_temperature(
@ -176,19 +181,19 @@ class FritzboxThermostat(FritzBoxEntity, ClimateEntity):
await self.async_set_temperature(temperature=self.device.eco_temperature) await self.async_set_temperature(temperature=self.device.eco_temperature)
@property @property
def min_temp(self): def min_temp(self) -> int:
"""Return the minimum temperature.""" """Return the minimum temperature."""
return MIN_TEMPERATURE return MIN_TEMPERATURE
@property @property
def max_temp(self): def max_temp(self) -> int:
"""Return the maximum temperature.""" """Return the maximum temperature."""
return MAX_TEMPERATURE return MAX_TEMPERATURE
@property @property
def extra_state_attributes(self): def extra_state_attributes(self) -> ClimateExtraAttributes:
"""Return the device specific state attributes.""" """Return the device specific state attributes."""
attrs = { attrs: ClimateExtraAttributes = {
ATTR_STATE_BATTERY_LOW: self.device.battery_low, ATTR_STATE_BATTERY_LOW: self.device.battery_low,
ATTR_STATE_DEVICE_LOCKED: self.device.device_lock, ATTR_STATE_DEVICE_LOCKED: self.device.device_lock,
ATTR_STATE_LOCKED: self.device.lock, ATTR_STATE_LOCKED: self.device.lock,

View File

@ -1,17 +1,22 @@
"""Config flow for AVM FRITZ!SmartHome.""" """Config flow for AVM FRITZ!SmartHome."""
from __future__ import annotations
from typing import Any
from urllib.parse import urlparse from urllib.parse import urlparse
from pyfritzhome import Fritzhome, LoginError from pyfritzhome import Fritzhome, LoginError
from requests.exceptions import HTTPError from requests.exceptions import HTTPError
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries
from homeassistant.components.ssdp import ( from homeassistant.components.ssdp import (
ATTR_SSDP_LOCATION, ATTR_SSDP_LOCATION,
ATTR_UPNP_FRIENDLY_NAME, ATTR_UPNP_FRIENDLY_NAME,
ATTR_UPNP_UDN, ATTR_UPNP_UDN,
) )
from homeassistant.config_entries import ConfigEntry, ConfigFlow
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.typing import DiscoveryInfoType
from .const import DEFAULT_HOST, DEFAULT_USERNAME, DOMAIN from .const import DEFAULT_HOST, DEFAULT_USERNAME, DOMAIN
@ -36,22 +41,22 @@ RESULT_NOT_SUPPORTED = "not_supported"
RESULT_SUCCESS = "success" RESULT_SUCCESS = "success"
class FritzboxConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): class FritzboxConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a AVM FRITZ!SmartHome config flow.""" """Handle a AVM FRITZ!SmartHome config flow."""
VERSION = 1 VERSION = 1
def __init__(self): def __init__(self) -> None:
"""Initialize flow.""" """Initialize flow."""
self._entry = None self._entry: ConfigEntry | None = None
self._host = None self._host: str | None = None
self._name = None self._name: str | None = None
self._password = None self._password: str | None = None
self._username = None self._username: str | None = None
def _get_entry(self): def _get_entry(self, name: str) -> FlowResult:
return self.async_create_entry( return self.async_create_entry(
title=self._name, title=name,
data={ data={
CONF_HOST: self._host, CONF_HOST: self._host,
CONF_PASSWORD: self._password, CONF_PASSWORD: self._password,
@ -59,7 +64,8 @@ class FritzboxConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
}, },
) )
async def _update_entry(self): async def _update_entry(self) -> None:
assert self._entry is not None
self.hass.config_entries.async_update_entry( self.hass.config_entries.async_update_entry(
self._entry, self._entry,
data={ data={
@ -70,7 +76,7 @@ class FritzboxConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
) )
await self.hass.config_entries.async_reload(self._entry.entry_id) await self.hass.config_entries.async_reload(self._entry.entry_id)
def _try_connect(self): def _try_connect(self) -> str:
"""Try to connect and check auth.""" """Try to connect and check auth."""
fritzbox = Fritzhome( fritzbox = Fritzhome(
host=self._host, user=self._username, password=self._password host=self._host, user=self._username, password=self._password
@ -87,7 +93,9 @@ class FritzboxConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
except OSError: except OSError:
return RESULT_NO_DEVICES_FOUND return RESULT_NO_DEVICES_FOUND
async def async_step_user(self, user_input=None): async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle a flow initialized by the user.""" """Handle a flow initialized by the user."""
errors = {} errors = {}
@ -95,14 +103,14 @@ class FritzboxConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]}) self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]})
self._host = user_input[CONF_HOST] self._host = user_input[CONF_HOST]
self._name = user_input[CONF_HOST] self._name = str(user_input[CONF_HOST])
self._password = user_input[CONF_PASSWORD] self._password = user_input[CONF_PASSWORD]
self._username = user_input[CONF_USERNAME] self._username = user_input[CONF_USERNAME]
result = await self.hass.async_add_executor_job(self._try_connect) result = await self.hass.async_add_executor_job(self._try_connect)
if result == RESULT_SUCCESS: if result == RESULT_SUCCESS:
return self._get_entry() return self._get_entry(self._name)
if result != RESULT_INVALID_AUTH: if result != RESULT_INVALID_AUTH:
return self.async_abort(reason=result) return self.async_abort(reason=result)
errors["base"] = result errors["base"] = result
@ -111,9 +119,10 @@ class FritzboxConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
step_id="user", data_schema=DATA_SCHEMA_USER, errors=errors step_id="user", data_schema=DATA_SCHEMA_USER, errors=errors
) )
async def async_step_ssdp(self, discovery_info): async def async_step_ssdp(self, discovery_info: DiscoveryInfoType) -> FlowResult:
"""Handle a flow initialized by discovery.""" """Handle a flow initialized by discovery."""
host = urlparse(discovery_info[ATTR_SSDP_LOCATION]).hostname host = urlparse(discovery_info[ATTR_SSDP_LOCATION]).hostname
assert isinstance(host, str)
self.context[CONF_HOST] = host self.context[CONF_HOST] = host
uuid = discovery_info.get(ATTR_UPNP_UDN) uuid = discovery_info.get(ATTR_UPNP_UDN)
@ -135,12 +144,14 @@ class FritzboxConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
return self.async_abort(reason="already_configured") return self.async_abort(reason="already_configured")
self._host = host self._host = host
self._name = discovery_info.get(ATTR_UPNP_FRIENDLY_NAME) or host self._name = str(discovery_info.get(ATTR_UPNP_FRIENDLY_NAME) or host)
self.context["title_placeholders"] = {"name": self._name} self.context["title_placeholders"] = {"name": self._name}
return await self.async_step_confirm() return await self.async_step_confirm()
async def async_step_confirm(self, user_input=None): async def async_step_confirm(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle user-confirmation of discovered node.""" """Handle user-confirmation of discovered node."""
errors = {} errors = {}
@ -150,7 +161,8 @@ class FritzboxConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
result = await self.hass.async_add_executor_job(self._try_connect) result = await self.hass.async_add_executor_job(self._try_connect)
if result == RESULT_SUCCESS: if result == RESULT_SUCCESS:
return self._get_entry() assert self._name is not None
return self._get_entry(self._name)
if result != RESULT_INVALID_AUTH: if result != RESULT_INVALID_AUTH:
return self.async_abort(reason=result) return self.async_abort(reason=result)
errors["base"] = result errors["base"] = result
@ -162,16 +174,20 @@ class FritzboxConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
errors=errors, errors=errors,
) )
async def async_step_reauth(self, data): async def async_step_reauth(self, data: dict[str, str]) -> FlowResult:
"""Trigger a reauthentication flow.""" """Trigger a reauthentication flow."""
self._entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) entry = self.hass.config_entries.async_get_entry(self.context["entry_id"])
assert entry is not None
self._entry = entry
self._host = data[CONF_HOST] self._host = data[CONF_HOST]
self._name = data[CONF_HOST] self._name = str(data[CONF_HOST])
self._username = data[CONF_USERNAME] self._username = data[CONF_USERNAME]
return await self.async_step_reauth_confirm() return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(self, user_input=None): async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle reauthorization flow.""" """Handle reauthorization flow."""
errors = {} errors = {}

View File

@ -1,26 +1,29 @@
"""Constants for the AVM FRITZ!SmartHome integration.""" """Constants for the AVM FRITZ!SmartHome integration."""
from __future__ import annotations
import logging import logging
from typing import Final
ATTR_STATE_BATTERY_LOW = "battery_low" ATTR_STATE_BATTERY_LOW: Final = "battery_low"
ATTR_STATE_DEVICE_LOCKED = "device_locked" ATTR_STATE_DEVICE_LOCKED: Final = "device_locked"
ATTR_STATE_HOLIDAY_MODE = "holiday_mode" ATTR_STATE_HOLIDAY_MODE: Final = "holiday_mode"
ATTR_STATE_LOCKED = "locked" ATTR_STATE_LOCKED: Final = "locked"
ATTR_STATE_SUMMER_MODE = "summer_mode" ATTR_STATE_SUMMER_MODE: Final = "summer_mode"
ATTR_STATE_WINDOW_OPEN = "window_open" ATTR_STATE_WINDOW_OPEN: Final = "window_open"
ATTR_TEMPERATURE_UNIT = "temperature_unit" ATTR_TEMPERATURE_UNIT: Final = "temperature_unit"
ATTR_TOTAL_CONSUMPTION = "total_consumption" ATTR_TOTAL_CONSUMPTION: Final = "total_consumption"
ATTR_TOTAL_CONSUMPTION_UNIT = "total_consumption_unit" ATTR_TOTAL_CONSUMPTION_UNIT: Final = "total_consumption_unit"
CONF_CONNECTIONS = "connections" CONF_CONNECTIONS: Final = "connections"
CONF_COORDINATOR = "coordinator" CONF_COORDINATOR: Final = "coordinator"
DEFAULT_HOST = "fritz.box" DEFAULT_HOST: Final = "fritz.box"
DEFAULT_USERNAME = "admin" DEFAULT_USERNAME: Final = "admin"
DOMAIN = "fritzbox" DOMAIN: Final = "fritzbox"
LOGGER: logging.Logger = logging.getLogger(__package__) LOGGER: Final[logging.Logger] = logging.getLogger(__package__)
PLATFORMS = ["binary_sensor", "climate", "switch", "sensor"] PLATFORMS: Final[list[str]] = ["binary_sensor", "climate", "switch", "sensor"]

View File

@ -0,0 +1,43 @@
"""Models for the AVM FRITZ!SmartHome integration."""
from __future__ import annotations
from typing import TypedDict
class EntityInfo(TypedDict):
"""TypedDict for EntityInfo."""
name: str
entity_id: str
unit_of_measurement: str | None
device_class: str | None
class ClimateExtraAttributes(TypedDict, total=False):
"""TypedDict for climates extra attributes."""
battery_low: bool
device_locked: bool
locked: bool
battery_level: int
holiday_mode: bool
summer_mode: bool
window_open: bool
class SensorExtraAttributes(TypedDict):
"""TypedDict for sensors extra attributes."""
device_locked: bool
locked: bool
class SwitchExtraAttributes(TypedDict, total=False):
"""TypedDict for sensors extra attributes."""
device_locked: bool
locked: bool
total_consumption: str
total_consumption_unit: str
temperature: str
temperature_unit: str

View File

@ -1,4 +1,6 @@
"""Support for AVM FRITZ!SmartHome temperature sensor only devices.""" """Support for AVM FRITZ!SmartHome temperature sensor only devices."""
from __future__ import annotations
from homeassistant.components.sensor import SensorEntity from homeassistant.components.sensor import SensorEntity
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
@ -20,13 +22,14 @@ from .const import (
CONF_COORDINATOR, CONF_COORDINATOR,
DOMAIN as FRITZBOX_DOMAIN, DOMAIN as FRITZBOX_DOMAIN,
) )
from .model import SensorExtraAttributes
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None: ) -> None:
"""Set up the FRITZ!SmartHome sensor from ConfigEntry.""" """Set up the FRITZ!SmartHome sensor from ConfigEntry."""
entities = [] entities: list[FritzBoxEntity] = []
coordinator = hass.data[FRITZBOX_DOMAIN][entry.entry_id][CONF_COORDINATOR] coordinator = hass.data[FRITZBOX_DOMAIN][entry.entry_id][CONF_COORDINATOR]
for ain, device in coordinator.data.items(): for ain, device in coordinator.data.items():
@ -69,23 +72,23 @@ class FritzBoxBatterySensor(FritzBoxEntity, SensorEntity):
"""The entity class for FRITZ!SmartHome sensors.""" """The entity class for FRITZ!SmartHome sensors."""
@property @property
def state(self): def state(self) -> int | None:
"""Return the state of the sensor.""" """Return the state of the sensor."""
return self.device.battery_level return self.device.battery_level # type: ignore [no-any-return]
class FritzBoxTempSensor(FritzBoxEntity, SensorEntity): class FritzBoxTempSensor(FritzBoxEntity, SensorEntity):
"""The entity class for FRITZ!SmartHome temperature sensors.""" """The entity class for FRITZ!SmartHome temperature sensors."""
@property @property
def state(self): def state(self) -> float | None:
"""Return the state of the sensor.""" """Return the state of the sensor."""
return self.device.temperature return self.device.temperature # type: ignore [no-any-return]
@property @property
def extra_state_attributes(self): def extra_state_attributes(self) -> SensorExtraAttributes:
"""Return the state attributes of the device.""" """Return the state attributes of the device."""
attrs = { attrs: SensorExtraAttributes = {
ATTR_STATE_DEVICE_LOCKED: self.device.device_lock, ATTR_STATE_DEVICE_LOCKED: self.device.device_lock,
ATTR_STATE_LOCKED: self.device.lock, ATTR_STATE_LOCKED: self.device.lock,
} }

View File

@ -1,4 +1,8 @@
"""Support for AVM FRITZ!SmartHome switch devices.""" """Support for AVM FRITZ!SmartHome switch devices."""
from __future__ import annotations
from typing import Any
from homeassistant.components.switch import SwitchEntity from homeassistant.components.switch import SwitchEntity
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
@ -23,6 +27,7 @@ from .const import (
CONF_COORDINATOR, CONF_COORDINATOR,
DOMAIN as FRITZBOX_DOMAIN, DOMAIN as FRITZBOX_DOMAIN,
) )
from .model import SwitchExtraAttributes
ATTR_TOTAL_CONSUMPTION_UNIT_VALUE = ENERGY_KILO_WATT_HOUR ATTR_TOTAL_CONSUMPTION_UNIT_VALUE = ENERGY_KILO_WATT_HOUR
@ -31,7 +36,7 @@ async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None: ) -> None:
"""Set up the FRITZ!SmartHome switch from ConfigEntry.""" """Set up the FRITZ!SmartHome switch from ConfigEntry."""
entities = [] entities: list[FritzboxSwitch] = []
coordinator = hass.data[FRITZBOX_DOMAIN][entry.entry_id][CONF_COORDINATOR] coordinator = hass.data[FRITZBOX_DOMAIN][entry.entry_id][CONF_COORDINATOR]
for ain, device in coordinator.data.items(): for ain, device in coordinator.data.items():
@ -58,31 +63,32 @@ class FritzboxSwitch(FritzBoxEntity, SwitchEntity):
"""The switch class for FRITZ!SmartHome switches.""" """The switch class for FRITZ!SmartHome switches."""
@property @property
def available(self): def available(self) -> bool:
"""Return if switch is available.""" """Return if switch is available."""
return self.device.present return self.device.present # type: ignore [no-any-return]
@property @property
def is_on(self): def is_on(self) -> bool:
"""Return true if the switch is on.""" """Return true if the switch is on."""
return self.device.switch_state return self.device.switch_state # type: ignore [no-any-return]
async def async_turn_on(self, **kwargs): async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the switch on.""" """Turn the switch on."""
await self.hass.async_add_executor_job(self.device.set_switch_state_on) await self.hass.async_add_executor_job(self.device.set_switch_state_on)
await self.coordinator.async_refresh() await self.coordinator.async_refresh()
async def async_turn_off(self, **kwargs): async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the switch off.""" """Turn the switch off."""
await self.hass.async_add_executor_job(self.device.set_switch_state_off) await self.hass.async_add_executor_job(self.device.set_switch_state_off)
await self.coordinator.async_refresh() await self.coordinator.async_refresh()
@property @property
def extra_state_attributes(self): def extra_state_attributes(self) -> SwitchExtraAttributes:
"""Return the state attributes of the device.""" """Return the state attributes of the device."""
attrs = {} attrs: SwitchExtraAttributes = {
attrs[ATTR_STATE_DEVICE_LOCKED] = self.device.device_lock ATTR_STATE_DEVICE_LOCKED: self.device.device_lock,
attrs[ATTR_STATE_LOCKED] = self.device.lock ATTR_STATE_LOCKED: self.device.lock,
}
if self.device.has_powermeter: if self.device.has_powermeter:
attrs[ attrs[
@ -99,6 +105,6 @@ class FritzboxSwitch(FritzBoxEntity, SwitchEntity):
return attrs return attrs
@property @property
def current_power_w(self): def current_power_w(self) -> float:
"""Return the current power usage in W.""" """Return the current power usage in W."""
return self.device.power / 1000 return self.device.power / 1000 # type: ignore [no-any-return]

View File

@ -294,7 +294,7 @@ ATTR_ID = "id"
ATTR_NAME: Final = "name" ATTR_NAME: Final = "name"
# Contains one string or a list of strings, each being an entity id # Contains one string or a list of strings, each being an entity id
ATTR_ENTITY_ID = "entity_id" ATTR_ENTITY_ID: Final = "entity_id"
# Contains one string or a list of strings, each being an area id # Contains one string or a list of strings, each being an area id
ATTR_AREA_ID = "area_id" ATTR_AREA_ID = "area_id"
@ -314,7 +314,7 @@ ATTR_IDENTIFIERS: Final = "identifiers"
ATTR_ICON = "icon" ATTR_ICON = "icon"
# The unit of measurement if applicable # The unit of measurement if applicable
ATTR_UNIT_OF_MEASUREMENT = "unit_of_measurement" ATTR_UNIT_OF_MEASUREMENT: Final = "unit_of_measurement"
CONF_UNIT_SYSTEM_METRIC: str = "metric" CONF_UNIT_SYSTEM_METRIC: str = "metric"
CONF_UNIT_SYSTEM_IMPERIAL: str = "imperial" CONF_UNIT_SYSTEM_IMPERIAL: str = "imperial"
@ -332,7 +332,7 @@ ATTR_MODEL: Final = "model"
ATTR_SW_VERSION: Final = "sw_version" ATTR_SW_VERSION: Final = "sw_version"
ATTR_BATTERY_CHARGING = "battery_charging" ATTR_BATTERY_CHARGING = "battery_charging"
ATTR_BATTERY_LEVEL = "battery_level" ATTR_BATTERY_LEVEL: Final = "battery_level"
ATTR_WAKEUP = "wake_up_interval" ATTR_WAKEUP = "wake_up_interval"
# For devices which support a code attribute # For devices which support a code attribute
@ -379,10 +379,10 @@ ATTR_RESTORED = "restored"
ATTR_SUPPORTED_FEATURES = "supported_features" ATTR_SUPPORTED_FEATURES = "supported_features"
# Class of device within its domain # Class of device within its domain
ATTR_DEVICE_CLASS = "device_class" ATTR_DEVICE_CLASS: Final = "device_class"
# Temperature attribute # Temperature attribute
ATTR_TEMPERATURE = "temperature" ATTR_TEMPERATURE: Final = "temperature"
# #### UNITS OF MEASUREMENT #### # #### UNITS OF MEASUREMENT ####
# Power units # Power units

View File

@ -154,6 +154,17 @@ no_implicit_optional = true
warn_return_any = true warn_return_any = true
warn_unreachable = true warn_unreachable = true
[mypy-homeassistant.components.fritzbox.*]
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.frontend.*] [mypy-homeassistant.components.frontend.*]
check_untyped_defs = true check_untyped_defs = true
disallow_incomplete_defs = true disallow_incomplete_defs = true
@ -781,9 +792,6 @@ ignore_errors = true
[mypy-homeassistant.components.freebox.*] [mypy-homeassistant.components.freebox.*]
ignore_errors = true ignore_errors = true
[mypy-homeassistant.components.fritzbox.*]
ignore_errors = true
[mypy-homeassistant.components.garmin_connect.*] [mypy-homeassistant.components.garmin_connect.*]
ignore_errors = true ignore_errors = true

View File

@ -69,7 +69,6 @@ IGNORED_MODULES: Final[list[str]] = [
"homeassistant.components.fortios.*", "homeassistant.components.fortios.*",
"homeassistant.components.foscam.*", "homeassistant.components.foscam.*",
"homeassistant.components.freebox.*", "homeassistant.components.freebox.*",
"homeassistant.components.fritzbox.*",
"homeassistant.components.garmin_connect.*", "homeassistant.components.garmin_connect.*",
"homeassistant.components.geniushub.*", "homeassistant.components.geniushub.*",
"homeassistant.components.glances.*", "homeassistant.components.glances.*",