Add select for heating circuit to Tado zones (#147902)

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
This commit is contained in:
Luuk Dobber 2025-07-21 13:45:57 +02:00 committed by GitHub
parent d774de79db
commit bc0162cf85
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 927 additions and 3 deletions

View File

@ -41,6 +41,7 @@ PLATFORMS = [
Platform.BINARY_SENSOR,
Platform.CLIMATE,
Platform.DEVICE_TRACKER,
Platform.SELECT,
Platform.SENSOR,
Platform.SWITCH,
Platform.WATER_HEATER,

View File

@ -73,6 +73,8 @@ class TadoDataUpdateCoordinator(DataUpdateCoordinator[dict[str, dict]]):
"weather": {},
"geofence": {},
"zone": {},
"zone_control": {},
"heating_circuits": {},
}
@property
@ -99,11 +101,14 @@ class TadoDataUpdateCoordinator(DataUpdateCoordinator[dict[str, dict]]):
self.home_name = tado_home["name"]
devices = await self._async_update_devices()
zones = await self._async_update_zones()
zones, zone_controls = await self._async_update_zones()
home = await self._async_update_home()
heating_circuits = await self._async_update_heating_circuits()
self.data["device"] = devices
self.data["zone"] = zones
self.data["zone_control"] = zone_controls
self.data["heating_circuits"] = heating_circuits
self.data["weather"] = home["weather"]
self.data["geofence"] = home["geofence"]
@ -166,7 +171,7 @@ class TadoDataUpdateCoordinator(DataUpdateCoordinator[dict[str, dict]]):
return mapped_devices
async def _async_update_zones(self) -> dict[int, dict]:
async def _async_update_zones(self) -> tuple[dict[int, dict], dict[int, dict]]:
"""Update the zone data from Tado."""
try:
@ -179,10 +184,12 @@ class TadoDataUpdateCoordinator(DataUpdateCoordinator[dict[str, dict]]):
raise UpdateFailed(f"Error updating Tado zones: {err}") from err
mapped_zones: dict[int, dict] = {}
mapped_zone_controls: dict[int, dict] = {}
for zone in zone_states:
mapped_zones[int(zone)] = await self._update_zone(int(zone))
mapped_zone_controls[int(zone)] = await self._update_zone_control(int(zone))
return mapped_zones
return mapped_zones, mapped_zone_controls
async def _update_zone(self, zone_id: int) -> dict[str, str]:
"""Update the internal data of a zone."""
@ -199,6 +206,24 @@ class TadoDataUpdateCoordinator(DataUpdateCoordinator[dict[str, dict]]):
_LOGGER.debug("Zone %s updated, with data: %s", zone_id, data)
return data
async def _update_zone_control(self, zone_id: int) -> dict[str, Any]:
"""Update the internal zone control data of a zone."""
_LOGGER.debug("Updating zone control for zone %s", zone_id)
try:
zone_control_data = await self.hass.async_add_executor_job(
self._tado.get_zone_control, zone_id
)
except RequestException as err:
_LOGGER.error(
"Error updating Tado zone control for zone %s: %s", zone_id, err
)
raise UpdateFailed(
f"Error updating Tado zone control for zone {zone_id}: {err}"
) from err
return zone_control_data
async def _async_update_home(self) -> dict[str, dict]:
"""Update the home data from Tado."""
@ -217,6 +242,23 @@ class TadoDataUpdateCoordinator(DataUpdateCoordinator[dict[str, dict]]):
return {"weather": weather, "geofence": geofence}
async def _async_update_heating_circuits(self) -> dict[str, dict]:
"""Update the heating circuits data from Tado."""
try:
heating_circuits = await self.hass.async_add_executor_job(
self._tado.get_heating_circuits
)
except RequestException as err:
_LOGGER.error("Error updating Tado heating circuits: %s", err)
raise UpdateFailed(f"Error updating Tado heating circuits: {err}") from err
mapped_heating_circuits: dict[str, dict] = {}
for circuit in heating_circuits:
mapped_heating_circuits[circuit["driverShortSerialNo"]] = circuit
return mapped_heating_circuits
async def get_capabilities(self, zone_id: int | str) -> dict:
"""Fetch the capabilities from Tado."""
@ -364,6 +406,20 @@ class TadoDataUpdateCoordinator(DataUpdateCoordinator[dict[str, dict]]):
except RequestException as exc:
raise HomeAssistantError(f"Error setting Tado child lock: {exc}") from exc
async def set_heating_circuit(self, zone_id: int, circuit_id: int | None) -> None:
"""Set heating circuit for zone."""
try:
await self.hass.async_add_executor_job(
self._tado.set_zone_heating_circuit,
zone_id,
circuit_id,
)
except RequestException as exc:
raise HomeAssistantError(
f"Error setting Tado heating circuit: {exc}"
) from exc
await self._update_zone_control(zone_id)
class TadoMobileDeviceUpdateCoordinator(DataUpdateCoordinator[dict[str, dict]]):
"""Class to manage the mobile devices from Tado via PyTado."""

View File

@ -0,0 +1,108 @@
"""Module for Tado select entities."""
import logging
from homeassistant.components.select import SelectEntity
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import TadoConfigEntry
from .entity import TadoDataUpdateCoordinator, TadoZoneEntity
_LOGGER = logging.getLogger(__name__)
NO_HEATING_CIRCUIT_OPTION = "no_heating_circuit"
async def async_setup_entry(
hass: HomeAssistant,
entry: TadoConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Tado select platform."""
tado = entry.runtime_data.coordinator
entities: list[SelectEntity] = [
TadoHeatingCircuitSelectEntity(tado, zone["name"], zone["id"])
for zone in tado.zones
if zone["type"] == "HEATING"
]
async_add_entities(entities, True)
class TadoHeatingCircuitSelectEntity(TadoZoneEntity, SelectEntity):
"""Representation of a Tado heating circuit select entity."""
_attr_entity_category = EntityCategory.CONFIG
_attr_has_entity_name = True
_attr_icon = "mdi:water-boiler"
_attr_translation_key = "heating_circuit"
def __init__(
self,
coordinator: TadoDataUpdateCoordinator,
zone_name: str,
zone_id: int,
) -> None:
"""Initialize the Tado heating circuit select entity."""
super().__init__(zone_name, coordinator.home_id, zone_id, coordinator)
self._attr_unique_id = f"{zone_id} {coordinator.home_id} heating_circuit"
self._attr_options = []
self._attr_current_option = None
async def async_select_option(self, option: str) -> None:
"""Update the selected heating circuit."""
heating_circuit_id = (
None
if option == NO_HEATING_CIRCUIT_OPTION
else self.coordinator.data["heating_circuits"].get(option, {}).get("number")
)
await self.coordinator.set_heating_circuit(self.zone_id, heating_circuit_id)
await self.coordinator.async_request_refresh()
@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
self._async_update_callback()
super()._handle_coordinator_update()
@callback
def _async_update_callback(self) -> None:
"""Handle update callbacks."""
# Heating circuits list
heating_circuits = self.coordinator.data["heating_circuits"].values()
self._attr_options = [NO_HEATING_CIRCUIT_OPTION]
self._attr_options.extend(hc["driverShortSerialNo"] for hc in heating_circuits)
# Current heating circuit
zone_control = self.coordinator.data["zone_control"].get(self.zone_id)
if zone_control and "heatingCircuit" in zone_control:
heating_circuit_number = zone_control["heatingCircuit"]
if heating_circuit_number is None:
self._attr_current_option = NO_HEATING_CIRCUIT_OPTION
else:
# Find heating circuit by number
heating_circuit = next(
(
hc
for hc in heating_circuits
if hc.get("number") == heating_circuit_number
),
None,
)
if heating_circuit is None:
_LOGGER.error(
"Heating circuit with number %s not found for zone %s",
heating_circuit_number,
self.zone_name,
)
self._attr_current_option = NO_HEATING_CIRCUIT_OPTION
else:
self._attr_current_option = heating_circuit.get(
"driverShortSerialNo"
)

View File

@ -59,6 +59,14 @@
}
}
},
"select": {
"heating_circuit": {
"name": "Heating circuit",
"state": {
"no_heating_circuit": "No circuit"
}
}
},
"switch": {
"child_lock": {
"name": "Child lock"

View File

@ -0,0 +1,7 @@
[
{
"number": 1,
"driverSerialNo": "RU1234567890",
"driverShortSerialNo": "RU1234567890"
}
]

View File

@ -0,0 +1,80 @@
{
"type": "HEATING",
"earlyStartEnabled": false,
"heatingCircuit": 1,
"duties": {
"type": "HEATING",
"leader": {
"deviceType": "RU01",
"serialNo": "RU1234567890",
"shortSerialNo": "RU1234567890",
"currentFwVersion": "54.20",
"connectionState": {
"value": true,
"timestamp": "2025-06-30T19:53:40.710Z"
},
"characteristics": {
"capabilities": ["INSIDE_TEMPERATURE_MEASUREMENT", "IDENTIFY"]
},
"batteryState": "NORMAL"
},
"drivers": [
{
"deviceType": "VA01",
"serialNo": "VA1234567890",
"shortSerialNo": "VA1234567890",
"currentFwVersion": "54.20",
"connectionState": {
"value": true,
"timestamp": "2025-06-30T19:54:15.166Z"
},
"characteristics": {
"capabilities": ["INSIDE_TEMPERATURE_MEASUREMENT", "IDENTIFY"]
},
"mountingState": {
"value": "CALIBRATED",
"timestamp": "2025-06-09T23:25:12.678Z"
},
"mountingStateWithError": "CALIBRATED",
"batteryState": "LOW",
"childLockEnabled": false
}
],
"uis": [
{
"deviceType": "RU01",
"serialNo": "RU1234567890",
"shortSerialNo": "RU1234567890",
"currentFwVersion": "54.20",
"connectionState": {
"value": true,
"timestamp": "2025-06-30T19:53:40.710Z"
},
"characteristics": {
"capabilities": ["INSIDE_TEMPERATURE_MEASUREMENT", "IDENTIFY"]
},
"batteryState": "NORMAL"
},
{
"deviceType": "VA01",
"serialNo": "VA1234567890",
"shortSerialNo": "VA1234567890",
"currentFwVersion": "54.20",
"connectionState": {
"value": true,
"timestamp": "2025-06-30T19:54:15.166Z"
},
"characteristics": {
"capabilities": ["INSIDE_TEMPERATURE_MEASUREMENT", "IDENTIFY"]
},
"mountingState": {
"value": "CALIBRATED",
"timestamp": "2025-06-09T23:25:12.678Z"
},
"mountingStateWithError": "CALIBRATED",
"batteryState": "LOW",
"childLockEnabled": false
}
]
}
}

View File

@ -62,6 +62,13 @@
'presence': 'HOME',
'presenceLocked': False,
}),
'heating_circuits': dict({
'RU1234567890': dict({
'driverSerialNo': 'RU1234567890',
'driverShortSerialNo': 'RU1234567890',
'number': 1,
}),
}),
'weather': dict({
'outsideTemperature': dict({
'celsius': 7.46,
@ -110,6 +117,560 @@
'repr': "TadoZone(zone_id=6, current_temp=24.3, connection=None, current_temp_timestamp='2024-06-28T22: 23: 15.679Z', current_humidity=70.9, current_humidity_timestamp='2024-06-28T22: 23: 15.679Z', is_away=False, current_hvac_action='HEATING', current_fan_speed='AUTO', current_fan_level='LEVEL3', current_hvac_mode='HEAT', current_swing_mode='OFF', current_vertical_swing_mode='ON', current_horizontal_swing_mode='ON', target_temp=25.0, available=True, power='ON', link='ONLINE', ac_power_timestamp='2022-07-13T18: 06: 58.183Z', heating_power_timestamp=None, ac_power='ON', heating_power=None, heating_power_percentage=None, tado_mode='HOME', overlay_termination_type='MANUAL', overlay_termination_timestamp=None, default_overlay_termination_type='MANUAL', default_overlay_termination_duration=None, preparation=False, open_window=False, open_window_detected=False, open_window_attr={}, precision=0.1)",
}),
}),
'zone_control': dict({
'1': dict({
'duties': dict({
'drivers': list([
dict({
'batteryState': 'LOW',
'characteristics': dict({
'capabilities': list([
'INSIDE_TEMPERATURE_MEASUREMENT',
'IDENTIFY',
]),
}),
'childLockEnabled': False,
'connectionState': dict({
'timestamp': '2025-06-30T19:54:15.166Z',
'value': True,
}),
'currentFwVersion': '54.20',
'deviceType': 'VA01',
'mountingState': dict({
'timestamp': '2025-06-09T23:25:12.678Z',
'value': 'CALIBRATED',
}),
'mountingStateWithError': 'CALIBRATED',
'serialNo': 'VA1234567890',
'shortSerialNo': 'VA1234567890',
}),
]),
'leader': dict({
'batteryState': 'NORMAL',
'characteristics': dict({
'capabilities': list([
'INSIDE_TEMPERATURE_MEASUREMENT',
'IDENTIFY',
]),
}),
'connectionState': dict({
'timestamp': '2025-06-30T19:53:40.710Z',
'value': True,
}),
'currentFwVersion': '54.20',
'deviceType': 'RU01',
'serialNo': 'RU1234567890',
'shortSerialNo': 'RU1234567890',
}),
'type': 'HEATING',
'uis': list([
dict({
'batteryState': 'NORMAL',
'characteristics': dict({
'capabilities': list([
'INSIDE_TEMPERATURE_MEASUREMENT',
'IDENTIFY',
]),
}),
'connectionState': dict({
'timestamp': '2025-06-30T19:53:40.710Z',
'value': True,
}),
'currentFwVersion': '54.20',
'deviceType': 'RU01',
'serialNo': 'RU1234567890',
'shortSerialNo': 'RU1234567890',
}),
dict({
'batteryState': 'LOW',
'characteristics': dict({
'capabilities': list([
'INSIDE_TEMPERATURE_MEASUREMENT',
'IDENTIFY',
]),
}),
'childLockEnabled': False,
'connectionState': dict({
'timestamp': '2025-06-30T19:54:15.166Z',
'value': True,
}),
'currentFwVersion': '54.20',
'deviceType': 'VA01',
'mountingState': dict({
'timestamp': '2025-06-09T23:25:12.678Z',
'value': 'CALIBRATED',
}),
'mountingStateWithError': 'CALIBRATED',
'serialNo': 'VA1234567890',
'shortSerialNo': 'VA1234567890',
}),
]),
}),
'earlyStartEnabled': False,
'heatingCircuit': 1,
'type': 'HEATING',
}),
'2': dict({
'duties': dict({
'drivers': list([
dict({
'batteryState': 'LOW',
'characteristics': dict({
'capabilities': list([
'INSIDE_TEMPERATURE_MEASUREMENT',
'IDENTIFY',
]),
}),
'childLockEnabled': False,
'connectionState': dict({
'timestamp': '2025-06-30T19:54:15.166Z',
'value': True,
}),
'currentFwVersion': '54.20',
'deviceType': 'VA01',
'mountingState': dict({
'timestamp': '2025-06-09T23:25:12.678Z',
'value': 'CALIBRATED',
}),
'mountingStateWithError': 'CALIBRATED',
'serialNo': 'VA1234567890',
'shortSerialNo': 'VA1234567890',
}),
]),
'leader': dict({
'batteryState': 'NORMAL',
'characteristics': dict({
'capabilities': list([
'INSIDE_TEMPERATURE_MEASUREMENT',
'IDENTIFY',
]),
}),
'connectionState': dict({
'timestamp': '2025-06-30T19:53:40.710Z',
'value': True,
}),
'currentFwVersion': '54.20',
'deviceType': 'RU01',
'serialNo': 'RU1234567890',
'shortSerialNo': 'RU1234567890',
}),
'type': 'HEATING',
'uis': list([
dict({
'batteryState': 'NORMAL',
'characteristics': dict({
'capabilities': list([
'INSIDE_TEMPERATURE_MEASUREMENT',
'IDENTIFY',
]),
}),
'connectionState': dict({
'timestamp': '2025-06-30T19:53:40.710Z',
'value': True,
}),
'currentFwVersion': '54.20',
'deviceType': 'RU01',
'serialNo': 'RU1234567890',
'shortSerialNo': 'RU1234567890',
}),
dict({
'batteryState': 'LOW',
'characteristics': dict({
'capabilities': list([
'INSIDE_TEMPERATURE_MEASUREMENT',
'IDENTIFY',
]),
}),
'childLockEnabled': False,
'connectionState': dict({
'timestamp': '2025-06-30T19:54:15.166Z',
'value': True,
}),
'currentFwVersion': '54.20',
'deviceType': 'VA01',
'mountingState': dict({
'timestamp': '2025-06-09T23:25:12.678Z',
'value': 'CALIBRATED',
}),
'mountingStateWithError': 'CALIBRATED',
'serialNo': 'VA1234567890',
'shortSerialNo': 'VA1234567890',
}),
]),
}),
'earlyStartEnabled': False,
'heatingCircuit': 1,
'type': 'HEATING',
}),
'3': dict({
'duties': dict({
'drivers': list([
dict({
'batteryState': 'LOW',
'characteristics': dict({
'capabilities': list([
'INSIDE_TEMPERATURE_MEASUREMENT',
'IDENTIFY',
]),
}),
'childLockEnabled': False,
'connectionState': dict({
'timestamp': '2025-06-30T19:54:15.166Z',
'value': True,
}),
'currentFwVersion': '54.20',
'deviceType': 'VA01',
'mountingState': dict({
'timestamp': '2025-06-09T23:25:12.678Z',
'value': 'CALIBRATED',
}),
'mountingStateWithError': 'CALIBRATED',
'serialNo': 'VA1234567890',
'shortSerialNo': 'VA1234567890',
}),
]),
'leader': dict({
'batteryState': 'NORMAL',
'characteristics': dict({
'capabilities': list([
'INSIDE_TEMPERATURE_MEASUREMENT',
'IDENTIFY',
]),
}),
'connectionState': dict({
'timestamp': '2025-06-30T19:53:40.710Z',
'value': True,
}),
'currentFwVersion': '54.20',
'deviceType': 'RU01',
'serialNo': 'RU1234567890',
'shortSerialNo': 'RU1234567890',
}),
'type': 'HEATING',
'uis': list([
dict({
'batteryState': 'NORMAL',
'characteristics': dict({
'capabilities': list([
'INSIDE_TEMPERATURE_MEASUREMENT',
'IDENTIFY',
]),
}),
'connectionState': dict({
'timestamp': '2025-06-30T19:53:40.710Z',
'value': True,
}),
'currentFwVersion': '54.20',
'deviceType': 'RU01',
'serialNo': 'RU1234567890',
'shortSerialNo': 'RU1234567890',
}),
dict({
'batteryState': 'LOW',
'characteristics': dict({
'capabilities': list([
'INSIDE_TEMPERATURE_MEASUREMENT',
'IDENTIFY',
]),
}),
'childLockEnabled': False,
'connectionState': dict({
'timestamp': '2025-06-30T19:54:15.166Z',
'value': True,
}),
'currentFwVersion': '54.20',
'deviceType': 'VA01',
'mountingState': dict({
'timestamp': '2025-06-09T23:25:12.678Z',
'value': 'CALIBRATED',
}),
'mountingStateWithError': 'CALIBRATED',
'serialNo': 'VA1234567890',
'shortSerialNo': 'VA1234567890',
}),
]),
}),
'earlyStartEnabled': False,
'heatingCircuit': 1,
'type': 'HEATING',
}),
'4': dict({
'duties': dict({
'drivers': list([
dict({
'batteryState': 'LOW',
'characteristics': dict({
'capabilities': list([
'INSIDE_TEMPERATURE_MEASUREMENT',
'IDENTIFY',
]),
}),
'childLockEnabled': False,
'connectionState': dict({
'timestamp': '2025-06-30T19:54:15.166Z',
'value': True,
}),
'currentFwVersion': '54.20',
'deviceType': 'VA01',
'mountingState': dict({
'timestamp': '2025-06-09T23:25:12.678Z',
'value': 'CALIBRATED',
}),
'mountingStateWithError': 'CALIBRATED',
'serialNo': 'VA1234567890',
'shortSerialNo': 'VA1234567890',
}),
]),
'leader': dict({
'batteryState': 'NORMAL',
'characteristics': dict({
'capabilities': list([
'INSIDE_TEMPERATURE_MEASUREMENT',
'IDENTIFY',
]),
}),
'connectionState': dict({
'timestamp': '2025-06-30T19:53:40.710Z',
'value': True,
}),
'currentFwVersion': '54.20',
'deviceType': 'RU01',
'serialNo': 'RU1234567890',
'shortSerialNo': 'RU1234567890',
}),
'type': 'HEATING',
'uis': list([
dict({
'batteryState': 'NORMAL',
'characteristics': dict({
'capabilities': list([
'INSIDE_TEMPERATURE_MEASUREMENT',
'IDENTIFY',
]),
}),
'connectionState': dict({
'timestamp': '2025-06-30T19:53:40.710Z',
'value': True,
}),
'currentFwVersion': '54.20',
'deviceType': 'RU01',
'serialNo': 'RU1234567890',
'shortSerialNo': 'RU1234567890',
}),
dict({
'batteryState': 'LOW',
'characteristics': dict({
'capabilities': list([
'INSIDE_TEMPERATURE_MEASUREMENT',
'IDENTIFY',
]),
}),
'childLockEnabled': False,
'connectionState': dict({
'timestamp': '2025-06-30T19:54:15.166Z',
'value': True,
}),
'currentFwVersion': '54.20',
'deviceType': 'VA01',
'mountingState': dict({
'timestamp': '2025-06-09T23:25:12.678Z',
'value': 'CALIBRATED',
}),
'mountingStateWithError': 'CALIBRATED',
'serialNo': 'VA1234567890',
'shortSerialNo': 'VA1234567890',
}),
]),
}),
'earlyStartEnabled': False,
'heatingCircuit': 1,
'type': 'HEATING',
}),
'5': dict({
'duties': dict({
'drivers': list([
dict({
'batteryState': 'LOW',
'characteristics': dict({
'capabilities': list([
'INSIDE_TEMPERATURE_MEASUREMENT',
'IDENTIFY',
]),
}),
'childLockEnabled': False,
'connectionState': dict({
'timestamp': '2025-06-30T19:54:15.166Z',
'value': True,
}),
'currentFwVersion': '54.20',
'deviceType': 'VA01',
'mountingState': dict({
'timestamp': '2025-06-09T23:25:12.678Z',
'value': 'CALIBRATED',
}),
'mountingStateWithError': 'CALIBRATED',
'serialNo': 'VA1234567890',
'shortSerialNo': 'VA1234567890',
}),
]),
'leader': dict({
'batteryState': 'NORMAL',
'characteristics': dict({
'capabilities': list([
'INSIDE_TEMPERATURE_MEASUREMENT',
'IDENTIFY',
]),
}),
'connectionState': dict({
'timestamp': '2025-06-30T19:53:40.710Z',
'value': True,
}),
'currentFwVersion': '54.20',
'deviceType': 'RU01',
'serialNo': 'RU1234567890',
'shortSerialNo': 'RU1234567890',
}),
'type': 'HEATING',
'uis': list([
dict({
'batteryState': 'NORMAL',
'characteristics': dict({
'capabilities': list([
'INSIDE_TEMPERATURE_MEASUREMENT',
'IDENTIFY',
]),
}),
'connectionState': dict({
'timestamp': '2025-06-30T19:53:40.710Z',
'value': True,
}),
'currentFwVersion': '54.20',
'deviceType': 'RU01',
'serialNo': 'RU1234567890',
'shortSerialNo': 'RU1234567890',
}),
dict({
'batteryState': 'LOW',
'characteristics': dict({
'capabilities': list([
'INSIDE_TEMPERATURE_MEASUREMENT',
'IDENTIFY',
]),
}),
'childLockEnabled': False,
'connectionState': dict({
'timestamp': '2025-06-30T19:54:15.166Z',
'value': True,
}),
'currentFwVersion': '54.20',
'deviceType': 'VA01',
'mountingState': dict({
'timestamp': '2025-06-09T23:25:12.678Z',
'value': 'CALIBRATED',
}),
'mountingStateWithError': 'CALIBRATED',
'serialNo': 'VA1234567890',
'shortSerialNo': 'VA1234567890',
}),
]),
}),
'earlyStartEnabled': False,
'heatingCircuit': 1,
'type': 'HEATING',
}),
'6': dict({
'duties': dict({
'drivers': list([
dict({
'batteryState': 'LOW',
'characteristics': dict({
'capabilities': list([
'INSIDE_TEMPERATURE_MEASUREMENT',
'IDENTIFY',
]),
}),
'childLockEnabled': False,
'connectionState': dict({
'timestamp': '2025-06-30T19:54:15.166Z',
'value': True,
}),
'currentFwVersion': '54.20',
'deviceType': 'VA01',
'mountingState': dict({
'timestamp': '2025-06-09T23:25:12.678Z',
'value': 'CALIBRATED',
}),
'mountingStateWithError': 'CALIBRATED',
'serialNo': 'VA1234567890',
'shortSerialNo': 'VA1234567890',
}),
]),
'leader': dict({
'batteryState': 'NORMAL',
'characteristics': dict({
'capabilities': list([
'INSIDE_TEMPERATURE_MEASUREMENT',
'IDENTIFY',
]),
}),
'connectionState': dict({
'timestamp': '2025-06-30T19:53:40.710Z',
'value': True,
}),
'currentFwVersion': '54.20',
'deviceType': 'RU01',
'serialNo': 'RU1234567890',
'shortSerialNo': 'RU1234567890',
}),
'type': 'HEATING',
'uis': list([
dict({
'batteryState': 'NORMAL',
'characteristics': dict({
'capabilities': list([
'INSIDE_TEMPERATURE_MEASUREMENT',
'IDENTIFY',
]),
}),
'connectionState': dict({
'timestamp': '2025-06-30T19:53:40.710Z',
'value': True,
}),
'currentFwVersion': '54.20',
'deviceType': 'RU01',
'serialNo': 'RU1234567890',
'shortSerialNo': 'RU1234567890',
}),
dict({
'batteryState': 'LOW',
'characteristics': dict({
'capabilities': list([
'INSIDE_TEMPERATURE_MEASUREMENT',
'IDENTIFY',
]),
}),
'childLockEnabled': False,
'connectionState': dict({
'timestamp': '2025-06-30T19:54:15.166Z',
'value': True,
}),
'currentFwVersion': '54.20',
'deviceType': 'VA01',
'mountingState': dict({
'timestamp': '2025-06-09T23:25:12.678Z',
'value': 'CALIBRATED',
}),
'mountingStateWithError': 'CALIBRATED',
'serialNo': 'VA1234567890',
'shortSerialNo': 'VA1234567890',
}),
]),
}),
'earlyStartEnabled': False,
'heatingCircuit': 1,
'type': 'HEATING',
}),
}),
}),
'mobile_devices': dict({
'mobile_device': dict({

View File

@ -0,0 +1,91 @@
"""The select tests for the tado platform."""
from unittest.mock import patch
import pytest
from homeassistant.components.select import (
DOMAIN as SELECT_DOMAIN,
SERVICE_SELECT_OPTION,
)
from homeassistant.const import ATTR_ENTITY_ID, ATTR_OPTION
from homeassistant.core import HomeAssistant
from .util import async_init_integration
HEATING_CIRCUIT_SELECT_ENTITY = "select.baseboard_heater_heating_circuit"
NO_HEATING_CIRCUIT = "no_heating_circuit"
HEATING_CIRCUIT_OPTION = "RU1234567890"
ZONE_ID = 1
HEATING_CIRCUIT_ID = 1
async def test_heating_circuit_select(hass: HomeAssistant) -> None:
"""Test creation of heating circuit select entity."""
await async_init_integration(hass)
state = hass.states.get(HEATING_CIRCUIT_SELECT_ENTITY)
assert state is not None
assert state.state == HEATING_CIRCUIT_OPTION
assert NO_HEATING_CIRCUIT in state.attributes["options"]
assert HEATING_CIRCUIT_OPTION in state.attributes["options"]
@pytest.mark.parametrize(
("option", "expected_circuit_id"),
[(HEATING_CIRCUIT_OPTION, HEATING_CIRCUIT_ID), (NO_HEATING_CIRCUIT, None)],
)
async def test_heating_circuit_select_action(
hass: HomeAssistant, option, expected_circuit_id
) -> None:
"""Test selecting heating circuit option."""
await async_init_integration(hass)
# Test selecting a specific heating circuit
with (
patch(
"homeassistant.components.tado.PyTado.interface.api.Tado.set_zone_heating_circuit"
) as mock_set_zone_heating_circuit,
patch(
"homeassistant.components.tado.PyTado.interface.api.Tado.get_zone_control"
) as mock_get_zone_control,
):
await hass.services.async_call(
SELECT_DOMAIN,
SERVICE_SELECT_OPTION,
{
ATTR_ENTITY_ID: HEATING_CIRCUIT_SELECT_ENTITY,
ATTR_OPTION: option,
},
blocking=True,
)
mock_set_zone_heating_circuit.assert_called_with(ZONE_ID, expected_circuit_id)
assert mock_get_zone_control.called
@pytest.mark.usefixtures("caplog")
async def test_heating_circuit_not_found(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Test when a heating circuit with a specific number is not found."""
circuit_not_matching_zone_control = 999
heating_circuits = [
{
"number": circuit_not_matching_zone_control,
"driverSerialNo": "RU1234567890",
"driverShortSerialNo": "RU1234567890",
}
]
with patch(
"homeassistant.components.tado.PyTado.interface.api.Tado.get_heating_circuits",
return_value=heating_circuits,
):
await async_init_integration(hass)
state = hass.states.get(HEATING_CIRCUIT_SELECT_ENTITY)
assert state.state == NO_HEATING_CIRCUIT
assert "Heating circuit with number 1 not found for zone" in caplog.text

View File

@ -20,8 +20,10 @@ async def async_init_integration(
me_fixture = "me.json"
weather_fixture = "weather.json"
home_fixture = "home.json"
home_heating_circuits_fixture = "heating_circuits.json"
home_state_fixture = "home_state.json"
zones_fixture = "zones.json"
zone_control_fixture = "zone_control.json"
zone_states_fixture = "zone_states.json"
# WR1 Device
@ -70,6 +72,10 @@ async def async_init_integration(
"https://my.tado.com/api/v2/homes/1/",
text=await async_load_fixture(hass, home_fixture, DOMAIN),
)
m.get(
"https://my.tado.com/api/v2/homes/1/heatingCircuits",
text=await async_load_fixture(hass, home_heating_circuits_fixture, DOMAIN),
)
m.get(
"https://my.tado.com/api/v2/homes/1/weather",
text=await async_load_fixture(hass, weather_fixture, DOMAIN),
@ -178,6 +184,12 @@ async def async_init_integration(
"https://my.tado.com/api/v2/homes/1/zones/1/state",
text=await async_load_fixture(hass, zone_1_state_fixture, DOMAIN),
)
zone_ids = [1, 2, 3, 4, 5, 6]
for zone_id in zone_ids:
m.get(
f"https://my.tado.com/api/v2/homes/1/zones/{zone_id}/control",
text=await async_load_fixture(hass, zone_control_fixture, DOMAIN),
)
m.post(
"https://login.tado.com/oauth2/token",
text=await async_load_fixture(hass, token_fixture, DOMAIN),