From 6a34e1b7ca614040978db8b071dd055c4dc324af Mon Sep 17 00:00:00 2001 From: Etienne Soufflet Date: Sat, 22 Jun 2024 18:25:17 +0200 Subject: [PATCH] Add tado climate swings and fan level (#117378) --- homeassistant/components/tado/__init__.py | 11 +- homeassistant/components/tado/climate.py | 162 +++++++++++++++++--- homeassistant/components/tado/const.py | 32 +++- homeassistant/components/tado/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 182 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/tado/__init__.py b/homeassistant/components/tado/__init__.py index 8f69ccdaffb..be58c68be91 100644 --- a/homeassistant/components/tado/__init__.py +++ b/homeassistant/components/tado/__init__.py @@ -384,12 +384,15 @@ class TadoConnector: mode=None, fan_speed=None, swing=None, + fan_level=None, + vertical_swing=None, + horizontal_swing=None, ): """Set a zone overlay.""" _LOGGER.debug( ( "Set overlay for zone %s: overlay_mode=%s, temp=%s, duration=%s," - " type=%s, mode=%s fan_speed=%s swing=%s" + " type=%s, mode=%s fan_speed=%s swing=%s fan_level=%s vertical_swing=%s horizontal_swing=%s" ), zone_id, overlay_mode, @@ -399,6 +402,9 @@ class TadoConnector: mode, fan_speed, swing, + fan_level, + vertical_swing, + horizontal_swing, ) try: @@ -412,6 +418,9 @@ class TadoConnector: mode, fan_speed=fan_speed, swing=swing, + fan_level=fan_level, + vertical_swing=vertical_swing, + horizontal_swing=horizontal_swing, ) except RequestException as exc: diff --git a/homeassistant/components/tado/climate.py b/homeassistant/components/tado/climate.py index 3cb5d7fbce9..2698b6e1446 100644 --- a/homeassistant/components/tado/climate.py +++ b/homeassistant/components/tado/climate.py @@ -13,6 +13,10 @@ from homeassistant.components.climate import ( FAN_AUTO, PRESET_AWAY, PRESET_HOME, + SWING_BOTH, + SWING_HORIZONTAL, + SWING_OFF, + SWING_VERTICAL, ClimateEntity, ClimateEntityFeature, HVACAction, @@ -42,6 +46,7 @@ from .const import ( HA_TERMINATION_DURATION, HA_TERMINATION_TYPE, HA_TO_TADO_FAN_MODE_MAP, + HA_TO_TADO_FAN_MODE_MAP_LEGACY, HA_TO_TADO_HVAC_MODE_MAP, HA_TO_TADO_SWING_MODE_MAP, ORDERED_KNOWN_TADO_MODES, @@ -51,11 +56,14 @@ from .const import ( SUPPORT_PRESET_MANUAL, TADO_DEFAULT_MAX_TEMP, TADO_DEFAULT_MIN_TEMP, + TADO_FAN_LEVELS, + TADO_FAN_SPEEDS, TADO_HVAC_ACTION_TO_HA_HVAC_ACTION, TADO_MODES_WITH_NO_TEMP_SETTING, TADO_SWING_OFF, TADO_SWING_ON, TADO_TO_HA_FAN_MODE_MAP, + TADO_TO_HA_FAN_MODE_MAP_LEGACY, TADO_TO_HA_HVAC_MODE_MAP, TADO_TO_HA_OFFSET_MAP, TADO_TO_HA_SWING_MODE_MAP, @@ -147,6 +155,7 @@ def create_climate_entity( TADO_TO_HA_HVAC_MODE_MAP[CONST_MODE_SMART_SCHEDULE], ] supported_fan_modes = None + supported_swing_modes = None heat_temperatures = None cool_temperatures = None @@ -157,10 +166,31 @@ def create_climate_entity( continue supported_hvac_modes.append(TADO_TO_HA_HVAC_MODE_MAP[mode]) - if capabilities[mode].get("swings"): + if ( + capabilities[mode].get("swings") + or capabilities[mode].get("verticalSwing") + or capabilities[mode].get("horizontalSwing") + ): support_flags |= ClimateEntityFeature.SWING_MODE + supported_swing_modes = [] + if capabilities[mode].get("swings"): + supported_swing_modes.append( + TADO_TO_HA_SWING_MODE_MAP[TADO_SWING_ON] + ) + if capabilities[mode].get("verticalSwing"): + supported_swing_modes.append(SWING_VERTICAL) + if capabilities[mode].get("horizontalSwing"): + supported_swing_modes.append(SWING_HORIZONTAL) + if ( + SWING_HORIZONTAL in supported_swing_modes + and SWING_HORIZONTAL in supported_swing_modes + ): + supported_swing_modes.append(SWING_BOTH) + supported_swing_modes.append(TADO_TO_HA_SWING_MODE_MAP[TADO_SWING_OFF]) - if not capabilities[mode].get("fanSpeeds"): + if not capabilities[mode].get("fanSpeeds") and not capabilities[mode].get( + "fanLevel" + ): continue support_flags |= ClimateEntityFeature.FAN_MODE @@ -168,10 +198,16 @@ def create_climate_entity( if supported_fan_modes: continue - supported_fan_modes = [ - TADO_TO_HA_FAN_MODE_MAP[speed] - for speed in capabilities[mode]["fanSpeeds"] - ] + if capabilities[mode].get("fanSpeeds"): + supported_fan_modes = [ + TADO_TO_HA_FAN_MODE_MAP_LEGACY[speed] + for speed in capabilities[mode]["fanSpeeds"] + ] + else: + supported_fan_modes = [ + TADO_TO_HA_FAN_MODE_MAP[level] + for level in capabilities[mode]["fanLevel"] + ] cool_temperatures = capabilities[CONST_MODE_COOL]["temperatures"] else: @@ -219,6 +255,7 @@ def create_climate_entity( cool_max_temp, cool_step, supported_fan_modes, + supported_swing_modes, ) @@ -247,6 +284,7 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): cool_max_temp: float | None = None, cool_step: float | None = None, supported_fan_modes: list[str] | None = None, + supported_swing_modes: list[str] | None = None, ) -> None: """Initialize of Tado climate entity.""" self._tado = tado @@ -267,11 +305,7 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): self._cur_temp = None self._cur_humidity = None - if self.supported_features & ClimateEntityFeature.SWING_MODE: - self._attr_swing_modes = [ - TADO_TO_HA_SWING_MODE_MAP[TADO_SWING_ON], - TADO_TO_HA_SWING_MODE_MAP[TADO_SWING_OFF], - ] + self._attr_swing_modes = supported_swing_modes self._heat_min_temp = heat_min_temp self._heat_max_temp = heat_max_temp @@ -287,6 +321,8 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): self._current_tado_hvac_mode = CONST_MODE_OFF self._current_tado_hvac_action = HVACAction.OFF self._current_tado_swing_mode = TADO_SWING_OFF + self._current_tado_vertical_swing = TADO_SWING_OFF + self._current_tado_horizontal_swing = TADO_SWING_OFF self._tado_zone_data: PyTado.TadoZone = {} self._tado_geofence_data: dict[str, str] | None = None @@ -348,12 +384,20 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): 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) + return TADO_TO_HA_FAN_MODE_MAP.get( + self._current_tado_fan_speed, + TADO_TO_HA_FAN_MODE_MAP_LEGACY.get( + self._current_tado_fan_speed, FAN_AUTO + ), + ) return None def set_fan_mode(self, fan_mode: str) -> None: """Turn fan on/off.""" - self._control_hvac(fan_mode=HA_TO_TADO_FAN_MODE_MAP[fan_mode]) + if self._current_tado_fan_speed in TADO_FAN_LEVELS: + self._control_hvac(fan_mode=HA_TO_TADO_FAN_MODE_MAP[fan_mode]) + else: + self._control_hvac(fan_mode=HA_TO_TADO_FAN_MODE_MAP_LEGACY[fan_mode]) @property def preset_mode(self) -> str: @@ -476,7 +520,23 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): @property def swing_mode(self) -> str | None: """Active swing mode for the device.""" - return TADO_TO_HA_SWING_MODE_MAP[self._current_tado_swing_mode] + swing_modes_tuple = ( + self._current_tado_swing_mode, + self._current_tado_vertical_swing, + self._current_tado_horizontal_swing, + ) + if swing_modes_tuple == (TADO_SWING_OFF, TADO_SWING_OFF, TADO_SWING_OFF): + return TADO_TO_HA_SWING_MODE_MAP[TADO_SWING_OFF] + if swing_modes_tuple == (TADO_SWING_ON, TADO_SWING_OFF, TADO_SWING_OFF): + return TADO_TO_HA_SWING_MODE_MAP[TADO_SWING_ON] + if swing_modes_tuple == (TADO_SWING_OFF, TADO_SWING_ON, TADO_SWING_OFF): + return SWING_VERTICAL + if swing_modes_tuple == (TADO_SWING_OFF, TADO_SWING_OFF, TADO_SWING_ON): + return SWING_HORIZONTAL + if swing_modes_tuple == (TADO_SWING_OFF, TADO_SWING_ON, TADO_SWING_ON): + return SWING_BOTH + + return TADO_TO_HA_SWING_MODE_MAP[TADO_SWING_OFF] @property def extra_state_attributes(self) -> Mapping[str, Any] | None: @@ -492,7 +552,35 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): def set_swing_mode(self, swing_mode: str) -> None: """Set swing modes for the device.""" - self._control_hvac(swing_mode=HA_TO_TADO_SWING_MODE_MAP[swing_mode]) + vertical_swing = None + horizontal_swing = None + swing = None + if self._attr_swing_modes is None: + return + if ( + SWING_VERTICAL in self._attr_swing_modes + or SWING_HORIZONTAL in self._attr_swing_modes + ): + if swing_mode == SWING_VERTICAL: + vertical_swing = TADO_SWING_ON + elif swing_mode == SWING_HORIZONTAL: + horizontal_swing = TADO_SWING_ON + elif swing_mode == SWING_BOTH: + vertical_swing = TADO_SWING_ON + horizontal_swing = TADO_SWING_ON + elif swing_mode == SWING_OFF: + if SWING_VERTICAL in self._attr_swing_modes: + vertical_swing = TADO_SWING_OFF + if SWING_HORIZONTAL in self._attr_swing_modes: + horizontal_swing = TADO_SWING_OFF + else: + swing = HA_TO_TADO_SWING_MODE_MAP[swing_mode] + + self._control_hvac( + swing_mode=swing, + vertical_swing=vertical_swing, + horizontal_swing=horizontal_swing, + ) @callback def _async_update_zone_data(self) -> None: @@ -509,10 +597,22 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): self._tado_zone_temp_offset[attr] = self._tado.data["device"][ self._device_id ][TEMP_OFFSET][offset_key] - self._current_tado_fan_speed = self._tado_zone_data.current_fan_speed + + self._current_tado_fan_speed = ( + self._tado_zone_data.current_fan_level + if self._tado_zone_data.current_fan_level is not None + else self._tado_zone_data.current_fan_speed + ) + self._current_tado_hvac_mode = self._tado_zone_data.current_hvac_mode self._current_tado_hvac_action = self._tado_zone_data.current_hvac_action self._current_tado_swing_mode = self._tado_zone_data.current_swing_mode + self._current_tado_vertical_swing = ( + self._tado_zone_data.current_vertical_swing_mode + ) + self._current_tado_horizontal_swing = ( + self._tado_zone_data.current_horizontal_swing_mode + ) @callback def _async_update_zone_callback(self) -> None: @@ -556,6 +656,8 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): swing_mode: str | None = None, duration: int | None = None, overlay_mode: str | None = None, + vertical_swing: str | None = None, + horizontal_swing: str | None = None, ): """Send new target temperature to Tado.""" if hvac_mode: @@ -570,6 +672,12 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): if swing_mode: self._current_tado_swing_mode = swing_mode + if vertical_swing: + self._current_tado_vertical_swing = vertical_swing + + if horizontal_swing: + self._current_tado_horizontal_swing = horizontal_swing + self._normalize_target_temp_for_hvac_mode() # tado does not permit setting the fan speed to @@ -627,11 +735,24 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): temperature_to_send = None fan_speed = None + fan_level = None if self.supported_features & ClimateEntityFeature.FAN_MODE: - fan_speed = self._current_tado_fan_speed + if self._current_tado_fan_speed in TADO_FAN_LEVELS: + fan_level = self._current_tado_fan_speed + elif self._current_tado_fan_speed in TADO_FAN_SPEEDS: + fan_speed = self._current_tado_fan_speed swing = None - if self.supported_features & ClimateEntityFeature.SWING_MODE: - swing = self._current_tado_swing_mode + vertical_swing = None + horizontal_swing = None + if ( + self.supported_features & ClimateEntityFeature.SWING_MODE + ) and self._attr_swing_modes is not None: + if SWING_VERTICAL in self._attr_swing_modes: + vertical_swing = self._current_tado_vertical_swing + if SWING_HORIZONTAL in self._attr_swing_modes: + horizontal_swing = self._current_tado_horizontal_swing + if vertical_swing is None and horizontal_swing is None: + swing = self._current_tado_swing_mode self._tado.set_zone_overlay( zone_id=self.zone_id, @@ -642,4 +763,7 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): mode=self._current_tado_hvac_mode, fan_speed=fan_speed, # api defaults to not sending fanSpeed if None specified swing=swing, # api defaults to not sending swing if None specified + fan_level=fan_level, # api defaults to not sending fanLevel if fanSpeend not None + vertical_swing=vertical_swing, # api defaults to not sending verticalSwing if swing not None + horizontal_swing=horizontal_swing, # api defaults to not sending horizontalSwing if swing not None ) diff --git a/homeassistant/components/tado/const.py b/homeassistant/components/tado/const.py index be35bbb8e25..a41003da95f 100644 --- a/homeassistant/components/tado/const.py +++ b/homeassistant/components/tado/const.py @@ -77,9 +77,13 @@ CONST_LINK_OFFLINE = "OFFLINE" CONST_FAN_OFF = "OFF" CONST_FAN_AUTO = "AUTO" -CONST_FAN_LOW = "LOW" -CONST_FAN_MIDDLE = "MIDDLE" -CONST_FAN_HIGH = "HIGH" +CONST_FAN_LOW_LEGACY = "LOW" +CONST_FAN_MIDDLE_LEGACY = "MIDDLE" +CONST_FAN_HIGH_LEGACY = "HIGH" + +CONST_FAN_LEVEL_1 = "LEVEL1" +CONST_FAN_LEVEL_2 = "LEVEL2" +CONST_FAN_LEVEL_3 = "LEVEL3" # When we change the temperature setting, we need an overlay mode @@ -139,20 +143,36 @@ HA_TO_TADO_HVAC_MODE_MAP = { HVACMode.FAN_ONLY: CONST_MODE_FAN, } +HA_TO_TADO_FAN_MODE_MAP_LEGACY = { + FAN_AUTO: CONST_FAN_AUTO, + FAN_OFF: CONST_FAN_OFF, + FAN_LOW: CONST_FAN_LOW_LEGACY, + FAN_MEDIUM: CONST_FAN_MIDDLE_LEGACY, + FAN_HIGH: CONST_FAN_HIGH_LEGACY, +} + HA_TO_TADO_FAN_MODE_MAP = { FAN_AUTO: CONST_FAN_AUTO, FAN_OFF: CONST_FAN_OFF, - FAN_LOW: CONST_FAN_LOW, - FAN_MEDIUM: CONST_FAN_MIDDLE, - FAN_HIGH: CONST_FAN_HIGH, + FAN_LOW: CONST_FAN_LEVEL_1, + FAN_MEDIUM: CONST_FAN_LEVEL_2, + FAN_HIGH: CONST_FAN_LEVEL_3, } TADO_TO_HA_HVAC_MODE_MAP = { value: key for key, value in HA_TO_TADO_HVAC_MODE_MAP.items() } +TADO_TO_HA_FAN_MODE_MAP_LEGACY = { + value: key for key, value in HA_TO_TADO_FAN_MODE_MAP_LEGACY.items() +} + TADO_TO_HA_FAN_MODE_MAP = {value: key for key, value in HA_TO_TADO_FAN_MODE_MAP.items()} +TADO_FAN_SPEEDS = list(HA_TO_TADO_FAN_MODE_MAP_LEGACY.values()) + +TADO_FAN_LEVELS = list(HA_TO_TADO_FAN_MODE_MAP.values()) + DEFAULT_TADO_PRECISION = 0.1 # Constant for Auto Geolocation mode diff --git a/homeassistant/components/tado/manifest.json b/homeassistant/components/tado/manifest.json index 0f3288ba904..b0c00c888b7 100644 --- a/homeassistant/components/tado/manifest.json +++ b/homeassistant/components/tado/manifest.json @@ -14,5 +14,5 @@ }, "iot_class": "cloud_polling", "loggers": ["PyTado"], - "requirements": ["python-tado==0.17.4"] + "requirements": ["python-tado==0.17.6"] } diff --git a/requirements_all.txt b/requirements_all.txt index 37d3b6e68a0..9c940ff410a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2327,7 +2327,7 @@ python-smarttub==0.0.36 python-songpal==0.16.2 # homeassistant.components.tado -python-tado==0.17.4 +python-tado==0.17.6 # homeassistant.components.technove python-technove==1.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0c565ad79f2..0d3112c7aba 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1821,7 +1821,7 @@ python-smarttub==0.0.36 python-songpal==0.16.2 # homeassistant.components.tado -python-tado==0.17.4 +python-tado==0.17.6 # homeassistant.components.technove python-technove==1.2.2