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 all 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
4 changes: 2 additions & 2 deletions CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
99 changes: 69 additions & 30 deletions homeassistant/components/tado/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -52,14 +54,20 @@ 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",
device_class=BinarySensorDeviceClass.POWER,
)
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 All @@ -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,
],
},
}


Expand All @@ -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:
Expand All @@ -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]
]
)

Expand Down
63 changes: 44 additions & 19 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 @@ -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,
Expand Down Expand Up @@ -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:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of having a lot of these if statements, can we have separate classes for it?

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"])
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Comment on lines +438 to +441
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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.is_x:
if self._tado_geofence_data["presence"] == "HOME":
return PRESET_HOME
if 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 +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
Expand Down
7 changes: 7 additions & 0 deletions homeassistant/components/tado/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
38 changes: 27 additions & 11 deletions homeassistant/components/tado/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
),
Comment on lines +40 to +45
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To deduplicate code please create local variables for these attributes and have a single DeviceInfo

)


class TadoHomeEntity(Entity):
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/tado/manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"domain": "tado",
"name": "Tado",
"codeowners": ["@chiefdragon", "@erwindouna"],
"codeowners": ["@chiefdragon", "@erwindouna", "@Moritz-Schmidt"],
Moritz-Schmidt marked this conversation as resolved.
Show resolved Hide resolved
"config_flow": true,
"dhcp": [
{
Expand Down
Loading