Activate mypy for Vallox (#55874)

This commit is contained in:
Andre Richter 2021-09-23 19:59:28 +02:00 committed by GitHub
parent 2fe8c78811
commit a5c6a65161
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 151 additions and 95 deletions

View File

@ -113,6 +113,7 @@ homeassistant.components.upcloud.*
homeassistant.components.uptime.* homeassistant.components.uptime.*
homeassistant.components.uptimerobot.* homeassistant.components.uptimerobot.*
homeassistant.components.vacuum.* homeassistant.components.vacuum.*
homeassistant.components.vallox.*
homeassistant.components.water_heater.* homeassistant.components.water_heater.*
homeassistant.components.weather.* homeassistant.components.weather.*
homeassistant.components.websocket_api.* homeassistant.components.websocket_api.*

View File

@ -1,7 +1,10 @@
"""Support for Vallox ventilation units.""" """Support for Vallox ventilation units."""
from __future__ import annotations
from datetime import datetime
import ipaddress import ipaddress
import logging import logging
from typing import Any
from vallox_websocket_api import PROFILE as VALLOX_PROFILE, Vallox from vallox_websocket_api import PROFILE as VALLOX_PROFILE, Vallox
from vallox_websocket_api.constants import vlxDevConstants from vallox_websocket_api.constants import vlxDevConstants
@ -9,10 +12,12 @@ from vallox_websocket_api.exceptions import ValloxApiException
import voluptuous as vol import voluptuous as vol
from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.const import CONF_HOST, CONF_NAME
import homeassistant.helpers.config_validation as cv from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.discovery import async_load_platform
from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.typing import ConfigType, StateType
from .const import ( from .const import (
DEFAULT_FAN_SPEED_AWAY, DEFAULT_FAN_SPEED_AWAY,
@ -95,7 +100,7 @@ SERVICE_TO_METHOD = {
} }
async def async_setup(hass, config): async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the client and boot the platforms.""" """Set up the client and boot the platforms."""
conf = config[DOMAIN] conf = config[DOMAIN]
host = conf.get(CONF_HOST) host = conf.get(CONF_HOST)
@ -113,13 +118,11 @@ async def async_setup(hass, config):
DOMAIN, vallox_service, service_handler.async_handle, schema=schema DOMAIN, vallox_service, service_handler.async_handle, schema=schema
) )
# The vallox hardware expects quite strict timings for websocket # The vallox hardware expects quite strict timings for websocket requests. Timings that machines
# requests. Timings that machines with less processing power, like # with less processing power, like Raspberries, cannot live up to during the busy start phase of
# Raspberries, cannot live up to during the busy start phase of Home # Home Asssistant. Hence, async_add_entities() for fan and sensor in respective code will be
# Asssistant. Hence, async_add_entities() for fan and sensor in respective # called with update_before_add=False to intentionally delay the first request, increasing
# code will be called with update_before_add=False to intentionally delay # chance that it is issued only when the machine is less busy again.
# the first request, increasing chance that it is issued only when the
# machine is less busy again.
hass.async_create_task(async_load_platform(hass, "sensor", DOMAIN, {}, config)) hass.async_create_task(async_load_platform(hass, "sensor", DOMAIN, {}, config))
hass.async_create_task(async_load_platform(hass, "fan", DOMAIN, {}, config)) hass.async_create_task(async_load_platform(hass, "fan", DOMAIN, {}, config))
@ -131,15 +134,15 @@ async def async_setup(hass, config):
class ValloxStateProxy: class ValloxStateProxy:
"""Helper class to reduce websocket API calls.""" """Helper class to reduce websocket API calls."""
def __init__(self, hass, client): def __init__(self, hass: HomeAssistant, client: Vallox) -> None:
"""Initialize the proxy.""" """Initialize the proxy."""
self._hass = hass self._hass = hass
self._client = client self._client = client
self._metric_cache = {} self._metric_cache: dict[str, Any] = {}
self._profile = None self._profile = VALLOX_PROFILE.NONE
self._valid = False self._valid = False
def fetch_metric(self, metric_key): def fetch_metric(self, metric_key: str) -> StateType:
"""Return cached state value.""" """Return cached state value."""
_LOGGER.debug("Fetching metric key: %s", metric_key) _LOGGER.debug("Fetching metric key: %s", metric_key)
@ -149,9 +152,18 @@ class ValloxStateProxy:
if metric_key not in vlxDevConstants.__dict__: if metric_key not in vlxDevConstants.__dict__:
raise KeyError(f"Unknown metric key: {metric_key}") raise KeyError(f"Unknown metric key: {metric_key}")
return self._metric_cache[metric_key] value = self._metric_cache[metric_key]
if value is None:
return None
def get_profile(self): if not isinstance(value, (str, int, float)):
raise TypeError(
f"Return value of metric {metric_key} has unexpected type {type(value)}"
)
return value
def get_profile(self) -> str:
"""Return cached profile value.""" """Return cached profile value."""
_LOGGER.debug("Returning profile") _LOGGER.debug("Returning profile")
@ -160,7 +172,7 @@ class ValloxStateProxy:
return PROFILE_TO_STR_REPORTABLE[self._profile] return PROFILE_TO_STR_REPORTABLE[self._profile]
async def async_update(self, event_time): async def async_update(self, time: datetime | None = None) -> None:
"""Fetch state update.""" """Fetch state update."""
_LOGGER.debug("Updating Vallox state cache") _LOGGER.debug("Updating Vallox state cache")
@ -180,7 +192,7 @@ class ValloxStateProxy:
class ValloxServiceHandler: class ValloxServiceHandler:
"""Services implementation.""" """Services implementation."""
def __init__(self, client, state_proxy): def __init__(self, client: Vallox, state_proxy: ValloxStateProxy) -> None:
"""Initialize the proxy.""" """Initialize the proxy."""
self._client = client self._client = client
self._state_proxy = state_proxy self._state_proxy = state_proxy
@ -245,10 +257,13 @@ class ValloxServiceHandler:
_LOGGER.error("Error setting fan speed for Boost profile: %s", err) _LOGGER.error("Error setting fan speed for Boost profile: %s", err)
return False return False
async def async_handle(self, service): async def async_handle(self, call: ServiceCall) -> None:
"""Dispatch a service call.""" """Dispatch a service call."""
method = SERVICE_TO_METHOD.get(service.service) method = SERVICE_TO_METHOD.get(call.service)
params = service.data.copy() params = call.data.copy()
if method is None:
return
if not hasattr(self, method["method"]): if not hasattr(self, method["method"]):
_LOGGER.error("Service not implemented: %s", method["method"]) _LOGGER.error("Service not implemented: %s", method["method"])
@ -256,7 +271,6 @@ class ValloxServiceHandler:
result = await getattr(self, method["method"])(**params) result = await getattr(self, method["method"])(**params)
# Force state_proxy to refresh device state, so that updates are # Force state_proxy to refresh device state, so that updates are propagated to platforms.
# propagated to platforms.
if result: if result:
await self._state_proxy.async_update(None) await self._state_proxy.async_update()

View File

@ -1,11 +1,19 @@
"""Support for the Vallox ventilation unit fan.""" """Support for the Vallox ventilation unit fan."""
from __future__ import annotations
from collections.abc import Mapping
import logging import logging
from typing import Any
from vallox_websocket_api import Vallox
from homeassistant.components.fan import FanEntity from homeassistant.components.fan import FanEntity
from homeassistant.core import callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import ValloxStateProxy
from .const import ( from .const import (
DOMAIN, DOMAIN,
METRIC_KEY_MODE, METRIC_KEY_MODE,
@ -34,13 +42,17 @@ ATTR_PROFILE_FAN_SPEED_BOOST = {
} }
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): async def async_setup_platform(
hass: HomeAssistant,
config: ConfigType,
async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the fan device.""" """Set up the fan device."""
if discovery_info is None: if discovery_info is None:
return return
client = hass.data[DOMAIN]["client"] client = hass.data[DOMAIN]["client"]
client.set_settable_address(METRIC_KEY_MODE, int) client.set_settable_address(METRIC_KEY_MODE, int)
device = ValloxFan( device = ValloxFan(
@ -53,39 +65,41 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
class ValloxFan(FanEntity): class ValloxFan(FanEntity):
"""Representation of the fan.""" """Representation of the fan."""
def __init__(self, name, client, state_proxy): def __init__(
self, name: str, client: Vallox, state_proxy: ValloxStateProxy
) -> None:
"""Initialize the fan.""" """Initialize the fan."""
self._name = name self._name = name
self._client = client self._client = client
self._state_proxy = state_proxy self._state_proxy = state_proxy
self._available = False self._available = False
self._state = None self._is_on = False
self._fan_speed_home = None self._fan_speed_home: int | None = None
self._fan_speed_away = None self._fan_speed_away: int | None = None
self._fan_speed_boost = None self._fan_speed_boost: int | None = None
@property @property
def should_poll(self): def should_poll(self) -> bool:
"""Do not poll the device.""" """Do not poll the device."""
return False return False
@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 available(self): def available(self) -> bool:
"""Return if state is known.""" """Return if state is known."""
return self._available return self._available
@property @property
def is_on(self): def is_on(self) -> bool:
"""Return if device is on.""" """Return if device is on."""
return self._state return self._is_on
@property @property
def extra_state_attributes(self): def extra_state_attributes(self) -> Mapping[str, int | None]:
"""Return device specific state attributes.""" """Return device specific state attributes."""
return { return {
ATTR_PROFILE_FAN_SPEED_HOME["description"]: self._fan_speed_home, ATTR_PROFILE_FAN_SPEED_HOME["description"]: self._fan_speed_home,
@ -93,7 +107,7 @@ class ValloxFan(FanEntity):
ATTR_PROFILE_FAN_SPEED_BOOST["description"]: self._fan_speed_boost, ATTR_PROFILE_FAN_SPEED_BOOST["description"]: self._fan_speed_boost,
} }
async def async_added_to_hass(self): async def async_added_to_hass(self) -> None:
"""Call to update.""" """Call to update."""
self.async_on_remove( self.async_on_remove(
async_dispatcher_connect( async_dispatcher_connect(
@ -102,38 +116,42 @@ class ValloxFan(FanEntity):
) )
@callback @callback
def _update_callback(self): def _update_callback(self) -> None:
"""Call update method.""" """Call update method."""
self.async_schedule_update_ha_state(True) self.async_schedule_update_ha_state(True)
async def async_update(self): async def async_update(self) -> None:
"""Fetch state from the device.""" """Fetch state from the device."""
try: try:
# Fetch if the whole device is in regular operation state. # Fetch if the whole device is in regular operation state.
self._state = self._state_proxy.fetch_metric(METRIC_KEY_MODE) == MODE_ON self._is_on = self._state_proxy.fetch_metric(METRIC_KEY_MODE) == MODE_ON
# Fetch the profile fan speeds. # Fetch the profile fan speeds.
self._fan_speed_home = int( fan_speed_home = self._state_proxy.fetch_metric(
self._state_proxy.fetch_metric( ATTR_PROFILE_FAN_SPEED_HOME["metric_key"]
ATTR_PROFILE_FAN_SPEED_HOME["metric_key"]
)
) )
self._fan_speed_away = int( fan_speed_away = self._state_proxy.fetch_metric(
self._state_proxy.fetch_metric( ATTR_PROFILE_FAN_SPEED_AWAY["metric_key"]
ATTR_PROFILE_FAN_SPEED_AWAY["metric_key"]
)
) )
self._fan_speed_boost = int( fan_speed_boost = self._state_proxy.fetch_metric(
self._state_proxy.fetch_metric( ATTR_PROFILE_FAN_SPEED_BOOST["metric_key"]
ATTR_PROFILE_FAN_SPEED_BOOST["metric_key"]
)
) )
except (OSError, KeyError) as err: except (OSError, KeyError, TypeError) as err:
self._available = False self._available = False
_LOGGER.error("Error updating fan: %s", err) _LOGGER.error("Error updating fan: %s", err)
return return
self._fan_speed_home = (
int(fan_speed_home) if isinstance(fan_speed_home, (int, float)) else None
)
self._fan_speed_away = (
int(fan_speed_away) if isinstance(fan_speed_away, (int, float)) else None
)
self._fan_speed_boost = (
int(fan_speed_boost) if isinstance(fan_speed_boost, (int, float)) else None
)
self._available = True self._available = True
# #
@ -145,20 +163,19 @@ class ValloxFan(FanEntity):
# #
async def async_turn_on( async def async_turn_on(
self, self,
speed: str = None, speed: str | None = None,
percentage: int = None, percentage: int | None = None,
preset_mode: str = None, preset_mode: str | None = None,
**kwargs, **kwargs: Any,
) -> None: ) -> None:
"""Turn the device on.""" """Turn the device on."""
_LOGGER.debug("Turn on: %s", speed) _LOGGER.debug("Turn on: %s", speed)
# Only the case speed == None equals the GUI toggle switch being # Only the case speed == None equals the GUI toggle switch being activated.
# activated.
if speed is not None: if speed is not None:
return return
if self._state is True: if self._is_on:
_LOGGER.error("Already on") _LOGGER.error("Already on")
return return
@ -172,11 +189,11 @@ class ValloxFan(FanEntity):
# This state change affects other entities like sensors. Force an immediate update that can # This state change affects other entities like sensors. Force an immediate update that can
# be observed by all parties involved. # be observed by all parties involved.
await self._state_proxy.async_update(None) await self._state_proxy.async_update()
async def async_turn_off(self, **kwargs) -> None: async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the device off.""" """Turn the device off."""
if self._state is False: if not self._is_on:
_LOGGER.error("Already off") _LOGGER.error("Already off")
return return

View File

@ -19,8 +19,10 @@ from homeassistant.const import (
PERCENTAGE, PERCENTAGE,
TEMP_CELSIUS, TEMP_CELSIUS,
) )
from homeassistant.core import callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import ValloxStateProxy from . import ValloxStateProxy
from .const import DOMAIN, METRIC_KEY_MODE, MODE_ON, SIGNAL_VALLOX_STATE_UPDATE from .const import DOMAIN, METRIC_KEY_MODE, MODE_ON, SIGNAL_VALLOX_STATE_UPDATE
@ -48,7 +50,7 @@ class ValloxSensor(SensorEntity):
self._attr_name = f"{name} {description.name}" self._attr_name = f"{name} {description.name}"
self._attr_available = False self._attr_available = False
async def async_added_to_hass(self): async def async_added_to_hass(self) -> None:
"""Call to update.""" """Call to update."""
self.async_on_remove( self.async_on_remove(
async_dispatcher_connect( async_dispatcher_connect(
@ -57,18 +59,23 @@ class ValloxSensor(SensorEntity):
) )
@callback @callback
def _update_callback(self): def _update_callback(self) -> None:
"""Call update method.""" """Call update method."""
self.async_schedule_update_ha_state(True) self.async_schedule_update_ha_state(True)
async def async_update(self): async def async_update(self) -> None:
"""Fetch state from the ventilation unit.""" """Fetch state from the ventilation unit."""
try: metric_key = self.entity_description.metric_key
self._attr_native_value = self._state_proxy.fetch_metric(
self.entity_description.metric_key
)
except (OSError, KeyError) as err: if metric_key is None:
self._attr_available = False
_LOGGER.error("Error updating sensor. Empty metric key")
return
try:
self._attr_native_value = self._state_proxy.fetch_metric(metric_key)
except (OSError, KeyError, TypeError) as err:
self._attr_available = False self._attr_available = False
_LOGGER.error("Error updating sensor: %s", err) _LOGGER.error("Error updating sensor: %s", err)
return return
@ -79,7 +86,7 @@ class ValloxSensor(SensorEntity):
class ValloxProfileSensor(ValloxSensor): class ValloxProfileSensor(ValloxSensor):
"""Child class for profile reporting.""" """Child class for profile reporting."""
async def async_update(self): async def async_update(self) -> None:
"""Fetch state from the ventilation unit.""" """Fetch state from the ventilation unit."""
try: try:
self._attr_native_value = self._state_proxy.get_profile() self._attr_native_value = self._state_proxy.get_profile()
@ -92,22 +99,21 @@ class ValloxProfileSensor(ValloxSensor):
self._attr_available = True self._attr_available = True
# There seems to be a quirk with respect to the fan speed reporting. The device # There seems to be a quirk with respect to the fan speed reporting. The device keeps on reporting
# keeps on reporting the last valid fan speed from when the device was in # the last valid fan speed from when the device was in regular operation mode, even if it left that
# regular operation mode, even if it left that state and has been shut off in # state and has been shut off in the meantime.
# the meantime.
# #
# Therefore, first query the overall state of the device, and report zero # Therefore, first query the overall state of the device, and report zero percent fan speed in case
# percent fan speed in case it is not in regular operation mode. # it is not in regular operation mode.
class ValloxFanSpeedSensor(ValloxSensor): class ValloxFanSpeedSensor(ValloxSensor):
"""Child class for fan speed reporting.""" """Child class for fan speed reporting."""
async def async_update(self): async def async_update(self) -> None:
"""Fetch state from the ventilation unit.""" """Fetch state from the ventilation unit."""
try: try:
fan_on = self._state_proxy.fetch_metric(METRIC_KEY_MODE) == MODE_ON fan_on = self._state_proxy.fetch_metric(METRIC_KEY_MODE) == MODE_ON
except (OSError, KeyError) as err: except (OSError, KeyError, TypeError) as err:
self._attr_available = False self._attr_available = False
_LOGGER.error("Error updating sensor: %s", err) _LOGGER.error("Error updating sensor: %s", err)
return return
@ -123,26 +129,28 @@ class ValloxFanSpeedSensor(ValloxSensor):
class ValloxFilterRemainingSensor(ValloxSensor): class ValloxFilterRemainingSensor(ValloxSensor):
"""Child class for filter remaining time reporting.""" """Child class for filter remaining time reporting."""
async def async_update(self): async def async_update(self) -> None:
"""Fetch state from the ventilation unit.""" """Fetch state from the ventilation unit."""
try: await super().async_update()
days_remaining = int(
self._state_proxy.fetch_metric(self.entity_description.metric_key)
)
except (OSError, KeyError) as err: # Check if the update in the super call was a success.
self._attr_available = False if not self._attr_available:
_LOGGER.error("Error updating sensor: %s", err)
return return
days_remaining_delta = timedelta(days=days_remaining) if not isinstance(self._attr_native_value, (int, float)):
self._attr_available = False
_LOGGER.error(
"Value has unexpected type: %s", type(self._attr_native_value)
)
return
# Since only a delta of days is received from the device, fix the # Since only a delta of days is received from the device, fix the time so the timestamp does
# time so the timestamp does not change with every update. # not change with every update.
days_remaining = float(self._attr_native_value)
days_remaining_delta = timedelta(days=days_remaining)
now = datetime.utcnow().replace(hour=13, minute=0, second=0, microsecond=0) now = datetime.utcnow().replace(hour=13, minute=0, second=0, microsecond=0)
self._attr_native_value = (now + days_remaining_delta).isoformat() self._attr_native_value = (now + days_remaining_delta).isoformat()
self._attr_available = True
@dataclass @dataclass
@ -235,7 +243,12 @@ SENSORS: tuple[ValloxSensorEntityDescription, ...] = (
) )
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): async def async_setup_platform(
hass: HomeAssistant,
config: ConfigType,
async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the sensors.""" """Set up the sensors."""
if discovery_info is None: if discovery_info is None:
return return

View File

@ -1254,6 +1254,17 @@ no_implicit_optional = true
warn_return_any = true warn_return_any = true
warn_unreachable = true warn_unreachable = true
[mypy-homeassistant.components.vallox.*]
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.water_heater.*] [mypy-homeassistant.components.water_heater.*]
check_untyped_defs = true check_untyped_defs = true
disallow_incomplete_defs = true disallow_incomplete_defs = true