Refactor Tado to use runtime_data (#121373)
This commit is contained in:
parent
1fefd396b9
commit
fb8eeac563
10 changed files with 393 additions and 356 deletions
|
@ -1,22 +1,19 @@
|
|||
"""Support for the (unofficial) Tado API."""
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from PyTado.interface import Tado
|
||||
from requests import RequestException
|
||||
import requests.exceptions
|
||||
|
||||
from homeassistant.components.climate import PRESET_AWAY, PRESET_HOME
|
||||
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, HomeAssistantError
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import dispatcher_send
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
from .const import (
|
||||
CONF_FALLBACK,
|
||||
|
@ -24,18 +21,13 @@ from .const import (
|
|||
CONST_OVERLAY_TADO_DEFAULT,
|
||||
CONST_OVERLAY_TADO_MODE,
|
||||
CONST_OVERLAY_TADO_OPTIONS,
|
||||
DATA,
|
||||
DOMAIN,
|
||||
INSIDE_TEMPERATURE_MEASUREMENT,
|
||||
PRESET_AUTO,
|
||||
SIGNAL_TADO_MOBILE_DEVICE_UPDATE_RECEIVED,
|
||||
SIGNAL_TADO_UPDATE_RECEIVED,
|
||||
TEMP_OFFSET,
|
||||
UPDATE_LISTENER,
|
||||
UPDATE_MOBILE_DEVICE_TRACK,
|
||||
UPDATE_TRACK,
|
||||
)
|
||||
from .services import setup_services
|
||||
from .tado_connector import TadoConnector
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -63,7 +55,20 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
type TadoConfigEntry = ConfigEntry[TadoRuntimeData]
|
||||
|
||||
|
||||
@dataclass
|
||||
class TadoRuntimeData:
|
||||
"""Dataclass for Tado runtime data."""
|
||||
|
||||
tadoconnector: TadoConnector
|
||||
update_track: Any
|
||||
update_mobile_device_track: Any
|
||||
update_listener: Any
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: TadoConfigEntry) -> bool:
|
||||
"""Set up Tado from a config entry."""
|
||||
|
||||
_async_import_options_from_data_if_missing(hass, entry)
|
||||
|
@ -108,13 +113,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||
|
||||
update_listener = entry.add_update_listener(_async_update_listener)
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
hass.data[DOMAIN][entry.entry_id] = {
|
||||
DATA: tadoconnector,
|
||||
UPDATE_TRACK: update_track,
|
||||
UPDATE_MOBILE_DEVICE_TRACK: update_mobile_devices,
|
||||
UPDATE_LISTENER: update_listener,
|
||||
}
|
||||
entry.runtime_data = TadoRuntimeData(
|
||||
tadoconnector=tadoconnector,
|
||||
update_track=update_track,
|
||||
update_mobile_device_track=update_mobile_devices,
|
||||
update_listener=update_listener,
|
||||
)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
|
@ -155,301 +159,3 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
|
||||
|
||||
class TadoConnector:
|
||||
"""An object to store the Tado data."""
|
||||
|
||||
def __init__(self, hass, username, password, fallback):
|
||||
"""Initialize Tado Connector."""
|
||||
self.hass = hass
|
||||
self._username = username
|
||||
self._password = password
|
||||
self._fallback = fallback
|
||||
|
||||
self.home_id = None
|
||||
self.home_name = None
|
||||
self.tado = None
|
||||
self.zones = None
|
||||
self.devices = None
|
||||
self.data = {
|
||||
"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, str]:
|
||||
"""Send meter reading to Tado."""
|
||||
dt: str = datetime.now().strftime("%Y-%m-%d")
|
||||
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,16 +12,13 @@ from homeassistant.components.binary_sensor import (
|
|||
BinarySensorEntity,
|
||||
BinarySensorEntityDescription,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
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 TadoConnector
|
||||
from . import TadoConfigEntry
|
||||
from .const import (
|
||||
DATA,
|
||||
DOMAIN,
|
||||
SIGNAL_TADO_UPDATE_RECEIVED,
|
||||
TYPE_AIR_CONDITIONING,
|
||||
TYPE_BATTERY,
|
||||
|
@ -30,6 +27,7 @@ from .const import (
|
|||
TYPE_POWER,
|
||||
)
|
||||
from .entity import TadoDeviceEntity, TadoZoneEntity
|
||||
from .tado_connector import TadoConnector
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -68,9 +66,9 @@ OVERLAY_ENTITY_DESCRIPTION = TadoBinarySensorEntityDescription(
|
|||
key="overlay",
|
||||
translation_key="overlay",
|
||||
state_fn=lambda data: data.overlay_active,
|
||||
attributes_fn=lambda data: {"termination": data.overlay_termination_type}
|
||||
if data.overlay_active
|
||||
else {},
|
||||
attributes_fn=lambda data: (
|
||||
{"termination": data.overlay_termination_type} if data.overlay_active else {}
|
||||
),
|
||||
device_class=BinarySensorDeviceClass.POWER,
|
||||
)
|
||||
OPEN_WINDOW_ENTITY_DESCRIPTION = TadoBinarySensorEntityDescription(
|
||||
|
@ -119,11 +117,11 @@ ZONE_SENSORS = {
|
|||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
hass: HomeAssistant, entry: TadoConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
) -> None:
|
||||
"""Set up the Tado sensor platform."""
|
||||
|
||||
tado = hass.data[DOMAIN][entry.entry_id][DATA]
|
||||
tado: TadoConnector = entry.runtime_data.tadoconnector
|
||||
devices = tado.devices
|
||||
zones = tado.zones
|
||||
entities: list[BinarySensorEntity] = []
|
||||
|
|
|
@ -22,7 +22,6 @@ from homeassistant.components.climate import (
|
|||
HVACAction,
|
||||
HVACMode,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
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
|
||||
|
@ -30,7 +29,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
|||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import VolDictType
|
||||
|
||||
from . import TadoConnector
|
||||
from . import TadoConfigEntry, TadoConnector
|
||||
from .const import (
|
||||
CONST_EXCLUSIVE_OVERLAY_GROUP,
|
||||
CONST_FAN_AUTO,
|
||||
|
@ -42,7 +41,6 @@ from .const import (
|
|||
CONST_MODE_SMART_SCHEDULE,
|
||||
CONST_OVERLAY_MANUAL,
|
||||
CONST_OVERLAY_TADO_OPTIONS,
|
||||
DATA,
|
||||
DOMAIN,
|
||||
HA_TERMINATION_DURATION,
|
||||
HA_TERMINATION_TYPE,
|
||||
|
@ -100,11 +98,11 @@ CLIMATE_TEMP_OFFSET_SCHEMA: VolDictType = {
|
|||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
hass: HomeAssistant, entry: TadoConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
) -> None:
|
||||
"""Set up the Tado climate platform."""
|
||||
|
||||
tado = hass.data[DOMAIN][entry.entry_id][DATA]
|
||||
tado: TadoConnector = entry.runtime_data.tadoconnector
|
||||
entities = await hass.async_add_executor_job(_generate_entities, tado)
|
||||
|
||||
platform = entity_platform.async_get_current_platform()
|
||||
|
|
|
@ -9,27 +9,27 @@ from homeassistant.components.device_tracker import (
|
|||
SourceType,
|
||||
TrackerEntity,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
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 . import TadoConnector
|
||||
from .const import DATA, DOMAIN, SIGNAL_TADO_MOBILE_DEVICE_UPDATE_RECEIVED
|
||||
from . import TadoConfigEntry
|
||||
from .const import DOMAIN, SIGNAL_TADO_MOBILE_DEVICE_UPDATE_RECEIVED
|
||||
from .tado_connector import TadoConnector
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: TadoConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Tado device scannery entity."""
|
||||
_LOGGER.debug("Setting up Tado device scanner entity")
|
||||
tado: TadoConnector = hass.data[DOMAIN][entry.entry_id][DATA]
|
||||
tado: TadoConnector = entry.runtime_data.tadoconnector
|
||||
tracked: set = set()
|
||||
|
||||
# Fix non-string unique_id for device trackers
|
||||
|
|
|
@ -43,7 +43,7 @@ class TadoHomeEntity(Entity):
|
|||
self.home_id = tado.home_id
|
||||
self._attr_device_info = DeviceInfo(
|
||||
configuration_url="https://app.tado.com",
|
||||
identifiers={(DOMAIN, tado.home_id)},
|
||||
identifiers={(DOMAIN, str(tado.home_id))},
|
||||
manufacturer=DEFAULT_NAME,
|
||||
model=TADO_HOME,
|
||||
name=tado.home_name,
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
"""Helper methods for Tado."""
|
||||
|
||||
from . import TadoConnector
|
||||
from .const import (
|
||||
CONST_OVERLAY_TADO_DEFAULT,
|
||||
CONST_OVERLAY_TADO_MODE,
|
||||
CONST_OVERLAY_TIMER,
|
||||
)
|
||||
from .tado_connector import TadoConnector
|
||||
|
||||
|
||||
def decide_overlay_mode(
|
||||
|
|
|
@ -13,18 +13,15 @@ from homeassistant.components.sensor import (
|
|||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
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
|
||||
|
||||
from . import TadoConnector
|
||||
from . import TadoConfigEntry
|
||||
from .const import (
|
||||
CONDITIONS_MAP,
|
||||
DATA,
|
||||
DOMAIN,
|
||||
SENSOR_DATA_CATEGORY_GEOFENCE,
|
||||
SENSOR_DATA_CATEGORY_WEATHER,
|
||||
SIGNAL_TADO_UPDATE_RECEIVED,
|
||||
|
@ -33,6 +30,7 @@ from .const import (
|
|||
TYPE_HOT_WATER,
|
||||
)
|
||||
from .entity import TadoHomeEntity, TadoZoneEntity
|
||||
from .tado_connector import TadoConnector
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -197,11 +195,11 @@ ZONE_SENSORS = {
|
|||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
hass: HomeAssistant, entry: TadoConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
) -> None:
|
||||
"""Set up the Tado sensor platform."""
|
||||
|
||||
tado = hass.data[DOMAIN][entry.entry_id][DATA]
|
||||
tado: TadoConnector = entry.runtime_data.tadoconnector
|
||||
zones = tado.zones
|
||||
entities: list[SensorEntity] = []
|
||||
|
||||
|
|
|
@ -5,17 +5,17 @@ import logging
|
|||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.helpers import selector
|
||||
|
||||
from .const import (
|
||||
ATTR_MESSAGE,
|
||||
CONF_CONFIG_ENTRY,
|
||||
CONF_READING,
|
||||
DATA,
|
||||
DOMAIN,
|
||||
SERVICE_ADD_METER_READING,
|
||||
)
|
||||
from .tado_connector import TadoConnector
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
SCHEMA_ADD_METER_READING = vol.Schema(
|
||||
|
@ -40,7 +40,12 @@ def setup_services(hass: HomeAssistant) -> None:
|
|||
reading: int = call.data[CONF_READING]
|
||||
_LOGGER.debug("Add meter reading %s", reading)
|
||||
|
||||
tadoconnector = hass.data[DOMAIN][entry_id][DATA]
|
||||
entry = hass.config_entries.async_get_entry(entry_id)
|
||||
if entry is None:
|
||||
raise ServiceValidationError("Config entry not found")
|
||||
|
||||
tadoconnector: TadoConnector = entry.runtime_data.tadoconnector
|
||||
|
||||
response: dict = await hass.async_add_executor_job(
|
||||
tadoconnector.set_meter_reading, call.data[CONF_READING]
|
||||
)
|
||||
|
|
332
homeassistant/components/tado/tado_connector.py
Normal file
332
homeassistant/components/tado/tado_connector.py
Normal file
|
@ -0,0 +1,332 @@
|
|||
"""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
|
|
@ -9,7 +9,6 @@ from homeassistant.components.water_heater import (
|
|||
WaterHeaterEntity,
|
||||
WaterHeaterEntityFeature,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import config_validation as cv, entity_platform
|
||||
|
@ -17,7 +16,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
|||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import VolDictType
|
||||
|
||||
from . import TadoConnector
|
||||
from . import TadoConfigEntry
|
||||
from .const import (
|
||||
CONST_HVAC_HEAT,
|
||||
CONST_MODE_AUTO,
|
||||
|
@ -27,14 +26,13 @@ from .const import (
|
|||
CONST_OVERLAY_MANUAL,
|
||||
CONST_OVERLAY_TADO_MODE,
|
||||
CONST_OVERLAY_TIMER,
|
||||
DATA,
|
||||
DOMAIN,
|
||||
SIGNAL_TADO_UPDATE_RECEIVED,
|
||||
TYPE_HOT_WATER,
|
||||
)
|
||||
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__)
|
||||
|
||||
|
@ -65,11 +63,11 @@ WATER_HEATER_TIMER_SCHEMA: VolDictType = {
|
|||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
hass: HomeAssistant, entry: TadoConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
) -> None:
|
||||
"""Set up the Tado water heater platform."""
|
||||
|
||||
tado = hass.data[DOMAIN][entry.entry_id][DATA]
|
||||
tado: TadoConnector = entry.runtime_data.tadoconnector
|
||||
entities = await hass.async_add_executor_job(_generate_entities, tado)
|
||||
|
||||
platform = entity_platform.async_get_current_platform()
|
||||
|
@ -95,7 +93,9 @@ def _generate_entities(tado: TadoConnector) -> list:
|
|||
|
||||
for zone in tado.zones:
|
||||
if zone["type"] == TYPE_HOT_WATER:
|
||||
entity = create_water_heater_entity(tado, zone["name"], zone["id"], zone)
|
||||
entity = create_water_heater_entity(
|
||||
tado, zone["name"], zone["id"], str(zone["name"])
|
||||
)
|
||||
entities.append(entity)
|
||||
|
||||
return entities
|
||||
|
|
Loading…
Add table
Reference in a new issue