mirror of
https://github.com/home-assistant/core.git
synced 2025-11-09 10:59:40 +00:00
279 lines
9.0 KiB
Python
279 lines
9.0 KiB
Python
"""Support for (EMEA/EU-based) Honeywell TCC systems.
|
|
|
|
Such systems provide heating/cooling and DHW and include Evohome, Round Thermostat, and
|
|
others.
|
|
|
|
Note that the API used by this integration's client does not support cooling.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from datetime import timedelta
|
|
import logging
|
|
from typing import Final
|
|
|
|
import evohomeasync as ec1
|
|
import evohomeasync2 as ec2
|
|
from evohomeasync2.const import SZ_CAN_BE_TEMPORARY, SZ_SYSTEM_MODE, SZ_TIMING_MODE
|
|
from evohomeasync2.schemas.const import (
|
|
S2_DURATION as SZ_DURATION,
|
|
S2_PERIOD as SZ_PERIOD,
|
|
SystemMode as EvoSystemMode,
|
|
)
|
|
import voluptuous as vol
|
|
|
|
from homeassistant.const import (
|
|
ATTR_ENTITY_ID,
|
|
ATTR_MODE,
|
|
CONF_PASSWORD,
|
|
CONF_SCAN_INTERVAL,
|
|
CONF_USERNAME,
|
|
Platform,
|
|
)
|
|
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
|
from homeassistant.helpers import config_validation as cv, entity_registry as er
|
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
|
from homeassistant.helpers.discovery import async_load_platform
|
|
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
|
from homeassistant.helpers.service import verify_domain_control
|
|
from homeassistant.helpers.typing import ConfigType
|
|
from homeassistant.util.hass_dict import HassKey
|
|
|
|
from .const import (
|
|
ATTR_DURATION,
|
|
ATTR_DURATION_UNTIL,
|
|
ATTR_PERIOD,
|
|
ATTR_SETPOINT,
|
|
CONF_LOCATION_IDX,
|
|
DOMAIN,
|
|
SCAN_INTERVAL_DEFAULT,
|
|
SCAN_INTERVAL_MINIMUM,
|
|
EvoService,
|
|
)
|
|
from .coordinator import EvoDataUpdateCoordinator
|
|
from .storage import TokenManager
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
CONFIG_SCHEMA: Final = vol.Schema(
|
|
{
|
|
DOMAIN: vol.Schema(
|
|
{
|
|
vol.Required(CONF_USERNAME): cv.string,
|
|
vol.Required(CONF_PASSWORD): cv.string,
|
|
vol.Optional(CONF_LOCATION_IDX, default=0): cv.positive_int,
|
|
vol.Optional(
|
|
CONF_SCAN_INTERVAL, default=SCAN_INTERVAL_DEFAULT
|
|
): vol.All(cv.time_period, vol.Range(min=SCAN_INTERVAL_MINIMUM)),
|
|
}
|
|
)
|
|
},
|
|
extra=vol.ALLOW_EXTRA,
|
|
)
|
|
|
|
# system mode schemas are built dynamically when the services are registered
|
|
# because supported modes can vary for edge-case systems
|
|
|
|
RESET_ZONE_OVERRIDE_SCHEMA: Final = vol.Schema(
|
|
{vol.Required(ATTR_ENTITY_ID): cv.entity_id}
|
|
)
|
|
SET_ZONE_OVERRIDE_SCHEMA: Final = vol.Schema(
|
|
{
|
|
vol.Required(ATTR_ENTITY_ID): cv.entity_id,
|
|
vol.Required(ATTR_SETPOINT): vol.All(
|
|
vol.Coerce(float), vol.Range(min=4.0, max=35.0)
|
|
),
|
|
vol.Optional(ATTR_DURATION_UNTIL): vol.All(
|
|
cv.time_period, vol.Range(min=timedelta(days=0), max=timedelta(days=1))
|
|
),
|
|
}
|
|
)
|
|
|
|
EVOHOME_KEY: HassKey[EvoData] = HassKey(DOMAIN)
|
|
|
|
|
|
@dataclass
|
|
class EvoData:
|
|
"""Dataclass for storing evohome data."""
|
|
|
|
coordinator: EvoDataUpdateCoordinator
|
|
loc_idx: int
|
|
tcs: ec2.ControlSystem
|
|
|
|
|
|
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|
"""Set up the Evohome integration."""
|
|
|
|
token_manager = TokenManager(
|
|
hass,
|
|
config[DOMAIN][CONF_USERNAME],
|
|
config[DOMAIN][CONF_PASSWORD],
|
|
async_get_clientsession(hass),
|
|
)
|
|
coordinator = EvoDataUpdateCoordinator(
|
|
hass,
|
|
_LOGGER,
|
|
ec2.EvohomeClient(token_manager),
|
|
name=f"{DOMAIN}_coordinator",
|
|
update_interval=config[DOMAIN][CONF_SCAN_INTERVAL],
|
|
location_idx=config[DOMAIN][CONF_LOCATION_IDX],
|
|
client_v1=ec1.EvohomeClient(token_manager),
|
|
)
|
|
|
|
await coordinator.async_register_shutdown()
|
|
await coordinator.async_first_refresh()
|
|
|
|
if not coordinator.last_update_success:
|
|
_LOGGER.error(f"Failed to fetch initial data: {coordinator.last_exception}") # noqa: G004
|
|
return False
|
|
|
|
assert coordinator.tcs is not None # mypy
|
|
|
|
hass.data[EVOHOME_KEY] = EvoData(
|
|
coordinator=coordinator,
|
|
loc_idx=coordinator.loc_idx,
|
|
tcs=coordinator.tcs,
|
|
)
|
|
|
|
hass.async_create_task(
|
|
async_load_platform(hass, Platform.CLIMATE, DOMAIN, {}, config)
|
|
)
|
|
if coordinator.tcs.hotwater:
|
|
hass.async_create_task(
|
|
async_load_platform(hass, Platform.WATER_HEATER, DOMAIN, {}, config)
|
|
)
|
|
|
|
setup_service_functions(hass, coordinator)
|
|
|
|
return True
|
|
|
|
|
|
@callback
|
|
def setup_service_functions(
|
|
hass: HomeAssistant, coordinator: EvoDataUpdateCoordinator
|
|
) -> None:
|
|
"""Set up the service handlers for the system/zone operating modes.
|
|
|
|
Not all Honeywell TCC-compatible systems support all operating modes. In addition,
|
|
each mode will require any of four distinct service schemas. This has to be
|
|
enumerated before registering the appropriate handlers.
|
|
|
|
It appears that all TCC-compatible systems support the same three zones modes.
|
|
"""
|
|
|
|
@verify_domain_control(hass, DOMAIN)
|
|
async def force_refresh(call: ServiceCall) -> None:
|
|
"""Obtain the latest state data via the vendor's RESTful API."""
|
|
await coordinator.async_refresh()
|
|
|
|
@verify_domain_control(hass, DOMAIN)
|
|
async def set_system_mode(call: ServiceCall) -> None:
|
|
"""Set the system mode."""
|
|
assert coordinator.tcs is not None # mypy
|
|
|
|
payload = {
|
|
"unique_id": coordinator.tcs.id,
|
|
"service": call.service,
|
|
"data": call.data,
|
|
}
|
|
async_dispatcher_send(hass, DOMAIN, payload)
|
|
|
|
@verify_domain_control(hass, DOMAIN)
|
|
async def set_zone_override(call: ServiceCall) -> None:
|
|
"""Set the zone override (setpoint)."""
|
|
entity_id = call.data[ATTR_ENTITY_ID]
|
|
|
|
registry = er.async_get(hass)
|
|
registry_entry = registry.async_get(entity_id)
|
|
|
|
if registry_entry is None or registry_entry.platform != DOMAIN:
|
|
raise ValueError(f"'{entity_id}' is not a known {DOMAIN} entity")
|
|
|
|
if registry_entry.domain != "climate":
|
|
raise ValueError(f"'{entity_id}' is not an {DOMAIN} controller/zone")
|
|
|
|
payload = {
|
|
"unique_id": registry_entry.unique_id,
|
|
"service": call.service,
|
|
"data": call.data,
|
|
}
|
|
|
|
async_dispatcher_send(hass, DOMAIN, payload)
|
|
|
|
assert coordinator.tcs is not None # mypy
|
|
|
|
hass.services.async_register(DOMAIN, EvoService.REFRESH_SYSTEM, force_refresh)
|
|
|
|
# Enumerate which operating modes are supported by this system
|
|
modes = list(coordinator.tcs.allowed_system_modes)
|
|
|
|
# Not all systems support "AutoWithReset": register this handler only if required
|
|
if any(
|
|
m[SZ_SYSTEM_MODE]
|
|
for m in modes
|
|
if m[SZ_SYSTEM_MODE] == EvoSystemMode.AUTO_WITH_RESET
|
|
):
|
|
hass.services.async_register(DOMAIN, EvoService.RESET_SYSTEM, set_system_mode)
|
|
|
|
system_mode_schemas = []
|
|
modes = [m for m in modes if m[SZ_SYSTEM_MODE] != EvoSystemMode.AUTO_WITH_RESET]
|
|
|
|
# Permanent-only modes will use this schema
|
|
perm_modes = [m[SZ_SYSTEM_MODE] for m in modes if not m[SZ_CAN_BE_TEMPORARY]]
|
|
if perm_modes: # any of: "Auto", "HeatingOff": permanent only
|
|
schema = vol.Schema({vol.Required(ATTR_MODE): vol.In(perm_modes)})
|
|
system_mode_schemas.append(schema)
|
|
|
|
modes = [m for m in modes if m[SZ_CAN_BE_TEMPORARY]]
|
|
|
|
# These modes are set for a number of hours (or indefinitely): use this schema
|
|
temp_modes = [m[SZ_SYSTEM_MODE] for m in modes if m[SZ_TIMING_MODE] == SZ_DURATION]
|
|
if temp_modes: # any of: "AutoWithEco", permanent or for 0-24 hours
|
|
schema = vol.Schema(
|
|
{
|
|
vol.Required(ATTR_MODE): vol.In(temp_modes),
|
|
vol.Optional(ATTR_DURATION): vol.All(
|
|
cv.time_period,
|
|
vol.Range(min=timedelta(hours=0), max=timedelta(hours=24)),
|
|
),
|
|
}
|
|
)
|
|
system_mode_schemas.append(schema)
|
|
|
|
# These modes are set for a number of days (or indefinitely): use this schema
|
|
temp_modes = [m[SZ_SYSTEM_MODE] for m in modes if m[SZ_TIMING_MODE] == SZ_PERIOD]
|
|
if temp_modes: # any of: "Away", "Custom", "DayOff", permanent or for 1-99 days
|
|
schema = vol.Schema(
|
|
{
|
|
vol.Required(ATTR_MODE): vol.In(temp_modes),
|
|
vol.Optional(ATTR_PERIOD): vol.All(
|
|
cv.time_period,
|
|
vol.Range(min=timedelta(days=1), max=timedelta(days=99)),
|
|
),
|
|
}
|
|
)
|
|
system_mode_schemas.append(schema)
|
|
|
|
if system_mode_schemas:
|
|
hass.services.async_register(
|
|
DOMAIN,
|
|
EvoService.SET_SYSTEM_MODE,
|
|
set_system_mode,
|
|
schema=vol.Schema(vol.Any(*system_mode_schemas)),
|
|
)
|
|
|
|
# The zone modes are consistent across all systems and use the same schema
|
|
hass.services.async_register(
|
|
DOMAIN,
|
|
EvoService.RESET_ZONE_OVERRIDE,
|
|
set_zone_override,
|
|
schema=RESET_ZONE_OVERRIDE_SCHEMA,
|
|
)
|
|
hass.services.async_register(
|
|
DOMAIN,
|
|
EvoService.SET_ZONE_OVERRIDE,
|
|
set_zone_override,
|
|
schema=SET_ZONE_OVERRIDE_SCHEMA,
|
|
)
|