Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for TadoX thermostatic radiator valves #129600

Closed
wants to merge 23 commits into from
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 24 additions & 8 deletions homeassistant/components/tado/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,9 @@ class TadoBinarySensorEntityDescription(BinarySensorEntityDescription):
CONNECTION_STATE_ENTITY_DESCRIPTION = TadoBinarySensorEntityDescription(
key="connection state",
translation_key="connection_state",
state_fn=lambda data: data.get("connectionState", {}).get("value", False),
state_fn=lambda data: data.get("connectionState", {}).get(
Moritz-Schmidt marked this conversation as resolved.
Show resolved Hide resolved
"value", data.get("connection", {}).get("state", False)
),
device_class=BinarySensorDeviceClass.CONNECTIVITY,
)
POWER_ENTITY_DESCRIPTION = TadoBinarySensorEntityDescription(
Expand All @@ -59,7 +61,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(
Expand Down Expand Up @@ -147,12 +149,26 @@ async def async_setup_entry(
_LOGGER.warning("Unknown zone type skipped: %s", zone_type)
continue

entities.extend(
[
TadoZoneBinarySensor(tado, zone["name"], zone["id"], entity_description)
for entity_description in ZONE_SENSORS[zone_type]
]
)
if tado.isX:
entities.extend(
[
TadoZoneBinarySensor(
tado, zone["name"], zone["id"], entity_description
)
for entity_description in ZONE_SENSORS[zone_type]
if entity_description.key
!= "early start" # early start is not available for TadoX
Moritz-Schmidt marked this conversation as resolved.
Show resolved Hide resolved
]
)
else:
entities.extend(
[
TadoZoneBinarySensor(
tado, zone["name"], zone["id"], entity_description
)
for entity_description in ZONE_SENSORS[zone_type]
]
)

async_add_entities(entities, True)

Expand Down
57 changes: 42 additions & 15 deletions homeassistant/components/tado/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -142,7 +147,10 @@ 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)
if tado.isX:
capabilities = {"type": TYPE_HEATING}
else:
capabilities = tado.get_capabilities(zone_id)
_LOGGER.debug("Capabilities for zone %s: %s", zone_id, capabilities)

zone_type = capabilities["type"]
Expand Down Expand Up @@ -222,7 +230,7 @@ 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.isX:
_LOGGER.debug("Not adding zone %s since it has no temperatures", name)
return None

Expand All @@ -233,6 +241,11 @@ def create_climate_entity(
cool_max_temp = None
cool_step = None

if tado.isX:
heat_min_temp = 5.0
heat_max_temp = 30.0
Moritz-Schmidt marked this conversation as resolved.
Show resolved Hide resolved
heat_step = PRECISION_HALVES

if heat_temperatures is not None:
heat_min_temp = float(heat_temperatures["celsius"]["min"])
heat_max_temp = float(heat_temperatures["celsius"]["max"])
Expand Down Expand Up @@ -299,7 +312,9 @@ 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.get(
"shortSerialNo", self._device_info.get("serialNumber")
Moritz-Schmidt marked this conversation as resolved.
Show resolved Hide resolved
)

self._ac_device = zone_type == TYPE_AIR_CONDITIONING
self._attr_hvac_modes = supported_hvac_modes
Expand Down Expand Up @@ -328,7 +343,10 @@ def __init__(
self._current_tado_vertical_swing = TADO_SWING_OFF
self._current_tado_horizontal_swing = TADO_SWING_OFF

capabilities = tado.get_capabilities(zone_id)
if tado.isX:
capabilities = {"type": TYPE_HEATING}
else:
capabilities = tado.get_capabilities(zone_id)
self._current_tado_capabilities = capabilities

self._tado_zone_data: PyTado.TadoZone = {}
Expand Down Expand Up @@ -419,6 +437,10 @@ def preset_mode(self) -> str:
):
if not self._tado_geofence_data["presenceLocked"]:
return PRESET_AUTO
if self._tado.isX and self._tado_geofence_data["presence"] == "HOME":
return PRESET_HOME
if self._tado.isX and self._tado_geofence_data["presence"] == "AWAY":
return PRESET_AWAY
if self._tado_zone_data.is_away:
return PRESET_AWAY
return PRESET_HOME
Expand Down Expand Up @@ -603,16 +625,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.isX:
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
Expand Down
17 changes: 12 additions & 5 deletions homeassistant/components/tado/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,23 @@ 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.device_name = device_info.get("serialNo", device_info.get("serialNumber"))
Moritz-Schmidt marked this conversation as resolved.
Show resolved Hide resolved
self.device_id = device_info.get(
"shortSerialNo", device_info.get("serialNumber")
)
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"]),
sw_version=device_info.get(
"currentFwVersion", device_info.get("firmwareVersion")
),
Moritz-Schmidt marked this conversation as resolved.
Show resolved Hide resolved
model=device_info.get("deviceType", device_info.get("type")),
via_device=(
DOMAIN,
device_info.get("serialNo", device_info.get("serialNumber")),
),
)


Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/tado/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,5 @@
},
"iot_class": "cloud_polling",
"loggers": ["PyTado"],
"requirements": ["python-tado==0.17.6"]
"requirements": ["python-tado==0.17.7"]
Moritz-Schmidt marked this conversation as resolved.
Show resolved Hide resolved
}
22 changes: 16 additions & 6 deletions homeassistant/components/tado/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,12 +216,22 @@ async def async_setup_entry(
_LOGGER.warning("Unknown zone type skipped: %s", zone_type)
continue

entities.extend(
[
TadoZoneSensor(tado, zone["name"], zone["id"], entity_description)
for entity_description in ZONE_SENSORS[zone_type]
]
)
if tado.isX:
entities.extend(
[
TadoZoneSensor(tado, zone["name"], zone["id"], entity_description)
for entity_description in ZONE_SENSORS[zone_type]
Moritz-Schmidt marked this conversation as resolved.
Show resolved Hide resolved
if entity_description.key
!= "tado mode" # tado mode is not available for TadoX
]
)
else:
entities.extend(
[
TadoZoneSensor(tado, zone["name"], zone["id"], entity_description)
for entity_description in ZONE_SENSORS[zone_type]
]
)

async_add_entities(entities, True)

Expand Down
52 changes: 35 additions & 17 deletions homeassistant/components/tado/tado_connector.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -53,6 +54,7 @@ def __init__(
"geofence": {},
"zone": {},
}
self.isX = False

@property
def fallback(self):
Expand All @@ -68,6 +70,12 @@ def setup(self):
tado_home = self.tado.get_me()["homes"][0]
self.home_id = tado_home["id"]
self.home_name = tado_home["name"]
self.isX = self.tado.http.isX
Moritz-Schmidt marked this conversation as resolved.
Show resolved Hide resolved
if self.isX:
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."""
Expand Down Expand Up @@ -139,22 +147,29 @@ def update_devices(self):
return

for device in devices:
device_short_serial_no = device["shortSerialNo"]
if self.isX:
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.isX:
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

Expand All @@ -174,13 +189,16 @@ def update_devices(self):
def update_zones(self):
"""Update the zone data from Tado."""
try:
zone_states = self.tado.get_zone_states()["zoneStates"]
if self.isX:
zone_states = self.tado.get_zone_states()
else:
zone_states = self.tado.get_zone_states()["zoneStates"]
except RuntimeError:
_LOGGER.error("Unable to connect to Tado while updating zones")
return

for zone in zone_states:
self.update_zone(int(zone))
self.update_zone(int(zone if not self.isX else zone["id"]))

def update_zone(self, zone_id):
"""Update the internal data from Tado."""
Expand Down Expand Up @@ -286,7 +304,7 @@ def set_zone_overlay(
zone_id,
overlay_mode,
temperature,
duration,
int(duration) if duration else None,
device_type,
"ON",
mode,
Expand Down