diff --git a/CODEOWNERS b/CODEOWNERS index ba233c0c1413e..24733710102a3 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1464,8 +1464,8 @@ build.json @home-assistant/supervisor /tests/components/system_bridge/ @timmo001 /homeassistant/components/systemmonitor/ @gjohansson-ST /tests/components/systemmonitor/ @gjohansson-ST -/homeassistant/components/tado/ @chiefdragon @erwindouna -/tests/components/tado/ @chiefdragon @erwindouna +/homeassistant/components/tado/ @chiefdragon @erwindouna @Moritz-Schmidt +/tests/components/tado/ @chiefdragon @erwindouna @Moritz-Schmidt /homeassistant/components/tag/ @balloob @dmulcahey /tests/components/tag/ @balloob @dmulcahey /homeassistant/components/tailscale/ @frenck diff --git a/homeassistant/components/tado/binary_sensor.py b/homeassistant/components/tado/binary_sensor.py index 25c1c801155b8..ce561fd0b4f13 100644 --- a/homeassistant/components/tado/binary_sensor.py +++ b/homeassistant/components/tado/binary_sensor.py @@ -20,6 +20,8 @@ from . import TadoConfigEntry from .const import ( SIGNAL_TADO_UPDATE_RECEIVED, + TADO_LINE_X, + TADO_PRE_LINE_X, TYPE_AIR_CONDITIONING, TYPE_BATTERY, TYPE_HEATING, @@ -52,6 +54,12 @@ class TadoBinarySensorEntityDescription(BinarySensorEntityDescription): state_fn=lambda data: data.get("connectionState", {}).get("value", False), device_class=BinarySensorDeviceClass.CONNECTIVITY, ) +TADO_X_CONNECTION_STATE_ENTITY_DESCRIPTION = TadoBinarySensorEntityDescription( + key="connection state", + translation_key="connection_state", + state_fn=lambda data: data.get("connection", {}).get("state", False), + device_class=BinarySensorDeviceClass.CONNECTIVITY, +) POWER_ENTITY_DESCRIPTION = TadoBinarySensorEntityDescription( key="power", state_fn=lambda data: data.power == "ON", @@ -59,7 +67,7 @@ class TadoBinarySensorEntityDescription(BinarySensorEntityDescription): ) LINK_ENTITY_DESCRIPTION = TadoBinarySensorEntityDescription( key="link", - state_fn=lambda data: data.link == "ONLINE", + state_fn=lambda data: data.link in ("ONLINE", "CONNECTED"), device_class=BinarySensorDeviceClass.CONNECTIVITY, ) OVERLAY_ENTITY_DESCRIPTION = TadoBinarySensorEntityDescription( @@ -85,34 +93,60 @@ class TadoBinarySensorEntityDescription(BinarySensorEntityDescription): ) DEVICE_SENSORS = { - TYPE_BATTERY: [ - BATTERY_STATE_ENTITY_DESCRIPTION, - CONNECTION_STATE_ENTITY_DESCRIPTION, - ], - TYPE_POWER: [ - CONNECTION_STATE_ENTITY_DESCRIPTION, - ], + TADO_LINE_X: { + TYPE_BATTERY: [ + BATTERY_STATE_ENTITY_DESCRIPTION, + TADO_X_CONNECTION_STATE_ENTITY_DESCRIPTION, + ], + TYPE_POWER: [ + TADO_X_CONNECTION_STATE_ENTITY_DESCRIPTION, + ], + }, + TADO_PRE_LINE_X: { + TYPE_BATTERY: [ + BATTERY_STATE_ENTITY_DESCRIPTION, + CONNECTION_STATE_ENTITY_DESCRIPTION, + ], + TYPE_POWER: [ + CONNECTION_STATE_ENTITY_DESCRIPTION, + ], + }, } ZONE_SENSORS = { - TYPE_HEATING: [ - POWER_ENTITY_DESCRIPTION, - LINK_ENTITY_DESCRIPTION, - OVERLAY_ENTITY_DESCRIPTION, - OPEN_WINDOW_ENTITY_DESCRIPTION, - EARLY_START_ENTITY_DESCRIPTION, - ], - TYPE_AIR_CONDITIONING: [ - POWER_ENTITY_DESCRIPTION, - LINK_ENTITY_DESCRIPTION, - OVERLAY_ENTITY_DESCRIPTION, - OPEN_WINDOW_ENTITY_DESCRIPTION, - ], - TYPE_HOT_WATER: [ - POWER_ENTITY_DESCRIPTION, - LINK_ENTITY_DESCRIPTION, - OVERLAY_ENTITY_DESCRIPTION, - ], + TADO_LINE_X: { + TYPE_HEATING: [ + POWER_ENTITY_DESCRIPTION, + LINK_ENTITY_DESCRIPTION, + OVERLAY_ENTITY_DESCRIPTION, + OPEN_WINDOW_ENTITY_DESCRIPTION, + ], + TYPE_HOT_WATER: [ + POWER_ENTITY_DESCRIPTION, + LINK_ENTITY_DESCRIPTION, + OVERLAY_ENTITY_DESCRIPTION, + ], + }, + TADO_PRE_LINE_X: { + TYPE_HEATING: [ + POWER_ENTITY_DESCRIPTION, + LINK_ENTITY_DESCRIPTION, + OVERLAY_ENTITY_DESCRIPTION, + OPEN_WINDOW_ENTITY_DESCRIPTION, + EARLY_START_ENTITY_DESCRIPTION, + ], + TYPE_AIR_CONDITIONING: [ + POWER_ENTITY_DESCRIPTION, + LINK_ENTITY_DESCRIPTION, + OVERLAY_ENTITY_DESCRIPTION, + OPEN_WINDOW_ENTITY_DESCRIPTION, + ], + TYPE_HOT_WATER: [ + POWER_ENTITY_DESCRIPTION, + LINK_ENTITY_DESCRIPTION, + OVERLAY_ENTITY_DESCRIPTION, + ], + }, } @@ -125,6 +159,7 @@ async def async_setup_entry( devices = tado.devices zones = tado.zones entities: list[BinarySensorEntity] = [] + tado_line = TADO_LINE_X if tado.is_x else TADO_PRE_LINE_X # Create device sensors for device in devices: @@ -136,21 +171,25 @@ async def async_setup_entry( entities.extend( [ TadoDeviceBinarySensor(tado, device, entity_description) - for entity_description in DEVICE_SENSORS[device_type] + for entity_description in DEVICE_SENSORS[tado_line][device_type] ] ) # Create zone sensors for zone in zones: zone_type = zone["type"] - if zone_type not in ZONE_SENSORS: - _LOGGER.warning("Unknown zone type skipped: %s", zone_type) + if zone_type not in ZONE_SENSORS[tado_line]: + _LOGGER.warning( + "Unknown or unsupported zone type skipped: %s, tado line: %s", + zone_type, + tado_line, + ) continue entities.extend( [ TadoZoneBinarySensor(tado, zone["name"], zone["id"], entity_description) - for entity_description in ZONE_SENSORS[zone_type] + for entity_description in ZONE_SENSORS[tado_line][zone_type] ] ) diff --git a/homeassistant/components/tado/climate.py b/homeassistant/components/tado/climate.py index 21a09086d4685..c23efaa21b1cf 100644 --- a/homeassistant/components/tado/climate.py +++ b/homeassistant/components/tado/climate.py @@ -23,7 +23,12 @@ HVACAction, HVACMode, ) -from homeassistant.const import ATTR_TEMPERATURE, PRECISION_TENTHS, UnitOfTemperature +from homeassistant.const import ( + ATTR_TEMPERATURE, + PRECISION_HALVES, + PRECISION_TENTHS, + UnitOfTemperature, +) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -69,6 +74,8 @@ TADO_TO_HA_OFFSET_MAP, TADO_TO_HA_SWING_MODE_MAP, TADO_VERTICAL_SWING_SETTING, + TADO_X_DEFAULT_MAX_TEMP, + TADO_X_DEFAULT_MIN_TEMP, TEMP_OFFSET, TYPE_AIR_CONDITIONING, TYPE_HEATING, @@ -222,16 +229,21 @@ def create_climate_entity( if heat_temperatures is None and "temperatures" in capabilities: heat_temperatures = capabilities["temperatures"] - if cool_temperatures is None and heat_temperatures is None: + if cool_temperatures is None and heat_temperatures is None and not tado.is_x: _LOGGER.debug("Not adding zone %s since it has no temperatures", name) return None - heat_min_temp = None - heat_max_temp = None - heat_step = None - cool_min_temp = None - cool_max_temp = None - cool_step = None + 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 + + if tado.is_x: + heat_min_temp = TADO_X_DEFAULT_MIN_TEMP + heat_max_temp = TADO_X_DEFAULT_MAX_TEMP + heat_step = PRECISION_HALVES if heat_temperatures is not None: heat_min_temp = float(heat_temperatures["celsius"]["min"]) @@ -299,7 +311,11 @@ def __init__( self._attr_unique_id = f"{zone_type} {zone_id} {tado.home_id}" self._device_info = device_info - self._device_id = self._device_info["shortSerialNo"] + self._device_id = ( + self._device_info["serialNumber"] + if self._tado.is_x + else self._device_info["shortSerialNo"] + ) self._ac_device = zone_type == TYPE_AIR_CONDITIONING self._attr_hvac_modes = supported_hvac_modes @@ -419,6 +435,10 @@ def preset_mode(self) -> str: ): if not self._tado_geofence_data["presenceLocked"]: return PRESET_AUTO + if self._tado.is_x and self._tado_geofence_data["presence"] == "HOME": + return PRESET_HOME + if self._tado.is_x and self._tado_geofence_data["presence"] == "AWAY": + return PRESET_AWAY if self._tado_zone_data.is_away: return PRESET_AWAY return PRESET_HOME @@ -603,16 +623,21 @@ def _async_update_zone_data(self) -> None: """Load tado data into zone.""" self._tado_zone_data = self._tado.data["zone"][self.zone_id] - # Assign offset values to mapped attributes - for offset_key, attr in TADO_TO_HA_OFFSET_MAP.items(): - if ( - self._device_id in self._tado.data["device"] - and offset_key - in self._tado.data["device"][self._device_id][TEMP_OFFSET] - ): - self._tado_zone_temp_offset[attr] = self._tado.data["device"][ - self._device_id - ][TEMP_OFFSET][offset_key] + if self._tado.is_x: + self._tado_zone_temp_offset["offsent_celsius"] = self._tado.data["device"][ + self._device_id + ][TEMP_OFFSET] + else: + # Assign offset values to mapped attributes + for offset_key, attr in TADO_TO_HA_OFFSET_MAP.items(): + if ( + self._device_id in self._tado.data["device"] + and offset_key + in self._tado.data["device"][self._device_id][TEMP_OFFSET] + ): + self._tado_zone_temp_offset[attr] = self._tado.data["device"][ + self._device_id + ][TEMP_OFFSET][offset_key] self._current_tado_hvac_mode = self._tado_zone_data.current_hvac_mode self._current_tado_hvac_action = self._tado_zone_data.current_hvac_action diff --git a/homeassistant/components/tado/const.py b/homeassistant/components/tado/const.py index bdc4bff1943b2..44546a56576d5 100644 --- a/homeassistant/components/tado/const.py +++ b/homeassistant/components/tado/const.py @@ -223,6 +223,10 @@ TADO_DEFAULT_MIN_TEMP = 5 TADO_DEFAULT_MAX_TEMP = 40 + +TADO_X_DEFAULT_MIN_TEMP = 5 +TADO_X_DEFAULT_MAX_TEMP = 30 + # Constants for service calls SERVICE_ADD_METER_READING = "add_meter_reading" CONF_CONFIG_ENTRY = "config_entry" @@ -237,3 +241,6 @@ TADO_FANLEVEL_SETTING = "fanLevel" TADO_VERTICAL_SWING_SETTING = "verticalSwing" TADO_HORIZONTAL_SWING_SETTING = "horizontalSwing" + +TADO_LINE_X = "is_x" +TADO_PRE_LINE_X = "is_pre_x" diff --git a/homeassistant/components/tado/entity.py b/homeassistant/components/tado/entity.py index 6bb90ab849ae9..a269f2cdbe2bd 100644 --- a/homeassistant/components/tado/entity.py +++ b/homeassistant/components/tado/entity.py @@ -17,17 +17,33 @@ def __init__(self, device_info: dict[str, str]) -> None: """Initialize a Tado device.""" super().__init__() self._device_info = device_info - self.device_name = device_info["serialNo"] - self.device_id = device_info["shortSerialNo"] - self._attr_device_info = DeviceInfo( - configuration_url=f"https://app.tado.com/en/main/settings/rooms-and-devices/device/{self.device_name}", - identifiers={(DOMAIN, self.device_id)}, - name=self.device_name, - manufacturer=DEFAULT_NAME, - sw_version=device_info["currentFwVersion"], - model=device_info["deviceType"], - via_device=(DOMAIN, device_info["serialNo"]), - ) + if device_info["is_x"]: + self.device_name = device_info["serialNumber"] + self.device_id = device_info["serialNumber"] + self._attr_device_info = DeviceInfo( + configuration_url=f"https://app.tado.com/en/main/settings/home/rooms-and-devices/device/{self.device_name}", + identifiers={(DOMAIN, self.device_id)}, + name=self.device_name, + manufacturer=DEFAULT_NAME, + sw_version=device_info["firmwareVersion"], + model=device_info["type"], + via_device=(DOMAIN, device_info["serialNumber"]), + ) + else: + self.device_name = device_info["serialNo"] + self.device_id = device_info["shortSerialNo"] + self._attr_device_info = DeviceInfo( + configuration_url=f"https://app.tado.com/en/main/settings/rooms-and-devices/device/{self.device_name}", + identifiers={(DOMAIN, self.device_id)}, + name=self.device_name, + manufacturer=DEFAULT_NAME, + sw_version=device_info["currentFwVersion"], + model=device_info["deviceType"], + via_device=( + DOMAIN, + device_info["serialNo"], + ), + ) class TadoHomeEntity(Entity): diff --git a/homeassistant/components/tado/manifest.json b/homeassistant/components/tado/manifest.json index 652d51f02619b..9b7e7c00ac566 100644 --- a/homeassistant/components/tado/manifest.json +++ b/homeassistant/components/tado/manifest.json @@ -1,7 +1,7 @@ { "domain": "tado", "name": "Tado", - "codeowners": ["@chiefdragon", "@erwindouna"], + "codeowners": ["@chiefdragon", "@erwindouna", "@Moritz-Schmidt"], "config_flow": true, "dhcp": [ { diff --git a/homeassistant/components/tado/sensor.py b/homeassistant/components/tado/sensor.py index 8bb13a02cd180..a6a88c40851fa 100644 --- a/homeassistant/components/tado/sensor.py +++ b/homeassistant/components/tado/sensor.py @@ -25,6 +25,8 @@ SENSOR_DATA_CATEGORY_GEOFENCE, SENSOR_DATA_CATEGORY_WEATHER, SIGNAL_TADO_UPDATE_RECEIVED, + TADO_LINE_X, + TADO_PRE_LINE_X, TYPE_AIR_CONDITIONING, TYPE_HEATING, TYPE_HOT_WATER, @@ -176,19 +178,29 @@ def get_geofencing_mode(data: dict[str, str]) -> str: ) ZONE_SENSORS = { - TYPE_HEATING: [ - TEMPERATURE_ENTITY_DESCRIPTION, - HUMIDITY_ENTITY_DESCRIPTION, - TADO_MODE_ENTITY_DESCRIPTION, - HEATING_ENTITY_DESCRIPTION, - ], - TYPE_AIR_CONDITIONING: [ - TEMPERATURE_ENTITY_DESCRIPTION, - HUMIDITY_ENTITY_DESCRIPTION, - TADO_MODE_ENTITY_DESCRIPTION, - AC_ENTITY_DESCRIPTION, - ], - TYPE_HOT_WATER: [TADO_MODE_ENTITY_DESCRIPTION], + TADO_LINE_X: { + TYPE_HEATING: [ + TEMPERATURE_ENTITY_DESCRIPTION, + HUMIDITY_ENTITY_DESCRIPTION, + HEATING_ENTITY_DESCRIPTION, + ], + TYPE_HOT_WATER: [TADO_MODE_ENTITY_DESCRIPTION], + }, + TADO_PRE_LINE_X: { + TYPE_HEATING: [ + TEMPERATURE_ENTITY_DESCRIPTION, + HUMIDITY_ENTITY_DESCRIPTION, + TADO_MODE_ENTITY_DESCRIPTION, + HEATING_ENTITY_DESCRIPTION, + ], + TYPE_AIR_CONDITIONING: [ + TEMPERATURE_ENTITY_DESCRIPTION, + HUMIDITY_ENTITY_DESCRIPTION, + TADO_MODE_ENTITY_DESCRIPTION, + AC_ENTITY_DESCRIPTION, + ], + TYPE_HOT_WATER: [TADO_MODE_ENTITY_DESCRIPTION], + }, } @@ -200,6 +212,7 @@ async def async_setup_entry( tado = entry.runtime_data zones = tado.zones entities: list[SensorEntity] = [] + tado_line = TADO_LINE_X if tado.is_x else TADO_PRE_LINE_X # Create home sensors entities.extend( @@ -212,14 +225,18 @@ async def async_setup_entry( # Create zone sensors for zone in zones: zone_type = zone["type"] - if zone_type not in ZONE_SENSORS: - _LOGGER.warning("Unknown zone type skipped: %s", zone_type) + if zone_type not in ZONE_SENSORS[tado_line]: + _LOGGER.warning( + "Unknown or unsupported zone type skipped: %s, tado line: %s", + zone_type, + tado_line, + ) continue entities.extend( [ TadoZoneSensor(tado, zone["name"], zone["id"], entity_description) - for entity_description in ZONE_SENSORS[zone_type] + for entity_description in ZONE_SENSORS[tado_line][zone_type] ] ) diff --git a/homeassistant/components/tado/tado_connector.py b/homeassistant/components/tado/tado_connector.py index 5ed5367515318..93d18afb7db44 100644 --- a/homeassistant/components/tado/tado_connector.py +++ b/homeassistant/components/tado/tado_connector.py @@ -19,6 +19,7 @@ SIGNAL_TADO_MOBILE_DEVICE_UPDATE_RECEIVED, SIGNAL_TADO_UPDATE_RECEIVED, TEMP_OFFSET, + TYPE_HEATING, ) MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=4) @@ -53,6 +54,7 @@ def __init__( "geofence": {}, "zone": {}, } + self.is_x = False @property def fallback(self): @@ -68,6 +70,13 @@ def setup(self): tado_home = self.tado.get_me()["homes"][0] self.home_id = tado_home["id"] self.home_name = tado_home["name"] + self.is_x = self.tado.http.isX + [device.update(is_x=self.is_x) for device in self.devices] + if self.is_x: + for z in self.zones: + z["type"] = TYPE_HEATING + z["name"] = z["roomName"] + z["id"] = z["roomId"] def get_mobile_devices(self): """Return the Tado mobile devices.""" @@ -139,22 +148,29 @@ def update_devices(self): return for device in devices: - device_short_serial_no = device["shortSerialNo"] + if self.is_x: + device_short_serial_no = device["serialNumber"] + else: + device_short_serial_no = device["shortSerialNo"] _LOGGER.debug("Updating device %s", device_short_serial_no) - try: - if ( - INSIDE_TEMPERATURE_MEASUREMENT - in device["characteristics"]["capabilities"] - ): - device[TEMP_OFFSET] = self.tado.get_device_info( - device_short_serial_no, TEMP_OFFSET + + if self.is_x: + device[TEMP_OFFSET] = device["temperatureOffset"] + else: + try: + if ( + INSIDE_TEMPERATURE_MEASUREMENT + in device["characteristics"]["capabilities"] + ): + device[TEMP_OFFSET] = self.tado.get_device_info( + device_short_serial_no, TEMP_OFFSET + ) + except RuntimeError: + _LOGGER.error( + "Unable to connect to Tado while updating device %s", + device_short_serial_no, ) - except RuntimeError: - _LOGGER.error( - "Unable to connect to Tado while updating device %s", - device_short_serial_no, - ) - return + return self.data["device"][device_short_serial_no] = device @@ -174,13 +190,16 @@ def update_devices(self): def update_zones(self): """Update the zone data from Tado.""" try: - zone_states = self.tado.get_zone_states()["zoneStates"] + zone_states = self.tado.get_zone_states() except RuntimeError: _LOGGER.error("Unable to connect to Tado while updating zones") return + if not self.is_x: + zone_states = zone_states["zoneStates"] + for zone in zone_states: - self.update_zone(int(zone)) + self.update_zone(int(zone if not self.is_x else zone["id"])) def update_zone(self, zone_id): """Update the internal data from Tado.""" @@ -221,6 +240,8 @@ def update_home(self): def get_capabilities(self, zone_id): """Return the capabilities of the devices.""" + if self.is_x: + return {"type": TYPE_HEATING} return self.tado.get_capabilities(zone_id) def get_auto_geofencing_supported(self): @@ -286,7 +307,7 @@ def set_zone_overlay( zone_id, overlay_mode, temperature, - duration, + int(duration) if duration else None, device_type, "ON", mode,