mirror of
https://github.com/home-assistant/core.git
synced 2025-07-21 12:17:07 +00:00
Tado change to async and add Data Update Coordinator (#134175)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
parent
09559a43ad
commit
5d353a9833
@ -3,14 +3,15 @@
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
import requests.exceptions
|
||||
import PyTado
|
||||
import PyTado.exceptions
|
||||
from PyTado.interface import Tado
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import (
|
||||
@ -21,11 +22,9 @@ from .const import (
|
||||
CONST_OVERLAY_TADO_OPTIONS,
|
||||
DOMAIN,
|
||||
)
|
||||
from .coordinator import TadoDataUpdateCoordinator, TadoMobileDeviceUpdateCoordinator
|
||||
from .models import TadoData
|
||||
from .services import setup_services
|
||||
from .tado_connector import TadoConnector
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
PLATFORMS = [
|
||||
Platform.BINARY_SENSOR,
|
||||
@ -41,16 +40,17 @@ SCAN_MOBILE_DEVICE_INTERVAL = timedelta(seconds=30)
|
||||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up Tado."""
|
||||
|
||||
setup_services(hass)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
type TadoConfigEntry = ConfigEntry[TadoConnector]
|
||||
type TadoConfigEntry = ConfigEntry[TadoData]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: TadoConfigEntry) -> bool:
|
||||
@ -58,53 +58,30 @@ async def async_setup_entry(hass: HomeAssistant, entry: TadoConfigEntry) -> bool
|
||||
|
||||
_async_import_options_from_data_if_missing(hass, entry)
|
||||
|
||||
username = entry.data[CONF_USERNAME]
|
||||
password = entry.data[CONF_PASSWORD]
|
||||
fallback = entry.options.get(CONF_FALLBACK, CONST_OVERLAY_TADO_DEFAULT)
|
||||
|
||||
tadoconnector = TadoConnector(hass, username, password, fallback)
|
||||
|
||||
_LOGGER.debug("Setting up Tado connection")
|
||||
try:
|
||||
await hass.async_add_executor_job(tadoconnector.setup)
|
||||
except KeyError:
|
||||
_LOGGER.error("Failed to login to tado")
|
||||
return False
|
||||
except RuntimeError as exc:
|
||||
_LOGGER.error("Failed to setup tado: %s", exc)
|
||||
return False
|
||||
except requests.exceptions.Timeout as ex:
|
||||
raise ConfigEntryNotReady from ex
|
||||
except requests.exceptions.HTTPError as ex:
|
||||
if ex.response.status_code > 400 and ex.response.status_code < 500:
|
||||
_LOGGER.error("Failed to login to tado: %s", ex)
|
||||
return False
|
||||
raise ConfigEntryNotReady from ex
|
||||
|
||||
# Do first update
|
||||
await hass.async_add_executor_job(tadoconnector.update)
|
||||
|
||||
# Poll for updates in the background
|
||||
entry.async_on_unload(
|
||||
async_track_time_interval(
|
||||
hass,
|
||||
lambda now: tadoconnector.update(),
|
||||
SCAN_INTERVAL,
|
||||
tado = await hass.async_add_executor_job(
|
||||
Tado,
|
||||
entry.data[CONF_USERNAME],
|
||||
entry.data[CONF_PASSWORD],
|
||||
)
|
||||
except PyTado.exceptions.TadoWrongCredentialsException as err:
|
||||
raise ConfigEntryError(f"Invalid Tado credentials. Error: {err}") from err
|
||||
except PyTado.exceptions.TadoException as err:
|
||||
raise ConfigEntryNotReady(f"Error during Tado setup: {err}") from err
|
||||
_LOGGER.debug(
|
||||
"Tado connection established for username: %s", entry.data[CONF_USERNAME]
|
||||
)
|
||||
|
||||
entry.async_on_unload(
|
||||
async_track_time_interval(
|
||||
hass,
|
||||
lambda now: tadoconnector.update_mobile_devices(),
|
||||
SCAN_MOBILE_DEVICE_INTERVAL,
|
||||
)
|
||||
)
|
||||
coordinator = TadoDataUpdateCoordinator(hass, entry, tado)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
entry.async_on_unload(entry.add_update_listener(_async_update_listener))
|
||||
|
||||
entry.runtime_data = tadoconnector
|
||||
mobile_coordinator = TadoMobileDeviceUpdateCoordinator(hass, entry, tado)
|
||||
await mobile_coordinator.async_config_entry_first_refresh()
|
||||
|
||||
entry.runtime_data = TadoData(coordinator, mobile_coordinator)
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
entry.async_on_unload(entry.add_update_listener(update_listener))
|
||||
|
||||
return True
|
||||
|
||||
@ -126,7 +103,7 @@ def _async_import_options_from_data_if_missing(hass: HomeAssistant, entry: Confi
|
||||
hass.config_entries.async_update_entry(entry, options=options)
|
||||
|
||||
|
||||
async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
async def update_listener(hass: HomeAssistant, entry: TadoConfigEntry):
|
||||
"""Handle options update."""
|
||||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
|
||||
|
@ -13,21 +13,19 @@ from homeassistant.components.binary_sensor import (
|
||||
BinarySensorEntityDescription,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
|
||||
from . import TadoConfigEntry
|
||||
from .const import (
|
||||
SIGNAL_TADO_UPDATE_RECEIVED,
|
||||
TYPE_AIR_CONDITIONING,
|
||||
TYPE_BATTERY,
|
||||
TYPE_HEATING,
|
||||
TYPE_HOT_WATER,
|
||||
TYPE_POWER,
|
||||
)
|
||||
from .coordinator import TadoDataUpdateCoordinator
|
||||
from .entity import TadoDeviceEntity, TadoZoneEntity
|
||||
from .tado_connector import TadoConnector
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -121,7 +119,7 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up the Tado sensor platform."""
|
||||
|
||||
tado = entry.runtime_data
|
||||
tado = entry.runtime_data.coordinator
|
||||
devices = tado.devices
|
||||
zones = tado.zones
|
||||
entities: list[BinarySensorEntity] = []
|
||||
@ -164,43 +162,23 @@ class TadoDeviceBinarySensor(TadoDeviceEntity, BinarySensorEntity):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
tado: TadoConnector,
|
||||
coordinator: TadoDataUpdateCoordinator,
|
||||
device_info: dict[str, Any],
|
||||
entity_description: TadoBinarySensorEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize of the Tado Sensor."""
|
||||
self.entity_description = entity_description
|
||||
self._tado = tado
|
||||
super().__init__(device_info)
|
||||
super().__init__(device_info, coordinator)
|
||||
|
||||
self._attr_unique_id = (
|
||||
f"{entity_description.key} {self.device_id} {tado.home_id}"
|
||||
f"{entity_description.key} {self.device_id} {coordinator.home_id}"
|
||||
)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register for sensor updates."""
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(
|
||||
self.hass,
|
||||
SIGNAL_TADO_UPDATE_RECEIVED.format(
|
||||
self._tado.home_id, "device", self.device_id
|
||||
),
|
||||
self._async_update_callback,
|
||||
)
|
||||
)
|
||||
self._async_update_device_data()
|
||||
|
||||
@callback
|
||||
def _async_update_callback(self) -> None:
|
||||
"""Update and write state."""
|
||||
self._async_update_device_data()
|
||||
self.async_write_ha_state()
|
||||
|
||||
@callback
|
||||
def _async_update_device_data(self) -> None:
|
||||
"""Handle update callbacks."""
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the coordinator."""
|
||||
try:
|
||||
self._device_info = self._tado.data["device"][self.device_id]
|
||||
self._device_info = self.coordinator.data["device"][self.device_id]
|
||||
except KeyError:
|
||||
return
|
||||
|
||||
@ -209,6 +187,7 @@ class TadoDeviceBinarySensor(TadoDeviceEntity, BinarySensorEntity):
|
||||
self._attr_extra_state_attributes = self.entity_description.attributes_fn(
|
||||
self._device_info
|
||||
)
|
||||
super()._handle_coordinator_update()
|
||||
|
||||
|
||||
class TadoZoneBinarySensor(TadoZoneEntity, BinarySensorEntity):
|
||||
@ -218,42 +197,24 @@ class TadoZoneBinarySensor(TadoZoneEntity, BinarySensorEntity):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
tado: TadoConnector,
|
||||
coordinator: TadoDataUpdateCoordinator,
|
||||
zone_name: str,
|
||||
zone_id: int,
|
||||
entity_description: TadoBinarySensorEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize of the Tado Sensor."""
|
||||
self.entity_description = entity_description
|
||||
self._tado = tado
|
||||
super().__init__(zone_name, tado.home_id, zone_id)
|
||||
super().__init__(zone_name, coordinator.home_id, zone_id, coordinator)
|
||||
|
||||
self._attr_unique_id = f"{entity_description.key} {zone_id} {tado.home_id}"
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register for sensor updates."""
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(
|
||||
self.hass,
|
||||
SIGNAL_TADO_UPDATE_RECEIVED.format(
|
||||
self._tado.home_id, "zone", self.zone_id
|
||||
),
|
||||
self._async_update_callback,
|
||||
)
|
||||
self._attr_unique_id = (
|
||||
f"{entity_description.key} {zone_id} {coordinator.home_id}"
|
||||
)
|
||||
self._async_update_zone_data()
|
||||
|
||||
@callback
|
||||
def _async_update_callback(self) -> None:
|
||||
"""Update and write state."""
|
||||
self._async_update_zone_data()
|
||||
self.async_write_ha_state()
|
||||
|
||||
@callback
|
||||
def _async_update_zone_data(self) -> None:
|
||||
"""Handle update callbacks."""
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the coordinator."""
|
||||
try:
|
||||
tado_zone_data = self._tado.data["zone"][self.zone_id]
|
||||
tado_zone_data = self.coordinator.data["zone"][self.zone_id]
|
||||
except KeyError:
|
||||
return
|
||||
|
||||
@ -262,3 +223,4 @@ class TadoZoneBinarySensor(TadoZoneEntity, BinarySensorEntity):
|
||||
self._attr_extra_state_attributes = self.entity_description.attributes_fn(
|
||||
tado_zone_data
|
||||
)
|
||||
super()._handle_coordinator_update()
|
||||
|
@ -26,11 +26,10 @@ from homeassistant.components.climate import (
|
||||
from homeassistant.const import ATTR_TEMPERATURE, 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
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import VolDictType
|
||||
|
||||
from . import TadoConfigEntry, TadoConnector
|
||||
from . import TadoConfigEntry
|
||||
from .const import (
|
||||
CONST_EXCLUSIVE_OVERLAY_GROUP,
|
||||
CONST_FAN_AUTO,
|
||||
@ -50,7 +49,6 @@ from .const import (
|
||||
HA_TO_TADO_HVAC_MODE_MAP,
|
||||
ORDERED_KNOWN_TADO_MODES,
|
||||
PRESET_AUTO,
|
||||
SIGNAL_TADO_UPDATE_RECEIVED,
|
||||
SUPPORT_PRESET_AUTO,
|
||||
SUPPORT_PRESET_MANUAL,
|
||||
TADO_DEFAULT_MAX_TEMP,
|
||||
@ -73,6 +71,7 @@ from .const import (
|
||||
TYPE_AIR_CONDITIONING,
|
||||
TYPE_HEATING,
|
||||
)
|
||||
from .coordinator import TadoDataUpdateCoordinator
|
||||
from .entity import TadoZoneEntity
|
||||
from .helper import decide_duration, decide_overlay_mode, generate_supported_fanmodes
|
||||
|
||||
@ -105,8 +104,8 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up the Tado climate platform."""
|
||||
|
||||
tado = entry.runtime_data
|
||||
entities = await hass.async_add_executor_job(_generate_entities, tado)
|
||||
tado = entry.runtime_data.coordinator
|
||||
entities = await _generate_entities(tado)
|
||||
|
||||
platform = entity_platform.async_get_current_platform()
|
||||
|
||||
@ -125,12 +124,12 @@ async def async_setup_entry(
|
||||
async_add_entities(entities, True)
|
||||
|
||||
|
||||
def _generate_entities(tado: TadoConnector) -> list[TadoClimate]:
|
||||
async def _generate_entities(tado: TadoDataUpdateCoordinator) -> list[TadoClimate]:
|
||||
"""Create all climate entities."""
|
||||
entities = []
|
||||
for zone in tado.zones:
|
||||
if zone["type"] in [TYPE_HEATING, TYPE_AIR_CONDITIONING]:
|
||||
entity = create_climate_entity(
|
||||
entity = await create_climate_entity(
|
||||
tado, zone["name"], zone["id"], zone["devices"][0]
|
||||
)
|
||||
if entity:
|
||||
@ -138,11 +137,11 @@ def _generate_entities(tado: TadoConnector) -> list[TadoClimate]:
|
||||
return entities
|
||||
|
||||
|
||||
def create_climate_entity(
|
||||
tado: TadoConnector, name: str, zone_id: int, device_info: dict
|
||||
async def create_climate_entity(
|
||||
tado: TadoDataUpdateCoordinator, name: str, zone_id: int, device_info: dict
|
||||
) -> TadoClimate | None:
|
||||
"""Create a Tado climate entity."""
|
||||
capabilities = tado.get_capabilities(zone_id)
|
||||
capabilities = await tado.get_capabilities(zone_id)
|
||||
_LOGGER.debug("Capabilities for zone %s: %s", zone_id, capabilities)
|
||||
|
||||
zone_type = capabilities["type"]
|
||||
@ -243,6 +242,8 @@ def create_climate_entity(
|
||||
cool_max_temp = float(cool_temperatures["celsius"]["max"])
|
||||
cool_step = cool_temperatures["celsius"].get("step", PRECISION_TENTHS)
|
||||
|
||||
auto_geofencing_supported = await tado.get_auto_geofencing_supported()
|
||||
|
||||
return TadoClimate(
|
||||
tado,
|
||||
name,
|
||||
@ -251,6 +252,8 @@ def create_climate_entity(
|
||||
supported_hvac_modes,
|
||||
support_flags,
|
||||
device_info,
|
||||
capabilities,
|
||||
auto_geofencing_supported,
|
||||
heat_min_temp,
|
||||
heat_max_temp,
|
||||
heat_step,
|
||||
@ -272,13 +275,15 @@ class TadoClimate(TadoZoneEntity, ClimateEntity):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
tado: TadoConnector,
|
||||
coordinator: TadoDataUpdateCoordinator,
|
||||
zone_name: str,
|
||||
zone_id: int,
|
||||
zone_type: str,
|
||||
supported_hvac_modes: list[HVACMode],
|
||||
support_flags: ClimateEntityFeature,
|
||||
device_info: dict[str, str],
|
||||
capabilities: dict[str, str],
|
||||
auto_geofencing_supported: bool,
|
||||
heat_min_temp: float | None = None,
|
||||
heat_max_temp: float | None = None,
|
||||
heat_step: float | None = None,
|
||||
@ -289,13 +294,13 @@ class TadoClimate(TadoZoneEntity, ClimateEntity):
|
||||
supported_swing_modes: list[str] | None = None,
|
||||
) -> None:
|
||||
"""Initialize of Tado climate entity."""
|
||||
self._tado = tado
|
||||
super().__init__(zone_name, tado.home_id, zone_id)
|
||||
self._tado = coordinator
|
||||
super().__init__(zone_name, coordinator.home_id, zone_id, coordinator)
|
||||
|
||||
self.zone_id = zone_id
|
||||
self.zone_type = zone_type
|
||||
|
||||
self._attr_unique_id = f"{zone_type} {zone_id} {tado.home_id}"
|
||||
self._attr_unique_id = f"{zone_type} {zone_id} {coordinator.home_id}"
|
||||
|
||||
self._device_info = device_info
|
||||
self._device_id = self._device_info["shortSerialNo"]
|
||||
@ -327,36 +332,61 @@ class TadoClimate(TadoZoneEntity, ClimateEntity):
|
||||
self._current_tado_vertical_swing = TADO_SWING_OFF
|
||||
self._current_tado_horizontal_swing = TADO_SWING_OFF
|
||||
|
||||
capabilities = tado.get_capabilities(zone_id)
|
||||
self._current_tado_capabilities = capabilities
|
||||
self._auto_geofencing_supported = auto_geofencing_supported
|
||||
|
||||
self._tado_zone_data: PyTado.TadoZone = {}
|
||||
self._tado_geofence_data: dict[str, str] | None = None
|
||||
|
||||
self._tado_zone_temp_offset: dict[str, Any] = {}
|
||||
|
||||
self._async_update_home_data()
|
||||
self._async_update_zone_data()
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register for sensor updates."""
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(
|
||||
self.hass,
|
||||
SIGNAL_TADO_UPDATE_RECEIVED.format(self._tado.home_id, "home", "data"),
|
||||
self._async_update_home_callback,
|
||||
)
|
||||
)
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the coordinator."""
|
||||
self._async_update_zone_data()
|
||||
super()._handle_coordinator_update()
|
||||
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(
|
||||
self.hass,
|
||||
SIGNAL_TADO_UPDATE_RECEIVED.format(
|
||||
self._tado.home_id, "zone", self.zone_id
|
||||
),
|
||||
self._async_update_zone_callback,
|
||||
@callback
|
||||
def _async_update_zone_data(self) -> None:
|
||||
"""Load tado data into zone."""
|
||||
self._tado_geofence_data = self._tado.data["geofence"]
|
||||
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]
|
||||
|
||||
self._current_tado_hvac_mode = self._tado_zone_data.current_hvac_mode
|
||||
self._current_tado_hvac_action = self._tado_zone_data.current_hvac_action
|
||||
|
||||
if self._is_valid_setting_for_hvac_mode(TADO_FANLEVEL_SETTING):
|
||||
self._current_tado_fan_level = self._tado_zone_data.current_fan_level
|
||||
if self._is_valid_setting_for_hvac_mode(TADO_FANSPEED_SETTING):
|
||||
self._current_tado_fan_speed = self._tado_zone_data.current_fan_speed
|
||||
if self._is_valid_setting_for_hvac_mode(TADO_SWING_SETTING):
|
||||
self._current_tado_swing_mode = self._tado_zone_data.current_swing_mode
|
||||
if self._is_valid_setting_for_hvac_mode(TADO_VERTICAL_SWING_SETTING):
|
||||
self._current_tado_vertical_swing = (
|
||||
self._tado_zone_data.current_vertical_swing_mode
|
||||
)
|
||||
)
|
||||
if self._is_valid_setting_for_hvac_mode(TADO_HORIZONTAL_SWING_SETTING):
|
||||
self._current_tado_horizontal_swing = (
|
||||
self._tado_zone_data.current_horizontal_swing_mode
|
||||
)
|
||||
|
||||
@callback
|
||||
def _async_update_zone_callback(self) -> None:
|
||||
"""Load tado data and update state."""
|
||||
self._async_update_zone_data()
|
||||
|
||||
@property
|
||||
def current_humidity(self) -> int | None:
|
||||
@ -401,12 +431,13 @@ class TadoClimate(TadoZoneEntity, ClimateEntity):
|
||||
return FAN_AUTO
|
||||
return None
|
||||
|
||||
def set_fan_mode(self, fan_mode: str) -> None:
|
||||
async def async_set_fan_mode(self, fan_mode: str) -> None:
|
||||
"""Turn fan on/off."""
|
||||
if self._is_valid_setting_for_hvac_mode(TADO_FANSPEED_SETTING):
|
||||
self._control_hvac(fan_mode=HA_TO_TADO_FAN_MODE_MAP_LEGACY[fan_mode])
|
||||
await self._control_hvac(fan_mode=HA_TO_TADO_FAN_MODE_MAP_LEGACY[fan_mode])
|
||||
elif self._is_valid_setting_for_hvac_mode(TADO_FANLEVEL_SETTING):
|
||||
self._control_hvac(fan_mode=HA_TO_TADO_FAN_MODE_MAP[fan_mode])
|
||||
await self._control_hvac(fan_mode=HA_TO_TADO_FAN_MODE_MAP[fan_mode])
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
@property
|
||||
def preset_mode(self) -> str:
|
||||
@ -425,13 +456,14 @@ class TadoClimate(TadoZoneEntity, ClimateEntity):
|
||||
@property
|
||||
def preset_modes(self) -> list[str]:
|
||||
"""Return a list of available preset modes."""
|
||||
if self._tado.get_auto_geofencing_supported():
|
||||
if self._auto_geofencing_supported:
|
||||
return SUPPORT_PRESET_AUTO
|
||||
return SUPPORT_PRESET_MANUAL
|
||||
|
||||
def set_preset_mode(self, preset_mode: str) -> None:
|
||||
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
||||
"""Set new preset mode."""
|
||||
self._tado.set_presence(preset_mode)
|
||||
await self._tado.set_presence(preset_mode)
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
@property
|
||||
def target_temperature_step(self) -> float | None:
|
||||
@ -449,7 +481,7 @@ class TadoClimate(TadoZoneEntity, ClimateEntity):
|
||||
# the device is switching states
|
||||
return self._tado_zone_data.target_temp or self._tado_zone_data.current_temp
|
||||
|
||||
def set_timer(
|
||||
async def set_timer(
|
||||
self,
|
||||
temperature: float,
|
||||
time_period: int | None = None,
|
||||
@ -457,14 +489,15 @@ class TadoClimate(TadoZoneEntity, ClimateEntity):
|
||||
):
|
||||
"""Set the timer on the entity, and temperature if supported."""
|
||||
|
||||
self._control_hvac(
|
||||
await self._control_hvac(
|
||||
hvac_mode=CONST_MODE_HEAT,
|
||||
target_temp=temperature,
|
||||
duration=time_period,
|
||||
overlay_mode=requested_overlay,
|
||||
)
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
def set_temp_offset(self, offset: float) -> None:
|
||||
async def set_temp_offset(self, offset: float) -> None:
|
||||
"""Set offset on the entity."""
|
||||
|
||||
_LOGGER.debug(
|
||||
@ -474,8 +507,9 @@ class TadoClimate(TadoZoneEntity, ClimateEntity):
|
||||
)
|
||||
|
||||
self._tado.set_temperature_offset(self._device_id, offset)
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
def set_temperature(self, **kwargs: Any) -> None:
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
"""Set new target temperature."""
|
||||
if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None:
|
||||
return
|
||||
@ -485,15 +519,21 @@ class TadoClimate(TadoZoneEntity, ClimateEntity):
|
||||
CONST_MODE_AUTO,
|
||||
CONST_MODE_SMART_SCHEDULE,
|
||||
):
|
||||
self._control_hvac(target_temp=temperature)
|
||||
await self._control_hvac(target_temp=temperature)
|
||||
await self.coordinator.async_request_refresh()
|
||||
return
|
||||
|
||||
new_hvac_mode = CONST_MODE_COOL if self._ac_device else CONST_MODE_HEAT
|
||||
self._control_hvac(target_temp=temperature, hvac_mode=new_hvac_mode)
|
||||
await self._control_hvac(target_temp=temperature, hvac_mode=new_hvac_mode)
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
def set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
||||
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
||||
"""Set new target hvac mode."""
|
||||
self._control_hvac(hvac_mode=HA_TO_TADO_HVAC_MODE_MAP[hvac_mode])
|
||||
_LOGGER.debug(
|
||||
"Setting new hvac mode for device %s to %s", self._device_id, hvac_mode
|
||||
)
|
||||
await self._control_hvac(hvac_mode=HA_TO_TADO_HVAC_MODE_MAP[hvac_mode])
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
@ -559,7 +599,7 @@ class TadoClimate(TadoZoneEntity, ClimateEntity):
|
||||
)
|
||||
return state_attr
|
||||
|
||||
def set_swing_mode(self, swing_mode: str) -> None:
|
||||
async def async_set_swing_mode(self, swing_mode: str) -> None:
|
||||
"""Set swing modes for the device."""
|
||||
vertical_swing = None
|
||||
horizontal_swing = None
|
||||
@ -591,62 +631,12 @@ class TadoClimate(TadoZoneEntity, ClimateEntity):
|
||||
if self._is_valid_setting_for_hvac_mode(TADO_HORIZONTAL_SWING_SETTING):
|
||||
horizontal_swing = TADO_SWING_ON
|
||||
|
||||
self._control_hvac(
|
||||
await self._control_hvac(
|
||||
swing_mode=swing,
|
||||
vertical_swing=vertical_swing,
|
||||
horizontal_swing=horizontal_swing,
|
||||
)
|
||||
|
||||
@callback
|
||||
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]
|
||||
|
||||
self._current_tado_hvac_mode = self._tado_zone_data.current_hvac_mode
|
||||
self._current_tado_hvac_action = self._tado_zone_data.current_hvac_action
|
||||
|
||||
if self._is_valid_setting_for_hvac_mode(TADO_FANLEVEL_SETTING):
|
||||
self._current_tado_fan_level = self._tado_zone_data.current_fan_level
|
||||
if self._is_valid_setting_for_hvac_mode(TADO_FANSPEED_SETTING):
|
||||
self._current_tado_fan_speed = self._tado_zone_data.current_fan_speed
|
||||
if self._is_valid_setting_for_hvac_mode(TADO_SWING_SETTING):
|
||||
self._current_tado_swing_mode = self._tado_zone_data.current_swing_mode
|
||||
if self._is_valid_setting_for_hvac_mode(TADO_VERTICAL_SWING_SETTING):
|
||||
self._current_tado_vertical_swing = (
|
||||
self._tado_zone_data.current_vertical_swing_mode
|
||||
)
|
||||
if self._is_valid_setting_for_hvac_mode(TADO_HORIZONTAL_SWING_SETTING):
|
||||
self._current_tado_horizontal_swing = (
|
||||
self._tado_zone_data.current_horizontal_swing_mode
|
||||
)
|
||||
|
||||
@callback
|
||||
def _async_update_zone_callback(self) -> None:
|
||||
"""Load tado data and update state."""
|
||||
self._async_update_zone_data()
|
||||
self.async_write_ha_state()
|
||||
|
||||
@callback
|
||||
def _async_update_home_data(self) -> None:
|
||||
"""Load tado geofencing data into zone."""
|
||||
self._tado_geofence_data = self._tado.data["geofence"]
|
||||
|
||||
@callback
|
||||
def _async_update_home_callback(self) -> None:
|
||||
"""Load tado data and update state."""
|
||||
self._async_update_home_data()
|
||||
self.async_write_ha_state()
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
def _normalize_target_temp_for_hvac_mode(self) -> None:
|
||||
def adjust_temp(min_temp, max_temp) -> float | None:
|
||||
@ -665,7 +655,7 @@ class TadoClimate(TadoZoneEntity, ClimateEntity):
|
||||
elif self._current_tado_hvac_mode == CONST_MODE_HEAT:
|
||||
self._target_temp = adjust_temp(self._heat_min_temp, self._heat_max_temp)
|
||||
|
||||
def _control_hvac(
|
||||
async def _control_hvac(
|
||||
self,
|
||||
hvac_mode: str | None = None,
|
||||
target_temp: float | None = None,
|
||||
@ -712,7 +702,9 @@ class TadoClimate(TadoZoneEntity, ClimateEntity):
|
||||
_LOGGER.debug(
|
||||
"Switching to OFF for zone %s (%d)", self.zone_name, self.zone_id
|
||||
)
|
||||
self._tado.set_zone_off(self.zone_id, CONST_OVERLAY_MANUAL, self.zone_type)
|
||||
await self._tado.set_zone_off(
|
||||
self.zone_id, CONST_OVERLAY_MANUAL, self.zone_type
|
||||
)
|
||||
return
|
||||
|
||||
if self._current_tado_hvac_mode == CONST_MODE_SMART_SCHEDULE:
|
||||
@ -721,17 +713,17 @@ class TadoClimate(TadoZoneEntity, ClimateEntity):
|
||||
self.zone_name,
|
||||
self.zone_id,
|
||||
)
|
||||
self._tado.reset_zone_overlay(self.zone_id)
|
||||
await self._tado.reset_zone_overlay(self.zone_id)
|
||||
return
|
||||
|
||||
overlay_mode = decide_overlay_mode(
|
||||
tado=self._tado,
|
||||
coordinator=self._tado,
|
||||
duration=duration,
|
||||
overlay_mode=overlay_mode,
|
||||
zone_id=self.zone_id,
|
||||
)
|
||||
duration = decide_duration(
|
||||
tado=self._tado,
|
||||
coordinator=self._tado,
|
||||
duration=duration,
|
||||
zone_id=self.zone_id,
|
||||
overlay_mode=overlay_mode,
|
||||
@ -785,7 +777,7 @@ class TadoClimate(TadoZoneEntity, ClimateEntity):
|
||||
):
|
||||
swing = self._current_tado_swing_mode
|
||||
|
||||
self._tado.set_zone_overlay(
|
||||
await self._tado.set_zone_overlay(
|
||||
zone_id=self.zone_id,
|
||||
overlay_mode=overlay_mode, # What to do when the period ends
|
||||
temperature=temperature_to_send,
|
||||
@ -800,18 +792,23 @@ class TadoClimate(TadoZoneEntity, ClimateEntity):
|
||||
)
|
||||
|
||||
def _is_valid_setting_for_hvac_mode(self, setting: str) -> bool:
|
||||
return (
|
||||
self._current_tado_capabilities.get(self._current_tado_hvac_mode, {}).get(
|
||||
setting
|
||||
)
|
||||
is not None
|
||||
"""Determine if a setting is valid for the current HVAC mode."""
|
||||
capabilities: str | dict[str, str] = self._current_tado_capabilities.get(
|
||||
self._current_tado_hvac_mode, {}
|
||||
)
|
||||
if isinstance(capabilities, dict):
|
||||
return capabilities.get(setting) is not None
|
||||
return False
|
||||
|
||||
def _is_current_setting_supported_by_current_hvac_mode(
|
||||
self, setting: str, current_state: str | None
|
||||
) -> bool:
|
||||
if self._is_valid_setting_for_hvac_mode(setting):
|
||||
return current_state in self._current_tado_capabilities[
|
||||
self._current_tado_hvac_mode
|
||||
].get(setting, [])
|
||||
"""Determine if the current setting is supported by the current HVAC mode."""
|
||||
capabilities: str | dict[str, str] = self._current_tado_capabilities.get(
|
||||
self._current_tado_hvac_mode, {}
|
||||
)
|
||||
if isinstance(capabilities, dict) and self._is_valid_setting_for_hvac_mode(
|
||||
setting
|
||||
):
|
||||
return current_state in capabilities.get(setting, [])
|
||||
return False
|
||||
|
391
homeassistant/components/tado/coordinator.py
Normal file
391
homeassistant/components/tado/coordinator.py
Normal file
@ -0,0 +1,391 @@
|
||||
"""Coordinator for the Tado integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from PyTado.interface import Tado
|
||||
from requests import RequestException
|
||||
|
||||
from homeassistant.components.climate import PRESET_AWAY, PRESET_HOME
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import (
|
||||
CONF_FALLBACK,
|
||||
CONST_OVERLAY_TADO_DEFAULT,
|
||||
DOMAIN,
|
||||
INSIDE_TEMPERATURE_MEASUREMENT,
|
||||
PRESET_AUTO,
|
||||
TEMP_OFFSET,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=4)
|
||||
SCAN_INTERVAL = timedelta(minutes=5)
|
||||
SCAN_MOBILE_DEVICE_INTERVAL = timedelta(seconds=30)
|
||||
|
||||
type TadoConfigEntry = ConfigEntry[TadoDataUpdateCoordinator]
|
||||
|
||||
|
||||
class TadoDataUpdateCoordinator(DataUpdateCoordinator[dict[str, dict]]):
|
||||
"""Class to manage API calls from and to Tado via PyTado."""
|
||||
|
||||
tado: Tado
|
||||
home_id: int
|
||||
home_name: str
|
||||
config_entry: TadoConfigEntry
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
tado: Tado,
|
||||
debug: bool = False,
|
||||
) -> None:
|
||||
"""Initialize the Tado data update coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
name=DOMAIN,
|
||||
update_interval=SCAN_INTERVAL,
|
||||
)
|
||||
self._tado = tado
|
||||
self._username = entry.data[CONF_USERNAME]
|
||||
self._password = entry.data[CONF_PASSWORD]
|
||||
self._fallback = entry.options.get(CONF_FALLBACK, CONST_OVERLAY_TADO_DEFAULT)
|
||||
self._debug = debug
|
||||
|
||||
self.home_id: int
|
||||
self.home_name: str
|
||||
self.zones: list[dict[Any, Any]] = []
|
||||
self.devices: list[dict[Any, Any]] = []
|
||||
self.data: dict[str, dict] = {
|
||||
"device": {},
|
||||
"weather": {},
|
||||
"geofence": {},
|
||||
"zone": {},
|
||||
}
|
||||
|
||||
@property
|
||||
def fallback(self) -> str:
|
||||
"""Return fallback flag to Smart Schedule."""
|
||||
return self._fallback
|
||||
|
||||
async def _async_update_data(self) -> dict[str, dict]:
|
||||
"""Fetch the (initial) latest data from Tado."""
|
||||
|
||||
try:
|
||||
_LOGGER.debug("Preloading home data")
|
||||
tado_home_call = await self.hass.async_add_executor_job(self._tado.get_me)
|
||||
_LOGGER.debug("Preloading zones and devices")
|
||||
self.zones = await self.hass.async_add_executor_job(self._tado.get_zones)
|
||||
self.devices = await self.hass.async_add_executor_job(
|
||||
self._tado.get_devices
|
||||
)
|
||||
except RequestException as err:
|
||||
raise UpdateFailed(f"Error during Tado setup: {err}") from err
|
||||
|
||||
tado_home = tado_home_call["homes"][0]
|
||||
self.home_id = tado_home["id"]
|
||||
self.home_name = tado_home["name"]
|
||||
|
||||
devices = await self._async_update_devices()
|
||||
zones = await self._async_update_zones()
|
||||
home = await self._async_update_home()
|
||||
|
||||
self.data["device"] = devices
|
||||
self.data["zone"] = zones
|
||||
self.data["weather"] = home["weather"]
|
||||
self.data["geofence"] = home["geofence"]
|
||||
|
||||
return self.data
|
||||
|
||||
async def _async_update_devices(self) -> dict[str, dict]:
|
||||
"""Update the device data from Tado."""
|
||||
|
||||
try:
|
||||
devices = await self.hass.async_add_executor_job(self._tado.get_devices)
|
||||
except RequestException as err:
|
||||
_LOGGER.error("Error updating Tado devices: %s", err)
|
||||
raise UpdateFailed(f"Error updating Tado devices: {err}") from err
|
||||
|
||||
if not devices:
|
||||
_LOGGER.error("No linked devices found for home ID %s", self.home_id)
|
||||
raise UpdateFailed(f"No linked devices found for home ID {self.home_id}")
|
||||
|
||||
return await self.hass.async_add_executor_job(self._update_device_info, devices)
|
||||
|
||||
def _update_device_info(self, devices: list[dict[str, Any]]) -> dict[str, dict]:
|
||||
"""Update the device data from Tado."""
|
||||
mapped_devices: dict[str, dict] = {}
|
||||
for device in devices:
|
||||
device_short_serial_no = device["shortSerialNo"]
|
||||
_LOGGER.debug("Updating device %s", device_short_serial_no)
|
||||
try:
|
||||
if (
|
||||
INSIDE_TEMPERATURE_MEASUREMENT
|
||||
in device["characteristics"]["capabilities"]
|
||||
):
|
||||
_LOGGER.debug(
|
||||
"Updating temperature offset for device %s",
|
||||
device_short_serial_no,
|
||||
)
|
||||
device[TEMP_OFFSET] = self._tado.get_device_info(
|
||||
device_short_serial_no, TEMP_OFFSET
|
||||
)
|
||||
except RequestException as err:
|
||||
_LOGGER.error(
|
||||
"Error updating device %s: %s", device_short_serial_no, err
|
||||
)
|
||||
|
||||
_LOGGER.debug(
|
||||
"Device %s updated, with data: %s", device_short_serial_no, device
|
||||
)
|
||||
mapped_devices[device_short_serial_no] = device
|
||||
|
||||
return mapped_devices
|
||||
|
||||
async def _async_update_zones(self) -> dict[int, dict]:
|
||||
"""Update the zone data from Tado."""
|
||||
|
||||
try:
|
||||
zone_states_call = await self.hass.async_add_executor_job(
|
||||
self._tado.get_zone_states
|
||||
)
|
||||
zone_states = zone_states_call["zoneStates"]
|
||||
except RequestException as err:
|
||||
_LOGGER.error("Error updating Tado zones: %s", err)
|
||||
raise UpdateFailed(f"Error updating Tado zones: {err}") from err
|
||||
|
||||
mapped_zones: dict[int, dict] = {}
|
||||
for zone in zone_states:
|
||||
mapped_zones[int(zone)] = await self._update_zone(int(zone))
|
||||
|
||||
return mapped_zones
|
||||
|
||||
async def _update_zone(self, zone_id: int) -> dict[str, str]:
|
||||
"""Update the internal data of a zone."""
|
||||
|
||||
_LOGGER.debug("Updating zone %s", zone_id)
|
||||
try:
|
||||
data = await self.hass.async_add_executor_job(
|
||||
self._tado.get_zone_state, zone_id
|
||||
)
|
||||
except RequestException as err:
|
||||
_LOGGER.error("Error updating Tado zone %s: %s", zone_id, err)
|
||||
raise UpdateFailed(f"Error updating Tado zone {zone_id}: {err}") from err
|
||||
|
||||
_LOGGER.debug("Zone %s updated, with data: %s", zone_id, data)
|
||||
return data
|
||||
|
||||
async def _async_update_home(self) -> dict[str, dict]:
|
||||
"""Update the home data from Tado."""
|
||||
|
||||
try:
|
||||
weather = await self.hass.async_add_executor_job(self._tado.get_weather)
|
||||
geofence = await self.hass.async_add_executor_job(self._tado.get_home_state)
|
||||
except RequestException as err:
|
||||
_LOGGER.error("Error updating Tado home: %s", err)
|
||||
raise UpdateFailed(f"Error updating Tado home: {err}") from err
|
||||
|
||||
_LOGGER.debug(
|
||||
"Home data updated, with weather and geofence data: %s, %s",
|
||||
weather,
|
||||
geofence,
|
||||
)
|
||||
|
||||
return {"weather": weather, "geofence": geofence}
|
||||
|
||||
async def get_capabilities(self, zone_id: int | str) -> dict:
|
||||
"""Fetch the capabilities from Tado."""
|
||||
|
||||
try:
|
||||
return await self.hass.async_add_executor_job(
|
||||
self._tado.get_capabilities, zone_id
|
||||
)
|
||||
except RequestException as err:
|
||||
raise UpdateFailed(f"Error updating Tado data: {err}") from err
|
||||
|
||||
async def get_auto_geofencing_supported(self) -> bool:
|
||||
"""Fetch the auto geofencing supported from Tado."""
|
||||
|
||||
try:
|
||||
return await self.hass.async_add_executor_job(
|
||||
self._tado.get_auto_geofencing_supported
|
||||
)
|
||||
except RequestException as err:
|
||||
raise UpdateFailed(f"Error updating Tado data: {err}") from err
|
||||
|
||||
async def reset_zone_overlay(self, zone_id):
|
||||
"""Reset the zone back to the default operation."""
|
||||
|
||||
try:
|
||||
await self.hass.async_add_executor_job(
|
||||
self._tado.reset_zone_overlay, zone_id
|
||||
)
|
||||
await self._update_zone(zone_id)
|
||||
except RequestException as err:
|
||||
raise UpdateFailed(f"Error resetting Tado data: {err}") from err
|
||||
|
||||
async def set_presence(
|
||||
self,
|
||||
presence=PRESET_HOME,
|
||||
):
|
||||
"""Set the presence to home, away or auto."""
|
||||
|
||||
if presence == PRESET_AWAY:
|
||||
await self.hass.async_add_executor_job(self._tado.set_away)
|
||||
elif presence == PRESET_HOME:
|
||||
await self.hass.async_add_executor_job(self._tado.set_home)
|
||||
elif presence == PRESET_AUTO:
|
||||
await self.hass.async_add_executor_job(self._tado.set_auto)
|
||||
|
||||
async def set_zone_overlay(
|
||||
self,
|
||||
zone_id=None,
|
||||
overlay_mode=None,
|
||||
temperature=None,
|
||||
duration=None,
|
||||
device_type="HEATING",
|
||||
mode=None,
|
||||
fan_speed=None,
|
||||
swing=None,
|
||||
fan_level=None,
|
||||
vertical_swing=None,
|
||||
horizontal_swing=None,
|
||||
) -> 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, fan_level=%s, vertical_swing=%s, horizontal_swing=%s",
|
||||
zone_id,
|
||||
overlay_mode,
|
||||
temperature,
|
||||
duration,
|
||||
device_type,
|
||||
mode,
|
||||
fan_speed,
|
||||
swing,
|
||||
fan_level,
|
||||
vertical_swing,
|
||||
horizontal_swing,
|
||||
)
|
||||
|
||||
try:
|
||||
await self.hass.async_add_executor_job(
|
||||
self._tado.set_zone_overlay,
|
||||
zone_id,
|
||||
overlay_mode,
|
||||
temperature,
|
||||
duration,
|
||||
device_type,
|
||||
"ON",
|
||||
mode,
|
||||
fan_speed,
|
||||
swing,
|
||||
fan_level,
|
||||
vertical_swing,
|
||||
horizontal_swing,
|
||||
)
|
||||
|
||||
except RequestException as err:
|
||||
raise UpdateFailed(f"Error setting Tado overlay: {err}") from err
|
||||
|
||||
await self._update_zone(zone_id)
|
||||
|
||||
async def set_zone_off(self, zone_id, overlay_mode, device_type="HEATING"):
|
||||
"""Set a zone to off."""
|
||||
try:
|
||||
await self.hass.async_add_executor_job(
|
||||
self._tado.set_zone_overlay,
|
||||
zone_id,
|
||||
overlay_mode,
|
||||
None,
|
||||
None,
|
||||
device_type,
|
||||
"OFF",
|
||||
)
|
||||
except RequestException as err:
|
||||
raise UpdateFailed(f"Error setting Tado overlay: {err}") from err
|
||||
|
||||
await self._update_zone(zone_id)
|
||||
|
||||
async def set_temperature_offset(self, device_id, offset):
|
||||
"""Set temperature offset of device."""
|
||||
try:
|
||||
await self.hass.async_add_executor_job(
|
||||
self._tado.set_temp_offset, device_id, offset
|
||||
)
|
||||
except RequestException as err:
|
||||
raise UpdateFailed(f"Error setting Tado temperature offset: {err}") from err
|
||||
|
||||
async def set_meter_reading(self, reading: int) -> dict[str, Any]:
|
||||
"""Send meter reading to Tado."""
|
||||
dt: str = datetime.now().strftime("%Y-%m-%d")
|
||||
if self._tado is None:
|
||||
raise HomeAssistantError("Tado client is not initialized")
|
||||
|
||||
try:
|
||||
return await self.hass.async_add_executor_job(
|
||||
self._tado.set_eiq_meter_readings, dt, reading
|
||||
)
|
||||
except RequestException as err:
|
||||
raise UpdateFailed(f"Error setting Tado meter reading: {err}") from err
|
||||
|
||||
|
||||
class TadoMobileDeviceUpdateCoordinator(DataUpdateCoordinator[dict[str, dict]]):
|
||||
"""Class to manage the mobile devices from Tado via PyTado."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
tado: Tado,
|
||||
) -> None:
|
||||
"""Initialize the Tado data update coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
name=DOMAIN,
|
||||
update_interval=SCAN_MOBILE_DEVICE_INTERVAL,
|
||||
)
|
||||
self._tado = tado
|
||||
self.data: dict[str, dict] = {}
|
||||
|
||||
async def _async_update_data(self) -> dict[str, dict]:
|
||||
"""Fetch the latest data from Tado."""
|
||||
|
||||
try:
|
||||
mobile_devices = await self.hass.async_add_executor_job(
|
||||
self._tado.get_mobile_devices
|
||||
)
|
||||
except RequestException as err:
|
||||
_LOGGER.error("Error updating Tado mobile devices: %s", err)
|
||||
raise UpdateFailed(f"Error updating Tado mobile devices: {err}") from err
|
||||
|
||||
mapped_mobile_devices: dict[str, dict] = {}
|
||||
for mobile_device in mobile_devices:
|
||||
mobile_device_id = mobile_device["id"]
|
||||
_LOGGER.debug("Updating mobile device %s", mobile_device_id)
|
||||
try:
|
||||
mapped_mobile_devices[mobile_device_id] = mobile_device
|
||||
_LOGGER.debug(
|
||||
"Mobile device %s updated, with data: %s",
|
||||
mobile_device_id,
|
||||
mobile_device,
|
||||
)
|
||||
except RequestException:
|
||||
_LOGGER.error(
|
||||
"Unable to connect to Tado while updating mobile device %s",
|
||||
mobile_device_id,
|
||||
)
|
||||
|
||||
self.data["mobile_device"] = mapped_mobile_devices
|
||||
return self.data
|
@ -11,12 +11,15 @@ from homeassistant.components.device_tracker import (
|
||||
from homeassistant.const import STATE_HOME, STATE_NOT_HOME
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import (
|
||||
CoordinatorEntity,
|
||||
DataUpdateCoordinator,
|
||||
)
|
||||
|
||||
from . import TadoConfigEntry
|
||||
from .const import DOMAIN, SIGNAL_TADO_MOBILE_DEVICE_UPDATE_RECEIVED
|
||||
from .tado_connector import TadoConnector
|
||||
from .const import DOMAIN
|
||||
from .coordinator import TadoMobileDeviceUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -28,7 +31,7 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up the Tado device scannery entity."""
|
||||
_LOGGER.debug("Setting up Tado device scanner entity")
|
||||
tado = entry.runtime_data
|
||||
tado = entry.runtime_data.mobile_coordinator
|
||||
tracked: set = set()
|
||||
|
||||
# Fix non-string unique_id for device trackers
|
||||
@ -49,58 +52,56 @@ async def async_setup_entry(
|
||||
|
||||
update_devices()
|
||||
|
||||
entry.async_on_unload(
|
||||
async_dispatcher_connect(
|
||||
hass,
|
||||
SIGNAL_TADO_MOBILE_DEVICE_UPDATE_RECEIVED.format(tado.home_id),
|
||||
update_devices,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
def add_tracked_entities(
|
||||
hass: HomeAssistant,
|
||||
tado: TadoConnector,
|
||||
coordinator: TadoMobileDeviceUpdateCoordinator,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
tracked: set[str],
|
||||
) -> None:
|
||||
"""Add new tracker entities from Tado."""
|
||||
_LOGGER.debug("Fetching Tado devices from API for (newly) tracked entities")
|
||||
new_tracked = []
|
||||
for device_key, device in tado.data["mobile_device"].items():
|
||||
for device_key, device in coordinator.data["mobile_device"].items():
|
||||
if device_key in tracked:
|
||||
continue
|
||||
|
||||
_LOGGER.debug(
|
||||
"Adding Tado device %s with deviceID %s", device["name"], device_key
|
||||
)
|
||||
new_tracked.append(TadoDeviceTrackerEntity(device_key, device["name"], tado))
|
||||
new_tracked.append(
|
||||
TadoDeviceTrackerEntity(device_key, device["name"], coordinator)
|
||||
)
|
||||
tracked.add(device_key)
|
||||
|
||||
async_add_entities(new_tracked)
|
||||
|
||||
|
||||
class TadoDeviceTrackerEntity(TrackerEntity):
|
||||
class TadoDeviceTrackerEntity(CoordinatorEntity[DataUpdateCoordinator], TrackerEntity):
|
||||
"""A Tado Device Tracker entity."""
|
||||
|
||||
_attr_should_poll = False
|
||||
_attr_available = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
device_id: str,
|
||||
device_name: str,
|
||||
tado: TadoConnector,
|
||||
coordinator: TadoMobileDeviceUpdateCoordinator,
|
||||
) -> None:
|
||||
"""Initialize a Tado Device Tracker entity."""
|
||||
super().__init__()
|
||||
super().__init__(coordinator)
|
||||
self._attr_unique_id = str(device_id)
|
||||
self._device_id = device_id
|
||||
self._device_name = device_name
|
||||
self._tado = tado
|
||||
self._active = False
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the coordinator."""
|
||||
self.update_state()
|
||||
super()._handle_coordinator_update()
|
||||
|
||||
@callback
|
||||
def update_state(self) -> None:
|
||||
"""Update the Tado device."""
|
||||
@ -109,7 +110,7 @@ class TadoDeviceTrackerEntity(TrackerEntity):
|
||||
self._device_name,
|
||||
self._device_id,
|
||||
)
|
||||
device = self._tado.data["mobile_device"][self._device_id]
|
||||
device = self.coordinator.data["mobile_device"][self._device_id]
|
||||
|
||||
self._attr_available = False
|
||||
_LOGGER.debug(
|
||||
@ -129,25 +130,6 @@ class TadoDeviceTrackerEntity(TrackerEntity):
|
||||
else:
|
||||
_LOGGER.debug("Tado device %s is not at home", device["name"])
|
||||
|
||||
@callback
|
||||
def on_demand_update(self) -> None:
|
||||
"""Update state on demand."""
|
||||
self.update_state()
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register state update callback."""
|
||||
_LOGGER.debug("Registering Tado device tracker entity")
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(
|
||||
self.hass,
|
||||
SIGNAL_TADO_MOBILE_DEVICE_UPDATE_RECEIVED.format(self._tado.home_id),
|
||||
self.on_demand_update,
|
||||
)
|
||||
)
|
||||
|
||||
self.update_state()
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return the name of the device."""
|
||||
|
@ -1,21 +1,30 @@
|
||||
"""Base class for Tado entity."""
|
||||
|
||||
import logging
|
||||
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from . import TadoConnector
|
||||
from .const import DEFAULT_NAME, DOMAIN, TADO_HOME, TADO_ZONE
|
||||
from .coordinator import TadoDataUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TadoDeviceEntity(Entity):
|
||||
"""Base implementation for Tado device."""
|
||||
class TadoCoordinatorEntity(CoordinatorEntity[TadoDataUpdateCoordinator]):
|
||||
"""Base class for Tado entity."""
|
||||
|
||||
_attr_should_poll = False
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(self, device_info: dict[str, str]) -> None:
|
||||
|
||||
class TadoDeviceEntity(TadoCoordinatorEntity):
|
||||
"""Base implementation for Tado device."""
|
||||
|
||||
def __init__(
|
||||
self, device_info: dict[str, str], coordinator: TadoDataUpdateCoordinator
|
||||
) -> None:
|
||||
"""Initialize a Tado device."""
|
||||
super().__init__()
|
||||
super().__init__(coordinator)
|
||||
self._device_info = device_info
|
||||
self.device_name = device_info["serialNo"]
|
||||
self.device_id = device_info["shortSerialNo"]
|
||||
@ -30,35 +39,35 @@ class TadoDeviceEntity(Entity):
|
||||
)
|
||||
|
||||
|
||||
class TadoHomeEntity(Entity):
|
||||
class TadoHomeEntity(TadoCoordinatorEntity):
|
||||
"""Base implementation for Tado home."""
|
||||
|
||||
_attr_should_poll = False
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(self, tado: TadoConnector) -> None:
|
||||
def __init__(self, coordinator: TadoDataUpdateCoordinator) -> None:
|
||||
"""Initialize a Tado home."""
|
||||
super().__init__()
|
||||
self.home_name = tado.home_name
|
||||
self.home_id = tado.home_id
|
||||
super().__init__(coordinator)
|
||||
self.home_name = coordinator.home_name
|
||||
self.home_id = coordinator.home_id
|
||||
self._attr_device_info = DeviceInfo(
|
||||
configuration_url="https://app.tado.com",
|
||||
identifiers={(DOMAIN, str(tado.home_id))},
|
||||
identifiers={(DOMAIN, str(coordinator.home_id))},
|
||||
manufacturer=DEFAULT_NAME,
|
||||
model=TADO_HOME,
|
||||
name=tado.home_name,
|
||||
name=coordinator.home_name,
|
||||
)
|
||||
|
||||
|
||||
class TadoZoneEntity(Entity):
|
||||
class TadoZoneEntity(TadoCoordinatorEntity):
|
||||
"""Base implementation for Tado zone."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_should_poll = False
|
||||
|
||||
def __init__(self, zone_name: str, home_id: int, zone_id: int) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
zone_name: str,
|
||||
home_id: int,
|
||||
zone_id: int,
|
||||
coordinator: TadoDataUpdateCoordinator,
|
||||
) -> None:
|
||||
"""Initialize a Tado zone."""
|
||||
super().__init__()
|
||||
super().__init__(coordinator)
|
||||
self.zone_name = zone_name
|
||||
self.zone_id = zone_id
|
||||
self._attr_device_info = DeviceInfo(
|
||||
|
@ -5,26 +5,27 @@ from .const import (
|
||||
CONST_OVERLAY_TADO_MODE,
|
||||
CONST_OVERLAY_TIMER,
|
||||
)
|
||||
from .tado_connector import TadoConnector
|
||||
from .coordinator import TadoDataUpdateCoordinator
|
||||
|
||||
|
||||
def decide_overlay_mode(
|
||||
tado: TadoConnector,
|
||||
coordinator: TadoDataUpdateCoordinator,
|
||||
duration: int | None,
|
||||
zone_id: int,
|
||||
overlay_mode: str | None = None,
|
||||
) -> str:
|
||||
"""Return correct overlay mode based on the action and defaults."""
|
||||
|
||||
# If user gave duration then overlay mode needs to be timer
|
||||
if duration:
|
||||
return CONST_OVERLAY_TIMER
|
||||
# If no duration or timer set to fallback setting
|
||||
if overlay_mode is None:
|
||||
overlay_mode = tado.fallback or CONST_OVERLAY_TADO_MODE
|
||||
overlay_mode = coordinator.fallback or CONST_OVERLAY_TADO_MODE
|
||||
# If default is Tado default then look it up
|
||||
if overlay_mode == CONST_OVERLAY_TADO_DEFAULT:
|
||||
overlay_mode = (
|
||||
tado.data["zone"][zone_id].default_overlay_termination_type
|
||||
coordinator.data["zone"][zone_id].default_overlay_termination_type
|
||||
or CONST_OVERLAY_TADO_MODE
|
||||
)
|
||||
|
||||
@ -32,18 +33,19 @@ def decide_overlay_mode(
|
||||
|
||||
|
||||
def decide_duration(
|
||||
tado: TadoConnector,
|
||||
coordinator: TadoDataUpdateCoordinator,
|
||||
duration: int | None,
|
||||
zone_id: int,
|
||||
overlay_mode: str | None = None,
|
||||
) -> None | int:
|
||||
"""Return correct duration based on the selected overlay mode/duration and tado config."""
|
||||
|
||||
# If we ended up with a timer but no duration, set a default duration
|
||||
# If we ended up with a timer but no duration, set a default duration
|
||||
if overlay_mode == CONST_OVERLAY_TIMER and duration is None:
|
||||
duration = (
|
||||
int(tado.data["zone"][zone_id].default_overlay_termination_duration)
|
||||
if tado.data["zone"][zone_id].default_overlay_termination_duration
|
||||
int(coordinator.data["zone"][zone_id].default_overlay_termination_duration)
|
||||
if coordinator.data["zone"][zone_id].default_overlay_termination_duration
|
||||
is not None
|
||||
else 3600
|
||||
)
|
||||
@ -53,6 +55,7 @@ def decide_duration(
|
||||
|
||||
def generate_supported_fanmodes(tado_to_ha_mapping: dict[str, str], options: list[str]):
|
||||
"""Return correct list of fan modes or None."""
|
||||
|
||||
supported_fanmodes = [
|
||||
tado_to_ha_mapping.get(option)
|
||||
for option in options
|
||||
|
13
homeassistant/components/tado/models.py
Normal file
13
homeassistant/components/tado/models.py
Normal file
@ -0,0 +1,13 @@
|
||||
"""Models for use in Tado integration."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from .coordinator import TadoDataUpdateCoordinator, TadoMobileDeviceUpdateCoordinator
|
||||
|
||||
|
||||
@dataclass
|
||||
class TadoData:
|
||||
"""Class to hold Tado data."""
|
||||
|
||||
coordinator: TadoDataUpdateCoordinator
|
||||
mobile_coordinator: TadoMobileDeviceUpdateCoordinator
|
@ -15,7 +15,6 @@ from homeassistant.components.sensor import (
|
||||
)
|
||||
from homeassistant.const import PERCENTAGE, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
|
||||
@ -24,13 +23,12 @@ from .const import (
|
||||
CONDITIONS_MAP,
|
||||
SENSOR_DATA_CATEGORY_GEOFENCE,
|
||||
SENSOR_DATA_CATEGORY_WEATHER,
|
||||
SIGNAL_TADO_UPDATE_RECEIVED,
|
||||
TYPE_AIR_CONDITIONING,
|
||||
TYPE_HEATING,
|
||||
TYPE_HOT_WATER,
|
||||
)
|
||||
from .coordinator import TadoDataUpdateCoordinator
|
||||
from .entity import TadoHomeEntity, TadoZoneEntity
|
||||
from .tado_connector import TadoConnector
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -197,7 +195,7 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up the Tado sensor platform."""
|
||||
|
||||
tado = entry.runtime_data
|
||||
tado = entry.runtime_data.coordinator
|
||||
zones = tado.zones
|
||||
entities: list[SensorEntity] = []
|
||||
|
||||
@ -232,39 +230,22 @@ class TadoHomeSensor(TadoHomeEntity, SensorEntity):
|
||||
entity_description: TadoSensorEntityDescription
|
||||
|
||||
def __init__(
|
||||
self, tado: TadoConnector, entity_description: TadoSensorEntityDescription
|
||||
self,
|
||||
coordinator: TadoDataUpdateCoordinator,
|
||||
entity_description: TadoSensorEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize of the Tado Sensor."""
|
||||
self.entity_description = entity_description
|
||||
super().__init__(tado)
|
||||
self._tado = tado
|
||||
super().__init__(coordinator)
|
||||
|
||||
self._attr_unique_id = f"{entity_description.key} {tado.home_id}"
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register for sensor updates."""
|
||||
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(
|
||||
self.hass,
|
||||
SIGNAL_TADO_UPDATE_RECEIVED.format(self._tado.home_id, "home", "data"),
|
||||
self._async_update_callback,
|
||||
)
|
||||
)
|
||||
self._async_update_home_data()
|
||||
self._attr_unique_id = f"{entity_description.key} {coordinator.home_id}"
|
||||
|
||||
@callback
|
||||
def _async_update_callback(self) -> None:
|
||||
"""Update and write state."""
|
||||
self._async_update_home_data()
|
||||
self.async_write_ha_state()
|
||||
|
||||
@callback
|
||||
def _async_update_home_data(self) -> None:
|
||||
"""Handle update callbacks."""
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the coordinator."""
|
||||
try:
|
||||
tado_weather_data = self._tado.data["weather"]
|
||||
tado_geofence_data = self._tado.data["geofence"]
|
||||
tado_weather_data = self.coordinator.data["weather"]
|
||||
tado_geofence_data = self.coordinator.data["geofence"]
|
||||
except KeyError:
|
||||
return
|
||||
|
||||
@ -278,6 +259,7 @@ class TadoHomeSensor(TadoHomeEntity, SensorEntity):
|
||||
self._attr_extra_state_attributes = self.entity_description.attributes_fn(
|
||||
tado_sensor_data
|
||||
)
|
||||
super()._handle_coordinator_update()
|
||||
|
||||
|
||||
class TadoZoneSensor(TadoZoneEntity, SensorEntity):
|
||||
@ -287,43 +269,24 @@ class TadoZoneSensor(TadoZoneEntity, SensorEntity):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
tado: TadoConnector,
|
||||
coordinator: TadoDataUpdateCoordinator,
|
||||
zone_name: str,
|
||||
zone_id: int,
|
||||
entity_description: TadoSensorEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize of the Tado Sensor."""
|
||||
self.entity_description = entity_description
|
||||
self._tado = tado
|
||||
super().__init__(zone_name, tado.home_id, zone_id)
|
||||
super().__init__(zone_name, coordinator.home_id, zone_id, coordinator)
|
||||
|
||||
self._attr_unique_id = f"{entity_description.key} {zone_id} {tado.home_id}"
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register for sensor updates."""
|
||||
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(
|
||||
self.hass,
|
||||
SIGNAL_TADO_UPDATE_RECEIVED.format(
|
||||
self._tado.home_id, "zone", self.zone_id
|
||||
),
|
||||
self._async_update_callback,
|
||||
)
|
||||
self._attr_unique_id = (
|
||||
f"{entity_description.key} {zone_id} {coordinator.home_id}"
|
||||
)
|
||||
self._async_update_zone_data()
|
||||
|
||||
@callback
|
||||
def _async_update_callback(self) -> None:
|
||||
"""Update and write state."""
|
||||
self._async_update_zone_data()
|
||||
self.async_write_ha_state()
|
||||
|
||||
@callback
|
||||
def _async_update_zone_data(self) -> None:
|
||||
"""Handle update callbacks."""
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the coordinator."""
|
||||
try:
|
||||
tado_zone_data = self._tado.data["zone"][self.zone_id]
|
||||
tado_zone_data = self.coordinator.data["zone"][self.zone_id]
|
||||
except KeyError:
|
||||
return
|
||||
|
||||
@ -332,3 +295,4 @@ class TadoZoneSensor(TadoZoneEntity, SensorEntity):
|
||||
self._attr_extra_state_attributes = self.entity_description.attributes_fn(
|
||||
tado_zone_data
|
||||
)
|
||||
super()._handle_coordinator_update()
|
||||
|
@ -43,11 +43,8 @@ def setup_services(hass: HomeAssistant) -> None:
|
||||
if entry is None:
|
||||
raise ServiceValidationError("Config entry not found")
|
||||
|
||||
tadoconnector = entry.runtime_data
|
||||
|
||||
response: dict = await hass.async_add_executor_job(
|
||||
tadoconnector.set_meter_reading, call.data[CONF_READING]
|
||||
)
|
||||
coordinator = entry.runtime_data.coordinator
|
||||
response: dict = await coordinator.set_meter_reading(call.data[CONF_READING])
|
||||
|
||||
if ATTR_MESSAGE in response:
|
||||
raise HomeAssistantError(response[ATTR_MESSAGE])
|
||||
|
@ -1,332 +0,0 @@
|
||||
"""Tado Connector a class to store the data as an object."""
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from PyTado.interface import Tado
|
||||
from requests import RequestException
|
||||
|
||||
from homeassistant.components.climate import PRESET_AWAY, PRESET_HOME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.dispatcher import dispatcher_send
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
from .const import (
|
||||
INSIDE_TEMPERATURE_MEASUREMENT,
|
||||
PRESET_AUTO,
|
||||
SIGNAL_TADO_MOBILE_DEVICE_UPDATE_RECEIVED,
|
||||
SIGNAL_TADO_UPDATE_RECEIVED,
|
||||
TEMP_OFFSET,
|
||||
)
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=4)
|
||||
SCAN_INTERVAL = timedelta(minutes=5)
|
||||
SCAN_MOBILE_DEVICE_INTERVAL = timedelta(seconds=30)
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TadoConnector:
|
||||
"""An object to store the Tado data."""
|
||||
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, username: str, password: str, fallback: str
|
||||
) -> None:
|
||||
"""Initialize Tado Connector."""
|
||||
self.hass = hass
|
||||
self._username = username
|
||||
self._password = password
|
||||
self._fallback = fallback
|
||||
|
||||
self.home_id: int = 0
|
||||
self.home_name = None
|
||||
self.tado = None
|
||||
self.zones: list[dict[Any, Any]] = []
|
||||
self.devices: list[dict[Any, Any]] = []
|
||||
self.data: dict[str, dict] = {
|
||||
"device": {},
|
||||
"mobile_device": {},
|
||||
"weather": {},
|
||||
"geofence": {},
|
||||
"zone": {},
|
||||
}
|
||||
|
||||
@property
|
||||
def fallback(self):
|
||||
"""Return fallback flag to Smart Schedule."""
|
||||
return self._fallback
|
||||
|
||||
def setup(self):
|
||||
"""Connect to Tado and fetch the zones."""
|
||||
self.tado = Tado(self._username, self._password)
|
||||
# Load zones and devices
|
||||
self.zones = self.tado.get_zones()
|
||||
self.devices = self.tado.get_devices()
|
||||
tado_home = self.tado.get_me()["homes"][0]
|
||||
self.home_id = tado_home["id"]
|
||||
self.home_name = tado_home["name"]
|
||||
|
||||
def get_mobile_devices(self):
|
||||
"""Return the Tado mobile devices."""
|
||||
return self.tado.get_mobile_devices()
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
"""Update the registered zones."""
|
||||
self.update_devices()
|
||||
self.update_mobile_devices()
|
||||
self.update_zones()
|
||||
self.update_home()
|
||||
|
||||
def update_mobile_devices(self) -> None:
|
||||
"""Update the mobile devices."""
|
||||
try:
|
||||
mobile_devices = self.get_mobile_devices()
|
||||
except RuntimeError:
|
||||
_LOGGER.error("Unable to connect to Tado while updating mobile devices")
|
||||
return
|
||||
|
||||
if not mobile_devices:
|
||||
_LOGGER.debug("No linked mobile devices found for home ID %s", self.home_id)
|
||||
return
|
||||
|
||||
# Errors are planned to be converted to exceptions
|
||||
# in PyTado library, so this can be removed
|
||||
if isinstance(mobile_devices, dict) and mobile_devices.get("errors"):
|
||||
_LOGGER.error(
|
||||
"Error for home ID %s while updating mobile devices: %s",
|
||||
self.home_id,
|
||||
mobile_devices["errors"],
|
||||
)
|
||||
return
|
||||
|
||||
for mobile_device in mobile_devices:
|
||||
self.data["mobile_device"][mobile_device["id"]] = mobile_device
|
||||
_LOGGER.debug(
|
||||
"Dispatching update to %s mobile device: %s",
|
||||
self.home_id,
|
||||
mobile_device,
|
||||
)
|
||||
|
||||
dispatcher_send(
|
||||
self.hass,
|
||||
SIGNAL_TADO_MOBILE_DEVICE_UPDATE_RECEIVED.format(self.home_id),
|
||||
)
|
||||
|
||||
def update_devices(self):
|
||||
"""Update the device data from Tado."""
|
||||
try:
|
||||
devices = self.tado.get_devices()
|
||||
except RuntimeError:
|
||||
_LOGGER.error("Unable to connect to Tado while updating devices")
|
||||
return
|
||||
|
||||
if not devices:
|
||||
_LOGGER.debug("No linked devices found for home ID %s", self.home_id)
|
||||
return
|
||||
|
||||
# Errors are planned to be converted to exceptions
|
||||
# in PyTado library, so this can be removed
|
||||
if isinstance(devices, dict) and devices.get("errors"):
|
||||
_LOGGER.error(
|
||||
"Error for home ID %s while updating devices: %s",
|
||||
self.home_id,
|
||||
devices["errors"],
|
||||
)
|
||||
return
|
||||
|
||||
for device in devices:
|
||||
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
|
||||
)
|
||||
except RuntimeError:
|
||||
_LOGGER.error(
|
||||
"Unable to connect to Tado while updating device %s",
|
||||
device_short_serial_no,
|
||||
)
|
||||
return
|
||||
|
||||
self.data["device"][device_short_serial_no] = device
|
||||
|
||||
_LOGGER.debug(
|
||||
"Dispatching update to %s device %s: %s",
|
||||
self.home_id,
|
||||
device_short_serial_no,
|
||||
device,
|
||||
)
|
||||
dispatcher_send(
|
||||
self.hass,
|
||||
SIGNAL_TADO_UPDATE_RECEIVED.format(
|
||||
self.home_id, "device", device_short_serial_no
|
||||
),
|
||||
)
|
||||
|
||||
def update_zones(self):
|
||||
"""Update the zone data from Tado."""
|
||||
try:
|
||||
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))
|
||||
|
||||
def update_zone(self, zone_id):
|
||||
"""Update the internal data from Tado."""
|
||||
_LOGGER.debug("Updating zone %s", zone_id)
|
||||
try:
|
||||
data = self.tado.get_zone_state(zone_id)
|
||||
except RuntimeError:
|
||||
_LOGGER.error("Unable to connect to Tado while updating zone %s", zone_id)
|
||||
return
|
||||
|
||||
self.data["zone"][zone_id] = data
|
||||
|
||||
_LOGGER.debug(
|
||||
"Dispatching update to %s zone %s: %s",
|
||||
self.home_id,
|
||||
zone_id,
|
||||
data,
|
||||
)
|
||||
dispatcher_send(
|
||||
self.hass,
|
||||
SIGNAL_TADO_UPDATE_RECEIVED.format(self.home_id, "zone", zone_id),
|
||||
)
|
||||
|
||||
def update_home(self):
|
||||
"""Update the home data from Tado."""
|
||||
try:
|
||||
self.data["weather"] = self.tado.get_weather()
|
||||
self.data["geofence"] = self.tado.get_home_state()
|
||||
dispatcher_send(
|
||||
self.hass,
|
||||
SIGNAL_TADO_UPDATE_RECEIVED.format(self.home_id, "home", "data"),
|
||||
)
|
||||
except RuntimeError:
|
||||
_LOGGER.error(
|
||||
"Unable to connect to Tado while updating weather and geofence data"
|
||||
)
|
||||
return
|
||||
|
||||
def get_capabilities(self, zone_id):
|
||||
"""Return the capabilities of the devices."""
|
||||
return self.tado.get_capabilities(zone_id)
|
||||
|
||||
def get_auto_geofencing_supported(self):
|
||||
"""Return whether the Tado Home supports auto geofencing."""
|
||||
return self.tado.get_auto_geofencing_supported()
|
||||
|
||||
def reset_zone_overlay(self, zone_id):
|
||||
"""Reset the zone back to the default operation."""
|
||||
self.tado.reset_zone_overlay(zone_id)
|
||||
self.update_zone(zone_id)
|
||||
|
||||
def set_presence(
|
||||
self,
|
||||
presence=PRESET_HOME,
|
||||
):
|
||||
"""Set the presence to home, away or auto."""
|
||||
if presence == PRESET_AWAY:
|
||||
self.tado.set_away()
|
||||
elif presence == PRESET_HOME:
|
||||
self.tado.set_home()
|
||||
elif presence == PRESET_AUTO:
|
||||
self.tado.set_auto()
|
||||
|
||||
# Update everything when changing modes
|
||||
self.update_zones()
|
||||
self.update_home()
|
||||
|
||||
def set_zone_overlay(
|
||||
self,
|
||||
zone_id=None,
|
||||
overlay_mode=None,
|
||||
temperature=None,
|
||||
duration=None,
|
||||
device_type="HEATING",
|
||||
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 fan_level=%s vertical_swing=%s horizontal_swing=%s"
|
||||
),
|
||||
zone_id,
|
||||
overlay_mode,
|
||||
temperature,
|
||||
duration,
|
||||
device_type,
|
||||
mode,
|
||||
fan_speed,
|
||||
swing,
|
||||
fan_level,
|
||||
vertical_swing,
|
||||
horizontal_swing,
|
||||
)
|
||||
|
||||
try:
|
||||
self.tado.set_zone_overlay(
|
||||
zone_id,
|
||||
overlay_mode,
|
||||
temperature,
|
||||
duration,
|
||||
device_type,
|
||||
"ON",
|
||||
mode,
|
||||
fan_speed=fan_speed,
|
||||
swing=swing,
|
||||
fan_level=fan_level,
|
||||
vertical_swing=vertical_swing,
|
||||
horizontal_swing=horizontal_swing,
|
||||
)
|
||||
|
||||
except RequestException as exc:
|
||||
_LOGGER.error("Could not set zone overlay: %s", exc)
|
||||
|
||||
self.update_zone(zone_id)
|
||||
|
||||
def set_zone_off(self, zone_id, overlay_mode, device_type="HEATING"):
|
||||
"""Set a zone to off."""
|
||||
try:
|
||||
self.tado.set_zone_overlay(
|
||||
zone_id, overlay_mode, None, None, device_type, "OFF"
|
||||
)
|
||||
except RequestException as exc:
|
||||
_LOGGER.error("Could not set zone overlay: %s", exc)
|
||||
|
||||
self.update_zone(zone_id)
|
||||
|
||||
def set_temperature_offset(self, device_id, offset):
|
||||
"""Set temperature offset of device."""
|
||||
try:
|
||||
self.tado.set_temp_offset(device_id, offset)
|
||||
except RequestException as exc:
|
||||
_LOGGER.error("Could not set temperature offset: %s", exc)
|
||||
|
||||
def set_meter_reading(self, reading: int) -> dict[str, Any]:
|
||||
"""Send meter reading to Tado."""
|
||||
dt: str = datetime.now().strftime("%Y-%m-%d")
|
||||
if self.tado is None:
|
||||
raise HomeAssistantError("Tado client is not initialized")
|
||||
|
||||
try:
|
||||
return self.tado.set_eiq_meter_readings(date=dt, reading=reading)
|
||||
except RequestException as exc:
|
||||
raise HomeAssistantError("Could not set meter reading") from exc
|
@ -12,7 +12,6 @@ from homeassistant.components.water_heater import (
|
||||
from homeassistant.const import ATTR_TEMPERATURE, 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
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import VolDictType
|
||||
|
||||
@ -26,13 +25,12 @@ from .const import (
|
||||
CONST_OVERLAY_MANUAL,
|
||||
CONST_OVERLAY_TADO_MODE,
|
||||
CONST_OVERLAY_TIMER,
|
||||
SIGNAL_TADO_UPDATE_RECEIVED,
|
||||
TYPE_HOT_WATER,
|
||||
)
|
||||
from .coordinator import TadoDataUpdateCoordinator
|
||||
from .entity import TadoZoneEntity
|
||||
from .helper import decide_duration, decide_overlay_mode
|
||||
from .repairs import manage_water_heater_fallback_issue
|
||||
from .tado_connector import TadoConnector
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -67,8 +65,9 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up the Tado water heater platform."""
|
||||
|
||||
tado = entry.runtime_data
|
||||
entities = await hass.async_add_executor_job(_generate_entities, tado)
|
||||
data = entry.runtime_data
|
||||
coordinator = data.coordinator
|
||||
entities = await _generate_entities(coordinator)
|
||||
|
||||
platform = entity_platform.async_get_current_platform()
|
||||
|
||||
@ -83,27 +82,29 @@ async def async_setup_entry(
|
||||
manage_water_heater_fallback_issue(
|
||||
hass=hass,
|
||||
water_heater_names=[e.zone_name for e in entities],
|
||||
integration_overlay_fallback=tado.fallback,
|
||||
integration_overlay_fallback=coordinator.fallback,
|
||||
)
|
||||
|
||||
|
||||
def _generate_entities(tado: TadoConnector) -> list:
|
||||
async def _generate_entities(coordinator: TadoDataUpdateCoordinator) -> list:
|
||||
"""Create all water heater entities."""
|
||||
entities = []
|
||||
|
||||
for zone in tado.zones:
|
||||
for zone in coordinator.zones:
|
||||
if zone["type"] == TYPE_HOT_WATER:
|
||||
entity = create_water_heater_entity(
|
||||
tado, zone["name"], zone["id"], str(zone["name"])
|
||||
entity = await create_water_heater_entity(
|
||||
coordinator, zone["name"], zone["id"], str(zone["name"])
|
||||
)
|
||||
entities.append(entity)
|
||||
|
||||
return entities
|
||||
|
||||
|
||||
def create_water_heater_entity(tado: TadoConnector, name: str, zone_id: int, zone: str):
|
||||
async def create_water_heater_entity(
|
||||
coordinator: TadoDataUpdateCoordinator, name: str, zone_id: int, zone: str
|
||||
):
|
||||
"""Create a Tado water heater device."""
|
||||
capabilities = tado.get_capabilities(zone_id)
|
||||
capabilities = await coordinator.get_capabilities(zone_id)
|
||||
|
||||
supports_temperature_control = capabilities["canSetTemperature"]
|
||||
|
||||
@ -116,7 +117,7 @@ def create_water_heater_entity(tado: TadoConnector, name: str, zone_id: int, zon
|
||||
max_temp = None
|
||||
|
||||
return TadoWaterHeater(
|
||||
tado,
|
||||
coordinator,
|
||||
name,
|
||||
zone_id,
|
||||
supports_temperature_control,
|
||||
@ -134,7 +135,7 @@ class TadoWaterHeater(TadoZoneEntity, WaterHeaterEntity):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
tado: TadoConnector,
|
||||
coordinator: TadoDataUpdateCoordinator,
|
||||
zone_name: str,
|
||||
zone_id: int,
|
||||
supports_temperature_control: bool,
|
||||
@ -142,11 +143,10 @@ class TadoWaterHeater(TadoZoneEntity, WaterHeaterEntity):
|
||||
max_temp,
|
||||
) -> None:
|
||||
"""Initialize of Tado water heater entity."""
|
||||
self._tado = tado
|
||||
super().__init__(zone_name, tado.home_id, zone_id)
|
||||
super().__init__(zone_name, coordinator.home_id, zone_id, coordinator)
|
||||
|
||||
self.zone_id = zone_id
|
||||
self._attr_unique_id = f"{zone_id} {tado.home_id}"
|
||||
self._attr_unique_id = f"{zone_id} {coordinator.home_id}"
|
||||
|
||||
self._device_is_active = False
|
||||
|
||||
@ -164,19 +164,14 @@ class TadoWaterHeater(TadoZoneEntity, WaterHeaterEntity):
|
||||
self._overlay_mode = CONST_MODE_SMART_SCHEDULE
|
||||
self._tado_zone_data: Any = None
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register for sensor updates."""
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(
|
||||
self.hass,
|
||||
SIGNAL_TADO_UPDATE_RECEIVED.format(
|
||||
self._tado.home_id, "zone", self.zone_id
|
||||
),
|
||||
self._async_update_callback,
|
||||
)
|
||||
)
|
||||
self._async_update_data()
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the coordinator."""
|
||||
self._async_update_data()
|
||||
super()._handle_coordinator_update()
|
||||
|
||||
@property
|
||||
def current_operation(self) -> str | None:
|
||||
"""Return current readable operation mode."""
|
||||
@ -202,7 +197,7 @@ class TadoWaterHeater(TadoZoneEntity, WaterHeaterEntity):
|
||||
"""Return the maximum temperature."""
|
||||
return self._max_temperature
|
||||
|
||||
def set_operation_mode(self, operation_mode: str) -> None:
|
||||
async def async_set_operation_mode(self, operation_mode: str) -> None:
|
||||
"""Set new operation mode."""
|
||||
mode = None
|
||||
|
||||
@ -213,18 +208,20 @@ class TadoWaterHeater(TadoZoneEntity, WaterHeaterEntity):
|
||||
elif operation_mode == MODE_HEAT:
|
||||
mode = CONST_MODE_HEAT
|
||||
|
||||
self._control_heater(hvac_mode=mode)
|
||||
await self._control_heater(hvac_mode=mode)
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
def set_timer(self, time_period: int, temperature: float | None = None):
|
||||
async def set_timer(self, time_period: int, temperature: float | None = None):
|
||||
"""Set the timer on the entity, and temperature if supported."""
|
||||
if not self._supports_temperature_control and temperature is not None:
|
||||
temperature = None
|
||||
|
||||
self._control_heater(
|
||||
await self._control_heater(
|
||||
hvac_mode=CONST_MODE_HEAT, target_temp=temperature, duration=time_period
|
||||
)
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
def set_temperature(self, **kwargs: Any) -> None:
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
"""Set new target temperature."""
|
||||
temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||
if not self._supports_temperature_control or temperature is None:
|
||||
@ -235,10 +232,11 @@ class TadoWaterHeater(TadoZoneEntity, WaterHeaterEntity):
|
||||
CONST_MODE_AUTO,
|
||||
CONST_MODE_SMART_SCHEDULE,
|
||||
):
|
||||
self._control_heater(target_temp=temperature)
|
||||
await self._control_heater(target_temp=temperature)
|
||||
return
|
||||
|
||||
self._control_heater(target_temp=temperature, hvac_mode=CONST_MODE_HEAT)
|
||||
await self._control_heater(target_temp=temperature, hvac_mode=CONST_MODE_HEAT)
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
@callback
|
||||
def _async_update_callback(self) -> None:
|
||||
@ -250,10 +248,10 @@ class TadoWaterHeater(TadoZoneEntity, WaterHeaterEntity):
|
||||
def _async_update_data(self) -> None:
|
||||
"""Load tado data."""
|
||||
_LOGGER.debug("Updating water_heater platform for zone %d", self.zone_id)
|
||||
self._tado_zone_data = self._tado.data["zone"][self.zone_id]
|
||||
self._tado_zone_data = self.coordinator.data["zone"][self.zone_id]
|
||||
self._current_tado_hvac_mode = self._tado_zone_data.current_hvac_mode
|
||||
|
||||
def _control_heater(
|
||||
async def _control_heater(
|
||||
self,
|
||||
hvac_mode: str | None = None,
|
||||
target_temp: float | None = None,
|
||||
@ -276,23 +274,26 @@ class TadoWaterHeater(TadoZoneEntity, WaterHeaterEntity):
|
||||
self.zone_name,
|
||||
self.zone_id,
|
||||
)
|
||||
self._tado.reset_zone_overlay(self.zone_id)
|
||||
await self.coordinator.reset_zone_overlay(self.zone_id)
|
||||
await self.coordinator.async_request_refresh()
|
||||
return
|
||||
|
||||
if self._current_tado_hvac_mode == CONST_MODE_OFF:
|
||||
_LOGGER.debug(
|
||||
"Switching to OFF for zone %s (%d)", self.zone_name, self.zone_id
|
||||
)
|
||||
self._tado.set_zone_off(self.zone_id, CONST_OVERLAY_MANUAL, TYPE_HOT_WATER)
|
||||
await self.coordinator.set_zone_off(
|
||||
self.zone_id, CONST_OVERLAY_MANUAL, TYPE_HOT_WATER
|
||||
)
|
||||
return
|
||||
|
||||
overlay_mode = decide_overlay_mode(
|
||||
tado=self._tado,
|
||||
coordinator=self.coordinator,
|
||||
duration=duration,
|
||||
zone_id=self.zone_id,
|
||||
)
|
||||
duration = decide_duration(
|
||||
tado=self._tado,
|
||||
coordinator=self.coordinator,
|
||||
duration=duration,
|
||||
zone_id=self.zone_id,
|
||||
overlay_mode=overlay_mode,
|
||||
@ -304,7 +305,7 @@ class TadoWaterHeater(TadoZoneEntity, WaterHeaterEntity):
|
||||
self.zone_id,
|
||||
self._target_temp,
|
||||
)
|
||||
self._tado.set_zone_overlay(
|
||||
await self.coordinator.set_zone_overlay(
|
||||
zone_id=self.zone_id,
|
||||
overlay_mode=overlay_mode,
|
||||
temperature=self._target_temp,
|
||||
|
115
tests/components/tado/snapshots/test_climate.ambr
Normal file
115
tests/components/tado/snapshots/test_climate.ambr
Normal file
@ -0,0 +1,115 @@
|
||||
# serializer version: 1
|
||||
# name: test_aircon_set_hvac_mode[cool-COOL]
|
||||
_Call(
|
||||
tuple(
|
||||
3,
|
||||
'NEXT_TIME_BLOCK',
|
||||
24.76,
|
||||
None,
|
||||
'AIR_CONDITIONING',
|
||||
'ON',
|
||||
'COOL',
|
||||
'AUTO',
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
),
|
||||
dict({
|
||||
}),
|
||||
)
|
||||
# ---
|
||||
# name: test_aircon_set_hvac_mode[dry-DRY]
|
||||
_Call(
|
||||
tuple(
|
||||
3,
|
||||
'NEXT_TIME_BLOCK',
|
||||
24.76,
|
||||
None,
|
||||
'AIR_CONDITIONING',
|
||||
'ON',
|
||||
'DRY',
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
),
|
||||
dict({
|
||||
}),
|
||||
)
|
||||
# ---
|
||||
# name: test_aircon_set_hvac_mode[fan_only-FAN]
|
||||
_Call(
|
||||
tuple(
|
||||
3,
|
||||
'NEXT_TIME_BLOCK',
|
||||
None,
|
||||
None,
|
||||
'AIR_CONDITIONING',
|
||||
'ON',
|
||||
'FAN',
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
),
|
||||
dict({
|
||||
}),
|
||||
)
|
||||
# ---
|
||||
# name: test_aircon_set_hvac_mode[heat-HEAT]
|
||||
_Call(
|
||||
tuple(
|
||||
3,
|
||||
'NEXT_TIME_BLOCK',
|
||||
24.76,
|
||||
None,
|
||||
'AIR_CONDITIONING',
|
||||
'ON',
|
||||
'HEAT',
|
||||
'AUTO',
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
),
|
||||
dict({
|
||||
}),
|
||||
)
|
||||
# ---
|
||||
# name: test_aircon_set_hvac_mode[off-OFF]
|
||||
_Call(
|
||||
tuple(
|
||||
3,
|
||||
'MANUAL',
|
||||
None,
|
||||
None,
|
||||
'AIR_CONDITIONING',
|
||||
'OFF',
|
||||
),
|
||||
dict({
|
||||
}),
|
||||
)
|
||||
# ---
|
||||
# name: test_heater_set_temperature
|
||||
_Call(
|
||||
tuple(
|
||||
1,
|
||||
'NEXT_TIME_BLOCK',
|
||||
22.0,
|
||||
None,
|
||||
'HEATING',
|
||||
'ON',
|
||||
'HEAT',
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
),
|
||||
dict({
|
||||
}),
|
||||
)
|
||||
# ---
|
@ -1,5 +1,19 @@
|
||||
"""The sensor tests for the tado platform."""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from PyTado.interface.api.my_tado import TadoZone
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
ATTR_HVAC_MODE,
|
||||
DOMAIN as CLIMATE_DOMAIN,
|
||||
SERVICE_SET_HVAC_MODE,
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
HVACMode,
|
||||
)
|
||||
from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .util import async_init_integration
|
||||
@ -121,3 +135,104 @@ async def test_smartac_with_fanlevel_vertical_and_horizontal_swing(
|
||||
# Only test for a subset of attributes in case
|
||||
# HA changes the implementation and a new one appears
|
||||
assert all(item in state.attributes.items() for item in expected_attributes.items())
|
||||
|
||||
|
||||
async def test_heater_set_temperature(
|
||||
hass: HomeAssistant, snapshot: SnapshotAssertion
|
||||
) -> None:
|
||||
"""Test the set temperature of the heater."""
|
||||
|
||||
await async_init_integration(hass)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.tado.PyTado.interface.api.Tado.set_zone_overlay"
|
||||
) as mock_set_state,
|
||||
patch(
|
||||
"homeassistant.components.tado.PyTado.interface.api.Tado.get_zone_state",
|
||||
return_value={"setting": {"temperature": {"celsius": 22.0}}},
|
||||
),
|
||||
):
|
||||
await hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
{ATTR_ENTITY_ID: "climate.baseboard_heater", ATTR_TEMPERATURE: 22.0},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
mock_set_state.assert_called_once()
|
||||
snapshot.assert_match(mock_set_state.call_args)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("hvac_mode", "set_hvac_mode"),
|
||||
[
|
||||
(HVACMode.HEAT, "HEAT"),
|
||||
(HVACMode.DRY, "DRY"),
|
||||
(HVACMode.FAN_ONLY, "FAN"),
|
||||
(HVACMode.COOL, "COOL"),
|
||||
(HVACMode.OFF, "OFF"),
|
||||
],
|
||||
)
|
||||
async def test_aircon_set_hvac_mode(
|
||||
hass: HomeAssistant,
|
||||
snapshot: SnapshotAssertion,
|
||||
hvac_mode: HVACMode,
|
||||
set_hvac_mode: str,
|
||||
) -> None:
|
||||
"""Test the set hvac mode of the air conditioning."""
|
||||
|
||||
await async_init_integration(hass)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.tado.__init__.PyTado.interface.api.Tado.set_zone_overlay"
|
||||
) as mock_set_state,
|
||||
patch(
|
||||
"homeassistant.components.tado.__init__.PyTado.interface.api.Tado.get_zone_state",
|
||||
return_value=TadoZone(
|
||||
zone_id=1,
|
||||
current_temp=18.7,
|
||||
connection=None,
|
||||
current_temp_timestamp="2025-01-02T12:51:52.802Z",
|
||||
current_humidity=45.1,
|
||||
current_humidity_timestamp="2025-01-02T12:51:52.802Z",
|
||||
is_away=False,
|
||||
current_hvac_action="IDLE",
|
||||
current_fan_speed=None,
|
||||
current_fan_level=None,
|
||||
current_hvac_mode=set_hvac_mode,
|
||||
current_swing_mode="OFF",
|
||||
current_vertical_swing_mode="OFF",
|
||||
current_horizontal_swing_mode="OFF",
|
||||
target_temp=16.0,
|
||||
available=True,
|
||||
power="ON",
|
||||
link="ONLINE",
|
||||
ac_power_timestamp=None,
|
||||
heating_power_timestamp="2025-01-02T13:01:11.758Z",
|
||||
ac_power=None,
|
||||
heating_power=None,
|
||||
heating_power_percentage=0.0,
|
||||
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,
|
||||
),
|
||||
),
|
||||
):
|
||||
await hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
SERVICE_SET_HVAC_MODE,
|
||||
{ATTR_ENTITY_ID: "climate.air_conditioning", ATTR_HVAC_MODE: hvac_mode},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
mock_set_state.assert_called_once()
|
||||
snapshot.assert_match(mock_set_state.call_args)
|
||||
|
@ -1,45 +1,94 @@
|
||||
"""Helper method tests."""
|
||||
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from homeassistant.components.tado import TadoConnector
|
||||
from PyTado.interface import Tado
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.tado import TadoDataUpdateCoordinator
|
||||
from homeassistant.components.tado.const import (
|
||||
CONST_OVERLAY_MANUAL,
|
||||
CONST_OVERLAY_TADO_DEFAULT,
|
||||
CONST_OVERLAY_TADO_MODE,
|
||||
CONST_OVERLAY_TIMER,
|
||||
DOMAIN,
|
||||
)
|
||||
from homeassistant.components.tado.helper import decide_duration, decide_overlay_mode
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
def dummy_tado_connector(hass: HomeAssistant, fallback) -> TadoConnector:
|
||||
|
||||
@pytest.fixture
|
||||
def entry(request: pytest.FixtureRequest) -> MockConfigEntry:
|
||||
"""Fixture for ConfigEntry with optional fallback."""
|
||||
fallback = (
|
||||
request.param if hasattr(request, "param") else CONST_OVERLAY_TADO_DEFAULT
|
||||
)
|
||||
return MockConfigEntry(
|
||||
version=1,
|
||||
minor_version=1,
|
||||
domain=DOMAIN,
|
||||
title="Tado",
|
||||
data={
|
||||
CONF_USERNAME: "test-username",
|
||||
CONF_PASSWORD: "test-password",
|
||||
},
|
||||
options={
|
||||
"fallback": fallback,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def tado() -> Tado:
|
||||
"""Fixture for Tado instance."""
|
||||
with patch(
|
||||
"homeassistant.components.tado.PyTado.interface.api.Tado.set_zone_overlay"
|
||||
) as mock_set_zone_overlay:
|
||||
instance = MagicMock(spec=Tado)
|
||||
instance.set_zone_overlay = mock_set_zone_overlay
|
||||
yield instance
|
||||
|
||||
|
||||
def dummy_tado_connector(
|
||||
hass: HomeAssistant, entry: ConfigEntry, tado: Tado
|
||||
) -> TadoDataUpdateCoordinator:
|
||||
"""Return dummy tado connector."""
|
||||
return TadoConnector(hass, username="dummy", password="dummy", fallback=fallback)
|
||||
return TadoDataUpdateCoordinator(hass, entry, tado)
|
||||
|
||||
|
||||
async def test_overlay_mode_duration_set(hass: HomeAssistant) -> None:
|
||||
@pytest.mark.parametrize("entry", [CONST_OVERLAY_TADO_MODE], indirect=True)
|
||||
async def test_overlay_mode_duration_set(
|
||||
hass: HomeAssistant, entry: ConfigEntry, tado: Tado
|
||||
) -> None:
|
||||
"""Test overlay method selection when duration is set."""
|
||||
tado = dummy_tado_connector(hass=hass, fallback=CONST_OVERLAY_TADO_MODE)
|
||||
overlay_mode = decide_overlay_mode(tado=tado, duration=3600, zone_id=1)
|
||||
tado = dummy_tado_connector(hass=hass, entry=entry, tado=tado)
|
||||
overlay_mode = decide_overlay_mode(coordinator=tado, duration=3600, zone_id=1)
|
||||
# Must select TIMER overlay
|
||||
assert overlay_mode == CONST_OVERLAY_TIMER
|
||||
|
||||
|
||||
async def test_overlay_mode_next_time_block_fallback(hass: HomeAssistant) -> None:
|
||||
@pytest.mark.parametrize("entry", [CONST_OVERLAY_TADO_MODE], indirect=True)
|
||||
async def test_overlay_mode_next_time_block_fallback(
|
||||
hass: HomeAssistant, entry: ConfigEntry, tado: Tado
|
||||
) -> None:
|
||||
"""Test overlay method selection when duration is not set."""
|
||||
integration_fallback = CONST_OVERLAY_TADO_MODE
|
||||
tado = dummy_tado_connector(hass=hass, fallback=integration_fallback)
|
||||
overlay_mode = decide_overlay_mode(tado=tado, duration=None, zone_id=1)
|
||||
tado = dummy_tado_connector(hass=hass, entry=entry, tado=tado)
|
||||
overlay_mode = decide_overlay_mode(coordinator=tado, duration=None, zone_id=1)
|
||||
# Must fallback to integration wide setting
|
||||
assert overlay_mode == integration_fallback
|
||||
assert overlay_mode == CONST_OVERLAY_TADO_MODE
|
||||
|
||||
|
||||
async def test_overlay_mode_tado_default_fallback(hass: HomeAssistant) -> None:
|
||||
@pytest.mark.parametrize("entry", [CONST_OVERLAY_TADO_DEFAULT], indirect=True)
|
||||
async def test_overlay_mode_tado_default_fallback(
|
||||
hass: HomeAssistant, entry: ConfigEntry, tado: Tado
|
||||
) -> None:
|
||||
"""Test overlay method selection when tado default is selected."""
|
||||
integration_fallback = CONST_OVERLAY_TADO_DEFAULT
|
||||
zone_fallback = CONST_OVERLAY_MANUAL
|
||||
tado = dummy_tado_connector(hass=hass, fallback=integration_fallback)
|
||||
tado = dummy_tado_connector(hass=hass, entry=entry, tado=tado)
|
||||
|
||||
class MockZoneData:
|
||||
def __init__(self) -> None:
|
||||
@ -49,28 +98,40 @@ async def test_overlay_mode_tado_default_fallback(hass: HomeAssistant) -> None:
|
||||
|
||||
zone_data = {"zone": {zone_id: MockZoneData()}}
|
||||
with patch.dict(tado.data, zone_data):
|
||||
overlay_mode = decide_overlay_mode(tado=tado, duration=None, zone_id=zone_id)
|
||||
overlay_mode = decide_overlay_mode(
|
||||
coordinator=tado, duration=None, zone_id=zone_id
|
||||
)
|
||||
# Must fallback to zone setting
|
||||
assert overlay_mode == zone_fallback
|
||||
|
||||
|
||||
async def test_duration_enabled_without_tado_default(hass: HomeAssistant) -> None:
|
||||
@pytest.mark.parametrize("entry", [CONST_OVERLAY_MANUAL], indirect=True)
|
||||
async def test_duration_enabled_without_tado_default(
|
||||
hass: HomeAssistant, entry: ConfigEntry, tado: Tado
|
||||
) -> None:
|
||||
"""Test duration decide method when overlay is timer and duration is set."""
|
||||
overlay = CONST_OVERLAY_TIMER
|
||||
expected_duration = 600
|
||||
tado = dummy_tado_connector(hass=hass, fallback=CONST_OVERLAY_MANUAL)
|
||||
tado = dummy_tado_connector(hass=hass, entry=entry, tado=tado)
|
||||
duration = decide_duration(
|
||||
tado=tado, duration=expected_duration, overlay_mode=overlay, zone_id=0
|
||||
coordinator=tado, duration=expected_duration, overlay_mode=overlay, zone_id=0
|
||||
)
|
||||
# Should return the same duration value
|
||||
assert duration == expected_duration
|
||||
|
||||
|
||||
async def test_duration_enabled_with_tado_default(hass: HomeAssistant) -> None:
|
||||
@pytest.mark.parametrize("entry", [CONST_OVERLAY_TIMER], indirect=True)
|
||||
async def test_duration_enabled_with_tado_default(
|
||||
hass: HomeAssistant, entry: ConfigEntry, tado: Tado
|
||||
) -> None:
|
||||
"""Test overlay method selection when ended up with timer overlay and None duration."""
|
||||
zone_fallback = CONST_OVERLAY_TIMER
|
||||
expected_duration = 45000
|
||||
tado = dummy_tado_connector(hass=hass, fallback=zone_fallback)
|
||||
tado = dummy_tado_connector(
|
||||
hass=hass,
|
||||
entry=entry,
|
||||
tado=tado,
|
||||
)
|
||||
|
||||
class MockZoneData:
|
||||
def __init__(self) -> None:
|
||||
@ -81,7 +142,7 @@ async def test_duration_enabled_with_tado_default(hass: HomeAssistant) -> None:
|
||||
zone_data = {"zone": {zone_id: MockZoneData()}}
|
||||
with patch.dict(tado.data, zone_data):
|
||||
duration = decide_duration(
|
||||
tado=tado, duration=None, zone_id=zone_id, overlay_mode=zone_fallback
|
||||
coordinator=tado, duration=None, zone_id=zone_id, overlay_mode=zone_fallback
|
||||
)
|
||||
# Must fallback to zone timer setting
|
||||
assert duration == expected_duration
|
||||
|
@ -80,7 +80,7 @@ async def test_add_meter_readings_exception(
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert "Could not set meter reading" in str(exc)
|
||||
assert "Error setting Tado meter reading: Error" in str(exc.value)
|
||||
|
||||
|
||||
async def test_add_meter_readings_invalid(
|
||||
|
@ -188,3 +188,8 @@ async def async_init_integration(
|
||||
if not skip_setup:
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# For a first refresh
|
||||
await entry.runtime_data.coordinator.async_refresh()
|
||||
await entry.runtime_data.mobile_coordinator.async_refresh()
|
||||
await hass.async_block_till_done()
|
||||
|
Loading…
x
Reference in New Issue
Block a user