From c52e2038be555c48dde61892fa138cadfd307ef6 Mon Sep 17 00:00:00 2001 From: Erwin Douna Date: Wed, 28 Feb 2024 11:28:51 +0100 Subject: [PATCH] Tado code quality improvements (#107678) Co-authored-by: Robert Resch --- .../components/tado/binary_sensor.py | 22 +-- homeassistant/components/tado/climate.py | 144 ++++++++++-------- homeassistant/components/tado/const.py | 2 + homeassistant/components/tado/water_heater.py | 56 ++++--- 4 files changed, 124 insertions(+), 100 deletions(-) diff --git a/homeassistant/components/tado/binary_sensor.py b/homeassistant/components/tado/binary_sensor.py index 0f7a1b2b307..c033ef62e03 100644 --- a/homeassistant/components/tado/binary_sensor.py +++ b/homeassistant/components/tado/binary_sensor.py @@ -17,6 +17,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType +from . import TadoConnector from .const import ( DATA, DOMAIN, @@ -170,7 +171,10 @@ class TadoDeviceBinarySensor(TadoDeviceEntity, BinarySensorEntity): entity_description: TadoBinarySensorEntityDescription def __init__( - self, tado, device_info, entity_description: TadoBinarySensorEntityDescription + self, + tado: TadoConnector, + device_info: dict[str, Any], + entity_description: TadoBinarySensorEntityDescription, ) -> None: """Initialize of the Tado Sensor.""" self.entity_description = entity_description @@ -183,7 +187,6 @@ class TadoDeviceBinarySensor(TadoDeviceEntity, BinarySensorEntity): async def async_added_to_hass(self) -> None: """Register for sensor updates.""" - self.async_on_remove( async_dispatcher_connect( self.hass, @@ -196,13 +199,13 @@ class TadoDeviceBinarySensor(TadoDeviceEntity, BinarySensorEntity): self._async_update_device_data() @callback - def _async_update_callback(self): + def _async_update_callback(self) -> None: """Update and write state.""" self._async_update_device_data() self.async_write_ha_state() @callback - def _async_update_device_data(self): + def _async_update_device_data(self) -> None: """Handle update callbacks.""" try: self._device_info = self._tado.data["device"][self.device_id] @@ -223,9 +226,9 @@ class TadoZoneBinarySensor(TadoZoneEntity, BinarySensorEntity): def __init__( self, - tado, - zone_name, - zone_id, + tado: TadoConnector, + zone_name: str, + zone_id: int, entity_description: TadoBinarySensorEntityDescription, ) -> None: """Initialize of the Tado Sensor.""" @@ -237,7 +240,6 @@ class TadoZoneBinarySensor(TadoZoneEntity, BinarySensorEntity): async def async_added_to_hass(self) -> None: """Register for sensor updates.""" - self.async_on_remove( async_dispatcher_connect( self.hass, @@ -250,13 +252,13 @@ class TadoZoneBinarySensor(TadoZoneEntity, BinarySensorEntity): self._async_update_zone_data() @callback - def _async_update_callback(self): + def _async_update_callback(self) -> None: """Update and write state.""" self._async_update_zone_data() self.async_write_ha_state() @callback - def _async_update_zone_data(self): + def _async_update_zone_data(self) -> None: """Handle update callbacks.""" try: tado_zone_data = self._tado.data["zone"][self.zone_id] diff --git a/homeassistant/components/tado/climate.py b/homeassistant/components/tado/climate.py index dd0d6a22a08..5d17655c104 100644 --- a/homeassistant/components/tado/climate.py +++ b/homeassistant/components/tado/climate.py @@ -1,9 +1,12 @@ """Support for Tado thermostats.""" + from __future__ import annotations +from collections.abc import Mapping import logging from typing import Any +import PyTado import voluptuous as vol from homeassistant.components.climate import ( @@ -22,6 +25,7 @@ from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback +from . import TadoConnector from .const import ( CONST_EXCLUSIVE_OVERLAY_GROUP, CONST_FAN_AUTO, @@ -48,6 +52,8 @@ from .const import ( SIGNAL_TADO_UPDATE_RECEIVED, SUPPORT_PRESET_AUTO, SUPPORT_PRESET_MANUAL, + TADO_DEFAULT_MAX_TEMP, + TADO_DEFAULT_MIN_TEMP, TADO_HVAC_ACTION_TO_HA_HVAC_ACTION, TADO_MODES_WITH_NO_TEMP_SETTING, TADO_SWING_OFF, @@ -111,7 +117,7 @@ async def async_setup_entry( async_add_entities(entities, True) -def _generate_entities(tado): +def _generate_entities(tado: TadoConnector) -> list[TadoClimate]: """Create all climate entities.""" entities = [] for zone in tado.zones: @@ -124,7 +130,9 @@ def _generate_entities(tado): return entities -def create_climate_entity(tado, name: str, zone_id: int, device_info: dict): +def create_climate_entity( + tado: TadoConnector, name: str, zone_id: int, device_info: dict +) -> TadoClimate | None: """Create a Tado climate entity.""" capabilities = tado.get_capabilities(zone_id) _LOGGER.debug("Capabilities for zone %s: %s", zone_id, capabilities) @@ -203,16 +211,16 @@ def create_climate_entity(tado, name: str, zone_id: int, device_info: dict): name, zone_id, zone_type, + supported_hvac_modes, + support_flags, + device_info, heat_min_temp, heat_max_temp, heat_step, cool_min_temp, cool_max_temp, cool_step, - supported_hvac_modes, supported_fan_modes, - support_flags, - device_info, ) return entity @@ -228,21 +236,21 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): def __init__( self, - tado, - zone_name, - zone_id, - zone_type, - heat_min_temp, - heat_max_temp, - heat_step, - cool_min_temp, - cool_max_temp, - cool_step, - supported_hvac_modes, - supported_fan_modes, - support_flags, - device_info, - ): + tado: TadoConnector, + zone_name: str, + zone_id: int, + zone_type: str, + supported_hvac_modes: list[HVACMode], + support_flags: ClimateEntityFeature, + device_info: dict[str, str], + heat_min_temp: float | None = None, + heat_max_temp: float | None = None, + heat_step: float | None = None, + cool_min_temp: float | None = None, + cool_max_temp: float | None = None, + cool_step: float | None = None, + supported_fan_modes: list[str] | None = None, + ) -> None: """Initialize of Tado climate entity.""" self._tado = tado super().__init__(zone_name, tado.home_id, zone_id) @@ -276,24 +284,23 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): self._cool_max_temp = cool_max_temp self._cool_step = cool_step - self._target_temp = None + self._target_temp: float | None = None self._current_tado_fan_speed = CONST_FAN_OFF self._current_tado_hvac_mode = CONST_MODE_OFF self._current_tado_hvac_action = HVACAction.OFF self._current_tado_swing_mode = TADO_SWING_OFF - self._tado_zone_data = None - self._tado_geofence_data = None + self._tado_zone_data: PyTado.TadoZone = {} + self._tado_geofence_data: dict[str, str] | None = None - self._tado_zone_temp_offset = {} + self._tado_zone_temp_offset: dict[str, Any] = {} self._async_update_home_data() self._async_update_zone_data() async def async_added_to_hass(self) -> None: """Register for sensor updates.""" - self.async_on_remove( async_dispatcher_connect( self.hass, @@ -313,12 +320,12 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): ) @property - def current_humidity(self): + def current_humidity(self) -> int | None: """Return the current humidity.""" return self._tado_zone_data.current_humidity @property - def current_temperature(self): + def current_temperature(self) -> float | None: """Return the sensor temperature.""" return self._tado_zone_data.current_temp @@ -341,7 +348,7 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): ) @property - def fan_mode(self): + def fan_mode(self) -> str | None: """Return the fan setting.""" if self._ac_device: return TADO_TO_HA_FAN_MODE_MAP.get(self._current_tado_fan_speed, FAN_AUTO) @@ -352,10 +359,13 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): self._control_hvac(fan_mode=HA_TO_TADO_FAN_MODE_MAP[fan_mode]) @property - def preset_mode(self): + def preset_mode(self) -> str: """Return the current preset mode (home, away or auto).""" - if "presenceLocked" in self._tado_geofence_data: + if ( + self._tado_geofence_data is not None + and "presenceLocked" in self._tado_geofence_data + ): if not self._tado_geofence_data["presenceLocked"]: return PRESET_AUTO if self._tado_zone_data.is_away: @@ -363,7 +373,7 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): return PRESET_HOME @property - def preset_modes(self): + def preset_modes(self) -> list[str]: """Return a list of available preset modes.""" if self._tado.get_auto_geofencing_supported(): return SUPPORT_PRESET_AUTO @@ -374,14 +384,14 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): self._tado.set_presence(preset_mode) @property - def target_temperature_step(self): + def target_temperature_step(self) -> float | None: """Return the supported step of target temperature.""" if self._tado_zone_data.current_hvac_mode == CONST_MODE_COOL: return self._cool_step or self._heat_step return self._heat_step or self._cool_step @property - def target_temperature(self): + def target_temperature(self) -> float | None: """Return the temperature we try to reach.""" # If the target temperature will be None # if the device is performing an action @@ -389,7 +399,12 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): # the device is switching states return self._tado_zone_data.target_temp or self._tado_zone_data.current_temp - def set_timer(self, temperature=None, time_period=None, requested_overlay=None): + def set_timer( + self, + temperature: float, + time_period: int, + requested_overlay: str, + ): """Set the timer on the entity, and temperature if supported.""" self._control_hvac( @@ -399,7 +414,7 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): overlay_mode=requested_overlay, ) - def set_temp_offset(self, offset): + def set_temp_offset(self, offset: float) -> None: """Set offset on the entity.""" _LOGGER.debug( @@ -428,7 +443,6 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): def set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set new target hvac mode.""" - self._control_hvac(hvac_mode=HA_TO_TADO_HVAC_MODE_MAP[hvac_mode]) @property @@ -437,7 +451,7 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): return self._tado_zone_data.available @property - def min_temp(self): + def min_temp(self) -> float: """Return the minimum temperature.""" if ( self._current_tado_hvac_mode == CONST_MODE_COOL @@ -447,10 +461,10 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): if self._heat_min_temp is not None: return self._heat_min_temp - return self._cool_min_temp + return TADO_DEFAULT_MIN_TEMP @property - def max_temp(self): + def max_temp(self) -> float: """Return the maximum temperature.""" if ( self._current_tado_hvac_mode == CONST_MODE_HEAT @@ -460,17 +474,17 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): if self._heat_max_temp is not None: return self._heat_max_temp - return self._heat_max_temp + return TADO_DEFAULT_MAX_TEMP @property - def swing_mode(self): + def swing_mode(self) -> str | None: """Active swing mode for the device.""" return TADO_TO_HA_SWING_MODE_MAP[self._current_tado_swing_mode] @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> Mapping[str, Any] | None: """Return temperature offset.""" - state_attr = self._tado_zone_temp_offset + state_attr: dict[str, Any] = self._tado_zone_temp_offset state_attr[ HA_TERMINATION_TYPE ] = self._tado_zone_data.default_overlay_termination_type @@ -484,7 +498,7 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): self._control_hvac(swing_mode=HA_TO_TADO_SWING_MODE_MAP[swing_mode]) @callback - def _async_update_zone_data(self): + def _async_update_zone_data(self) -> None: """Load tado data into zone.""" self._tado_zone_data = self._tado.data["zone"][self.zone_id] @@ -504,49 +518,49 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): self._current_tado_swing_mode = self._tado_zone_data.current_swing_mode @callback - def _async_update_zone_callback(self): + def _async_update_zone_callback(self) -> None: """Load tado data and update state.""" self._async_update_zone_data() self.async_write_ha_state() @callback - def _async_update_home_data(self): + def _async_update_home_data(self) -> None: """Load tado geofencing data into zone.""" self._tado_geofence_data = self._tado.data["geofence"] @callback - def _async_update_home_callback(self): + def _async_update_home_callback(self) -> None: """Load tado data and update state.""" self._async_update_home_data() self.async_write_ha_state() - def _normalize_target_temp_for_hvac_mode(self): + def _normalize_target_temp_for_hvac_mode(self) -> None: + def adjust_temp(min_temp, max_temp) -> float | None: + if max_temp is not None and self._target_temp > max_temp: + return max_temp + if min_temp is not None and self._target_temp < min_temp: + return min_temp + return self._target_temp + # Set a target temperature if we don't have any # This can happen when we switch from Off to On if self._target_temp is None: self._target_temp = self._tado_zone_data.current_temp elif self._current_tado_hvac_mode == CONST_MODE_COOL: - if self._target_temp > self._cool_max_temp: - self._target_temp = self._cool_max_temp - elif self._target_temp < self._cool_min_temp: - self._target_temp = self._cool_min_temp + self._target_temp = adjust_temp(self._cool_min_temp, self._cool_max_temp) elif self._current_tado_hvac_mode == CONST_MODE_HEAT: - if self._target_temp > self._heat_max_temp: - self._target_temp = self._heat_max_temp - elif self._target_temp < self._heat_min_temp: - self._target_temp = self._heat_min_temp + self._target_temp = adjust_temp(self._heat_min_temp, self._heat_max_temp) def _control_hvac( self, - hvac_mode=None, - target_temp=None, - fan_mode=None, - swing_mode=None, - duration=None, - overlay_mode=None, + hvac_mode: str | None = None, + target_temp: float | None = None, + fan_mode: str | None = None, + swing_mode: str | None = None, + duration: int | None = None, + overlay_mode: str | None = None, ): """Send new target temperature to Tado.""" - if hvac_mode: self._current_tado_hvac_mode = hvac_mode @@ -605,9 +619,9 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): # If we ended up with a timer but no duration, set a default duration if overlay_mode == CONST_OVERLAY_TIMER and duration is None: duration = ( - self._tado_zone_data.default_overlay_termination_duration + int(self._tado_zone_data.default_overlay_termination_duration) if self._tado_zone_data.default_overlay_termination_duration is not None - else "3600" + else 3600 ) _LOGGER.debug( diff --git a/homeassistant/components/tado/const.py b/homeassistant/components/tado/const.py index 24123d3f2b8..6f32eb1a05c 100644 --- a/homeassistant/components/tado/const.py +++ b/homeassistant/components/tado/const.py @@ -205,6 +205,8 @@ TADO_TO_HA_OFFSET_MAP = { HA_TERMINATION_TYPE = "default_overlay_type" HA_TERMINATION_DURATION = "default_overlay_seconds" +TADO_DEFAULT_MIN_TEMP = 5 +TADO_DEFAULT_MAX_TEMP = 25 # Constants for service calls SERVICE_ADD_METER_READING = "add_meter_reading" CONF_CONFIG_ENTRY = "config_entry" diff --git a/homeassistant/components/tado/water_heater.py b/homeassistant/components/tado/water_heater.py index b7e68bbb100..cdbc041f535 100644 --- a/homeassistant/components/tado/water_heater.py +++ b/homeassistant/components/tado/water_heater.py @@ -2,6 +2,7 @@ import logging from typing import Any +import PyTado import voluptuous as vol from homeassistant.components.water_heater import ( @@ -15,6 +16,7 @@ from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback +from . import TadoConnector from .const import ( CONST_HVAC_HEAT, CONST_MODE_AUTO, @@ -27,6 +29,8 @@ from .const import ( DATA, DOMAIN, SIGNAL_TADO_UPDATE_RECEIVED, + TADO_DEFAULT_MAX_TEMP, + TADO_DEFAULT_MIN_TEMP, TYPE_HOT_WATER, ) from .entity import TadoZoneEntity @@ -78,7 +82,7 @@ async def async_setup_entry( async_add_entities(entities, True) -def _generate_entities(tado): +def _generate_entities(tado: TadoConnector) -> list[WaterHeaterEntity]: """Create all water heater entities.""" entities = [] @@ -90,7 +94,7 @@ def _generate_entities(tado): return entities -def create_water_heater_entity(tado, name: str, zone_id: int, zone: str): +def create_water_heater_entity(tado: TadoConnector, name: str, zone_id: int, zone: str): """Create a Tado water heater device.""" capabilities = tado.get_capabilities(zone_id) @@ -125,15 +129,14 @@ class TadoWaterHeater(TadoZoneEntity, WaterHeaterEntity): def __init__( self, - tado, - zone_name, - zone_id, - supports_temperature_control, - min_temp, - max_temp, - ): + tado: TadoConnector, + zone_name: str, + zone_id: int, + supports_temperature_control: bool, + min_temp: float | None = None, + max_temp: float | None = None, + ) -> None: """Initialize of Tado water heater entity.""" - self._tado = tado super().__init__(zone_name, tado.home_id, zone_id) @@ -143,10 +146,10 @@ class TadoWaterHeater(TadoZoneEntity, WaterHeaterEntity): self._device_is_active = False self._supports_temperature_control = supports_temperature_control - self._min_temperature = min_temp - self._max_temperature = max_temp + self._min_temperature = min_temp or TADO_DEFAULT_MIN_TEMP + self._max_temperature = max_temp or TADO_DEFAULT_MAX_TEMP - self._target_temp = None + self._target_temp: float | None = None self._attr_supported_features = WaterHeaterEntityFeature.OPERATION_MODE if self._supports_temperature_control: @@ -154,11 +157,10 @@ class TadoWaterHeater(TadoZoneEntity, WaterHeaterEntity): self._current_tado_hvac_mode = CONST_MODE_SMART_SCHEDULE self._overlay_mode = CONST_MODE_SMART_SCHEDULE - self._tado_zone_data = None + self._tado_zone_data: PyTado.TadoZone = {} async def async_added_to_hass(self) -> None: """Register for sensor updates.""" - self.async_on_remove( async_dispatcher_connect( self.hass, @@ -171,27 +173,27 @@ class TadoWaterHeater(TadoZoneEntity, WaterHeaterEntity): self._async_update_data() @property - def current_operation(self): + def current_operation(self) -> str | None: """Return current readable operation mode.""" return WATER_HEATER_MAP_TADO.get(self._current_tado_hvac_mode) @property - def target_temperature(self): + def target_temperature(self) -> float | None: """Return the temperature we try to reach.""" return self._tado_zone_data.target_temp @property - def is_away_mode_on(self): + def is_away_mode_on(self) -> bool: """Return true if away mode is on.""" return self._tado_zone_data.is_away @property - def min_temp(self): + def min_temp(self) -> float: """Return the minimum temperature.""" return self._min_temperature @property - def max_temp(self): + def max_temp(self) -> float: """Return the maximum temperature.""" return self._max_temperature @@ -208,7 +210,7 @@ class TadoWaterHeater(TadoZoneEntity, WaterHeaterEntity): self._control_heater(hvac_mode=mode) - def set_timer(self, time_period, temperature=None): + def set_timer(self, time_period: int, temperature: float | None = None): """Set the timer on the entity, and temperature if supported.""" if not self._supports_temperature_control and temperature is not None: temperature = None @@ -234,21 +236,25 @@ class TadoWaterHeater(TadoZoneEntity, WaterHeaterEntity): self._control_heater(target_temp=temperature, hvac_mode=CONST_MODE_HEAT) @callback - def _async_update_callback(self): + def _async_update_callback(self) -> None: """Load tado data and update state.""" self._async_update_data() self.async_write_ha_state() @callback - def _async_update_data(self): + def _async_update_data(self) -> None: """Load tado data.""" _LOGGER.debug("Updating water_heater platform for zone %d", self.zone_id) self._tado_zone_data = self._tado.data["zone"][self.zone_id] self._current_tado_hvac_mode = self._tado_zone_data.current_hvac_mode - def _control_heater(self, hvac_mode=None, target_temp=None, duration=None): + def _control_heater( + self, + hvac_mode: str | None = None, + target_temp: float | None = None, + duration: int | None = None, + ): """Send new target temperature.""" - if hvac_mode: self._current_tado_hvac_mode = hvac_mode