Add services to evohome (#29816)
parent
bfa8cb760f
commit
a037c1d788
|
@ -1,8 +1,8 @@
|
|||
"""Support for (EMEA/EU-based) Honeywell TCC climate systems.
|
||||
|
||||
Such systems include evohome (multi-zone), and Round Thermostat (single zone).
|
||||
Such systems include evohome, Round Thermostat, and others.
|
||||
"""
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import datetime as dt, timedelta
|
||||
import logging
|
||||
import re
|
||||
from typing import Any, Dict, Optional, Tuple
|
||||
|
@ -13,6 +13,7 @@ import evohomeasync2
|
|||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
CONF_PASSWORD,
|
||||
CONF_SCAN_INTERVAL,
|
||||
CONF_USERNAME,
|
||||
|
@ -24,7 +25,12 @@ from homeassistant.core import callback
|
|||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.discovery import async_load_platform
|
||||
from homeassistant.helpers.dispatcher import (
|
||||
async_dispatcher_connect,
|
||||
async_dispatcher_send,
|
||||
)
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.service import verify_domain_control
|
||||
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
|
@ -58,16 +64,44 @@ CONFIG_SCHEMA = vol.Schema(
|
|||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
ATTR_SYSTEM_MODE = "mode"
|
||||
ATTR_DURATION_DAYS = "period"
|
||||
ATTR_DURATION_HOURS = "duration"
|
||||
|
||||
def _local_dt_to_aware(dt_naive: datetime) -> datetime:
|
||||
dt_aware = dt_util.now() + (dt_naive - datetime.now())
|
||||
ATTR_ZONE_TEMP = "setpoint"
|
||||
ATTR_DURATION_UNTIL = "duration"
|
||||
|
||||
SVC_REFRESH_SYSTEM = "refresh_system"
|
||||
SVC_SET_SYSTEM_MODE = "set_system_mode"
|
||||
SVC_RESET_SYSTEM = "reset_system"
|
||||
SVC_SET_ZONE_OVERRIDE = "set_zone_override"
|
||||
SVC_RESET_ZONE_OVERRIDE = "clear_zone_override"
|
||||
|
||||
|
||||
RESET_ZONE_OVERRIDE_SCHEMA = vol.Schema({vol.Required(ATTR_ENTITY_ID): cv.entity_id})
|
||||
SET_ZONE_OVERRIDE_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_ENTITY_ID): cv.entity_id,
|
||||
vol.Required(ATTR_ZONE_TEMP): 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)),
|
||||
),
|
||||
}
|
||||
)
|
||||
# system mode schemas are built dynamically, below
|
||||
|
||||
|
||||
def _local_dt_to_aware(dt_naive: dt) -> dt:
|
||||
dt_aware = dt_util.now() + (dt_naive - dt.now())
|
||||
if dt_aware.microsecond >= 500000:
|
||||
dt_aware += timedelta(seconds=1)
|
||||
return dt_aware.replace(microsecond=0)
|
||||
|
||||
|
||||
def _dt_to_local_naive(dt_aware: datetime) -> datetime:
|
||||
dt_naive = datetime.now() + (dt_aware - dt_util.now())
|
||||
def _dt_to_local_naive(dt_aware: dt) -> dt:
|
||||
dt_naive = dt.now() + (dt_aware - dt_util.now())
|
||||
if dt_naive.microsecond >= 500000:
|
||||
dt_naive += timedelta(seconds=1)
|
||||
return dt_naive.replace(microsecond=0)
|
||||
|
@ -114,7 +148,7 @@ def _handle_exception(err) -> bool:
|
|||
return False
|
||||
|
||||
except aiohttp.ClientConnectionError:
|
||||
# this appears to be common with Honeywell's servers
|
||||
# this appears to be a common occurance with the vendor's servers
|
||||
_LOGGER.warning(
|
||||
"Unable to connect with the vendor's server. "
|
||||
"Check your network and the vendor's service status page. "
|
||||
|
@ -143,7 +177,7 @@ def _handle_exception(err) -> bool:
|
|||
|
||||
|
||||
async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
|
||||
"""Create a (EMEA/EU-based) Honeywell evohome system."""
|
||||
"""Create a (EMEA/EU-based) Honeywell TCC system."""
|
||||
|
||||
async def load_auth_tokens(store) -> Tuple[Dict, Optional[Dict]]:
|
||||
app_storage = await store.async_load()
|
||||
|
@ -209,7 +243,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
|
|||
)
|
||||
|
||||
await broker.save_auth_tokens()
|
||||
await broker.update() # get initial state
|
||||
await broker.async_update() # get initial state
|
||||
|
||||
hass.async_create_task(async_load_platform(hass, "climate", DOMAIN, {}, config))
|
||||
if broker.tcs.hotwater:
|
||||
|
@ -218,12 +252,133 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
|
|||
)
|
||||
|
||||
hass.helpers.event.async_track_time_interval(
|
||||
broker.update, config[DOMAIN][CONF_SCAN_INTERVAL]
|
||||
broker.async_update, config[DOMAIN][CONF_SCAN_INTERVAL]
|
||||
)
|
||||
|
||||
setup_service_functions(hass, broker)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@callback
|
||||
def setup_service_functions(hass: HomeAssistantType, broker):
|
||||
"""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 approperiate handlers.
|
||||
|
||||
It appears that all TCC-compatible systems support the same three zones modes.
|
||||
"""
|
||||
|
||||
@verify_domain_control(hass, DOMAIN)
|
||||
async def force_refresh(call) -> None:
|
||||
"""Obtain the latest state data via the vendor's RESTful API."""
|
||||
await broker.async_update()
|
||||
|
||||
@verify_domain_control(hass, DOMAIN)
|
||||
async def set_system_mode(call) -> None:
|
||||
"""Set the system mode."""
|
||||
payload = {
|
||||
"unique_id": broker.tcs.systemId,
|
||||
"service": call.service,
|
||||
"data": call.data,
|
||||
}
|
||||
async_dispatcher_send(hass, DOMAIN, payload)
|
||||
|
||||
@verify_domain_control(hass, DOMAIN)
|
||||
async def set_zone_override(call) -> None:
|
||||
"""Set the zone override (setpoint)."""
|
||||
entity_id = call.data[ATTR_ENTITY_ID]
|
||||
|
||||
registry = await hass.helpers.entity_registry.async_get_registry()
|
||||
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)
|
||||
|
||||
hass.services.async_register(DOMAIN, SVC_REFRESH_SYSTEM, force_refresh)
|
||||
|
||||
# Enumerate which operating modes are supported by this system
|
||||
modes = broker.config["allowedSystemModes"]
|
||||
|
||||
# Not all systems support "AutoWithReset": register this handler only if required
|
||||
if [m["systemMode"] for m in modes if m["systemMode"] == "AutoWithReset"]:
|
||||
hass.services.async_register(DOMAIN, SVC_RESET_SYSTEM, set_system_mode)
|
||||
|
||||
system_mode_schemas = []
|
||||
modes = [m for m in modes if m["systemMode"] != "AutoWithReset"]
|
||||
|
||||
# Permanent-only modes will use this schema
|
||||
perm_modes = [m["systemMode"] for m in modes if not m["canBeTemporary"]]
|
||||
if perm_modes: # any of: "Auto", "HeatingOff": permanent only
|
||||
schema = vol.Schema({vol.Required(ATTR_SYSTEM_MODE): vol.In(perm_modes)})
|
||||
system_mode_schemas.append(schema)
|
||||
|
||||
modes = [m for m in modes if m["canBeTemporary"]]
|
||||
|
||||
# These modes are set for a number of hours (or indefinitely): use this schema
|
||||
temp_modes = [m["systemMode"] for m in modes if m["timingMode"] == "Duration"]
|
||||
if temp_modes: # any of: "AutoWithEco", permanent or for 0-24 hours
|
||||
schema = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_SYSTEM_MODE): vol.In(temp_modes),
|
||||
vol.Optional(ATTR_DURATION_HOURS): 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["systemMode"] for m in modes if m["timingMode"] == "Period"]
|
||||
if temp_modes: # any of: "Away", "Custom", "DayOff", permanent or for 1-99 days
|
||||
schema = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_SYSTEM_MODE): vol.In(temp_modes),
|
||||
vol.Optional(ATTR_DURATION_DAYS): 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,
|
||||
SVC_SET_SYSTEM_MODE,
|
||||
set_system_mode,
|
||||
schema=vol.Any(*system_mode_schemas),
|
||||
)
|
||||
|
||||
# The zone modes are consistent across all systems and use the same schema
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SVC_RESET_ZONE_OVERRIDE,
|
||||
set_zone_override,
|
||||
schema=RESET_ZONE_OVERRIDE_SCHEMA,
|
||||
)
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SVC_SET_ZONE_OVERRIDE,
|
||||
set_zone_override,
|
||||
schema=SET_ZONE_OVERRIDE_SCHEMA,
|
||||
)
|
||||
|
||||
|
||||
class EvoBroker:
|
||||
"""Container for evohome client and data."""
|
||||
|
||||
|
@ -238,7 +393,7 @@ class EvoBroker:
|
|||
loc_idx = params[CONF_LOCATION_IDX]
|
||||
self.config = client.installation_info[loc_idx][GWS][0][TCS][0]
|
||||
self.tcs = client.locations[loc_idx]._gateways[0]._control_systems[0]
|
||||
self.temps = None
|
||||
self.temps = {}
|
||||
|
||||
async def save_auth_tokens(self) -> None:
|
||||
"""Save access tokens and session IDs to the store for later use."""
|
||||
|
@ -260,6 +415,19 @@ class EvoBroker:
|
|||
|
||||
await self._store.async_save(app_storage)
|
||||
|
||||
async def call_client_api(self, api_function, refresh=True) -> Any:
|
||||
"""Call a client API."""
|
||||
try:
|
||||
result = await api_function
|
||||
except (aiohttp.ClientError, evohomeasync2.AuthenticationError) as err:
|
||||
if not _handle_exception(err):
|
||||
return
|
||||
|
||||
if refresh:
|
||||
self.hass.helpers.event.async_call_later(1, self.async_update())
|
||||
|
||||
return result
|
||||
|
||||
async def _update_v1(self, *args, **kwargs) -> None:
|
||||
"""Get the latest high-precision temperatures of the default Location."""
|
||||
|
||||
|
@ -311,15 +479,15 @@ class EvoBroker:
|
|||
except (aiohttp.ClientError, evohomeasync2.AuthenticationError) as err:
|
||||
_handle_exception(err)
|
||||
else:
|
||||
self.hass.helpers.dispatcher.async_dispatcher_send(DOMAIN)
|
||||
async_dispatcher_send(self.hass, DOMAIN)
|
||||
|
||||
_LOGGER.debug("Status = %s", status[GWS][0][TCS][0])
|
||||
|
||||
if access_token != self.client.access_token:
|
||||
await self.save_auth_tokens()
|
||||
|
||||
async def update(self, *args, **kwargs) -> None:
|
||||
"""Get the latest state data of an entire evohome Location.
|
||||
async def async_update(self, *args, **kwargs) -> None:
|
||||
"""Get the latest state data of an entire Honeywell TCC Location.
|
||||
|
||||
This includes state data for a Controller and all its child devices, such as the
|
||||
operating mode of the Controller and the current temp of its children (e.g.
|
||||
|
@ -331,7 +499,7 @@ class EvoBroker:
|
|||
await self._update_v1()
|
||||
|
||||
# inform the evohome devices that state data has been updated
|
||||
self.hass.helpers.dispatcher.async_dispatcher_send(DOMAIN)
|
||||
async_dispatcher_send(self.hass, DOMAIN)
|
||||
|
||||
|
||||
class EvoDevice(Entity):
|
||||
|
@ -351,9 +519,25 @@ class EvoDevice(Entity):
|
|||
self._supported_features = None
|
||||
self._device_state_attrs = {}
|
||||
|
||||
@callback
|
||||
def _refresh(self) -> None:
|
||||
self.async_schedule_update_ha_state(force_refresh=True)
|
||||
async def async_refresh(self, payload: Optional[dict] = None) -> None:
|
||||
"""Process any signals."""
|
||||
if payload is None:
|
||||
self.async_schedule_update_ha_state(force_refresh=True)
|
||||
return
|
||||
if payload["unique_id"] != self._unique_id:
|
||||
return
|
||||
if payload["service"] in [SVC_SET_ZONE_OVERRIDE, SVC_RESET_ZONE_OVERRIDE]:
|
||||
await self.async_zone_svc_request(payload["service"], payload["data"])
|
||||
return
|
||||
await self.async_tcs_svc_request(payload["service"], payload["data"])
|
||||
|
||||
async def async_tcs_svc_request(self, service: dict, data: dict) -> None:
|
||||
"""Process a service request (system mode) for a controller."""
|
||||
raise NotImplementedError
|
||||
|
||||
async def async_zone_svc_request(self, service: dict, data: dict) -> None:
|
||||
"""Process a service request (setpoint override) for a zone."""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def should_poll(self) -> bool:
|
||||
|
@ -367,12 +551,12 @@ class EvoDevice(Entity):
|
|||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return the name of the Evohome entity."""
|
||||
"""Return the name of the evohome entity."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def device_state_attributes(self) -> Dict[str, Any]:
|
||||
"""Return the Evohome-specific state attributes."""
|
||||
"""Return the evohome-specific state attributes."""
|
||||
status = self._device_state_attrs
|
||||
if "systemModeStatus" in status:
|
||||
convert_until(status["systemModeStatus"], "timeUntil")
|
||||
|
@ -395,7 +579,7 @@ class EvoDevice(Entity):
|
|||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Run when entity about to be added to hass."""
|
||||
self.hass.helpers.dispatcher.async_dispatcher_connect(DOMAIN, self._refresh)
|
||||
async_dispatcher_connect(self.hass, DOMAIN, self.async_refresh)
|
||||
|
||||
@property
|
||||
def precision(self) -> float:
|
||||
|
@ -407,18 +591,6 @@ class EvoDevice(Entity):
|
|||
"""Return the temperature unit to use in the frontend UI."""
|
||||
return TEMP_CELSIUS
|
||||
|
||||
async def _call_client_api(self, api_function, refresh=True) -> Any:
|
||||
try:
|
||||
result = await api_function
|
||||
except (aiohttp.ClientError, evohomeasync2.AuthenticationError) as err:
|
||||
if not _handle_exception(err):
|
||||
return
|
||||
|
||||
if refresh is True:
|
||||
self.hass.helpers.event.async_call_later(1, self._evo_broker.update())
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class EvoChild(EvoDevice):
|
||||
"""Base for any evohome child.
|
||||
|
@ -497,12 +669,12 @@ class EvoChild(EvoDevice):
|
|||
return self._setpoints
|
||||
|
||||
async def _update_schedule(self) -> None:
|
||||
"""Get the latest schedule."""
|
||||
"""Get the latest schedule, if any."""
|
||||
if "DailySchedules" in self._schedule and not self._schedule["DailySchedules"]:
|
||||
if not self._evo_device.setpointStatus["setpointMode"] == EVO_FOLLOW:
|
||||
return # avoid unnecessary I/O - there's nothing to update
|
||||
|
||||
self._schedule = await self._call_client_api(
|
||||
self._schedule = await self._evo_broker.call_client_api(
|
||||
self._evo_device.schedule(), refresh=False
|
||||
)
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
"""Support for Climate devices of (EMEA/EU-based) Honeywell TCC systems."""
|
||||
from datetime import datetime as dt
|
||||
import logging
|
||||
from typing import List, Optional
|
||||
|
||||
|
@ -21,7 +22,18 @@ from homeassistant.const import PRECISION_TENTHS
|
|||
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
|
||||
from homeassistant.util.dt import parse_datetime
|
||||
|
||||
from . import CONF_LOCATION_IDX, EvoChild, EvoDevice
|
||||
from . import (
|
||||
ATTR_DURATION_DAYS,
|
||||
ATTR_DURATION_HOURS,
|
||||
ATTR_DURATION_UNTIL,
|
||||
ATTR_SYSTEM_MODE,
|
||||
ATTR_ZONE_TEMP,
|
||||
CONF_LOCATION_IDX,
|
||||
SVC_RESET_ZONE_OVERRIDE,
|
||||
SVC_SET_SYSTEM_MODE,
|
||||
EvoChild,
|
||||
EvoDevice,
|
||||
)
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
EVO_AUTO,
|
||||
|
@ -90,8 +102,9 @@ async def async_setup_platform(
|
|||
zone.zoneId,
|
||||
zone.name,
|
||||
)
|
||||
new_entity = EvoThermostat(broker, zone)
|
||||
|
||||
async_add_entities([EvoThermostat(broker, zone)], update_before_add=True)
|
||||
async_add_entities([new_entity], update_before_add=True)
|
||||
return
|
||||
|
||||
controller = EvoController(broker, broker.tcs)
|
||||
|
@ -105,13 +118,15 @@ async def async_setup_platform(
|
|||
zone.zoneId,
|
||||
zone.name,
|
||||
)
|
||||
zones.append(EvoZone(broker, zone))
|
||||
new_entity = EvoZone(broker, zone)
|
||||
|
||||
zones.append(new_entity)
|
||||
|
||||
async_add_entities([controller] + zones, update_before_add=True)
|
||||
|
||||
|
||||
class EvoClimateDevice(EvoDevice, ClimateDevice):
|
||||
"""Base for a Honeywell evohome Climate device."""
|
||||
"""Base for an evohome Climate device."""
|
||||
|
||||
def __init__(self, evo_broker, evo_device) -> None:
|
||||
"""Initialize a Climate device."""
|
||||
|
@ -119,9 +134,31 @@ class EvoClimateDevice(EvoDevice, ClimateDevice):
|
|||
|
||||
self._preset_modes = None
|
||||
|
||||
async def _set_tcs_mode(self, op_mode: str) -> None:
|
||||
async def async_tcs_svc_request(self, service: dict, data: dict) -> None:
|
||||
"""Process a service request (system mode) for a controller.
|
||||
|
||||
Data validation is not required, it will have been done upstream.
|
||||
"""
|
||||
if service == SVC_SET_SYSTEM_MODE:
|
||||
mode = data[ATTR_SYSTEM_MODE]
|
||||
else: # otherwise it is SVC_RESET_SYSTEM
|
||||
mode = EVO_RESET
|
||||
|
||||
if ATTR_DURATION_DAYS in data:
|
||||
until = dt.combine(dt.now().date(), dt.min.time())
|
||||
until += data[ATTR_DURATION_DAYS]
|
||||
|
||||
elif ATTR_DURATION_HOURS in data:
|
||||
until = dt.now() + data[ATTR_DURATION_HOURS]
|
||||
|
||||
else:
|
||||
until = None
|
||||
|
||||
await self._set_tcs_mode(mode, until=until)
|
||||
|
||||
async def _set_tcs_mode(self, mode: str, until: Optional[dt] = None) -> None:
|
||||
"""Set a Controller to any of its native EVO_* operating modes."""
|
||||
await self._call_client_api(self._evo_tcs.set_status(op_mode))
|
||||
await self._evo_broker.call_client_api(self._evo_tcs.set_status(mode))
|
||||
|
||||
@property
|
||||
def hvac_modes(self) -> List[str]:
|
||||
|
@ -135,7 +172,7 @@ class EvoClimateDevice(EvoDevice, ClimateDevice):
|
|||
|
||||
|
||||
class EvoZone(EvoChild, EvoClimateDevice):
|
||||
"""Base for a Honeywell evohome Zone."""
|
||||
"""Base for a Honeywell TCC Zone."""
|
||||
|
||||
def __init__(self, evo_broker, evo_device) -> None:
|
||||
"""Initialize a Zone."""
|
||||
|
@ -152,6 +189,32 @@ class EvoZone(EvoChild, EvoClimateDevice):
|
|||
else:
|
||||
self._precision = self._evo_device.setpointCapabilities["valueResolution"]
|
||||
|
||||
async def async_zone_svc_request(self, service: dict, data: dict) -> None:
|
||||
"""Process a service request (setpoint override) for a zone."""
|
||||
if service == SVC_RESET_ZONE_OVERRIDE:
|
||||
await self._evo_broker.call_client_api(
|
||||
self._evo_device.cancel_temp_override()
|
||||
)
|
||||
return
|
||||
|
||||
# otherwise it is SVC_SET_ZONE_OVERRIDE
|
||||
temp = round(data[ATTR_ZONE_TEMP] * self.precision) / self.precision
|
||||
temp = max(min(temp, self.max_temp), self.min_temp)
|
||||
|
||||
if ATTR_DURATION_UNTIL in data:
|
||||
duration = data[ATTR_DURATION_UNTIL]
|
||||
if duration == 0:
|
||||
await self._update_schedule()
|
||||
until = parse_datetime(str(self.setpoints.get("next_sp_from")))
|
||||
else:
|
||||
until = dt.now() + data[ATTR_DURATION_UNTIL]
|
||||
else:
|
||||
until = None # indefinitely
|
||||
|
||||
await self._evo_broker.call_client_api(
|
||||
self._evo_device.set_temperature(temperature=temp, until=until)
|
||||
)
|
||||
|
||||
@property
|
||||
def hvac_mode(self) -> str:
|
||||
"""Return the current operating mode of a Zone."""
|
||||
|
@ -206,16 +269,16 @@ class EvoZone(EvoChild, EvoClimateDevice):
|
|||
async def async_set_temperature(self, **kwargs) -> None:
|
||||
"""Set a new target temperature."""
|
||||
temperature = kwargs["temperature"]
|
||||
until = kwargs.get("until")
|
||||
|
||||
if self._evo_device.setpointStatus["setpointMode"] == EVO_FOLLOW:
|
||||
await self._update_schedule()
|
||||
until = parse_datetime(str(self.setpoints.get("next_sp_from")))
|
||||
elif self._evo_device.setpointStatus["setpointMode"] == EVO_TEMPOVER:
|
||||
until = parse_datetime(self._evo_device.setpointStatus["until"])
|
||||
else: # EVO_PERMOVER
|
||||
until = None
|
||||
if until is None:
|
||||
if self._evo_device.setpointStatus["setpointMode"] == EVO_FOLLOW:
|
||||
await self._update_schedule()
|
||||
until = parse_datetime(str(self.setpoints.get("next_sp_from")))
|
||||
elif self._evo_device.setpointStatus["setpointMode"] == EVO_TEMPOVER:
|
||||
until = parse_datetime(self._evo_device.setpointStatus["until"])
|
||||
|
||||
await self._call_client_api(
|
||||
await self._evo_broker.call_client_api(
|
||||
self._evo_device.set_temperature(temperature, until)
|
||||
)
|
||||
|
||||
|
@ -237,18 +300,22 @@ class EvoZone(EvoChild, EvoClimateDevice):
|
|||
and 'Away', Zones to (by default) 12C.
|
||||
"""
|
||||
if hvac_mode == HVAC_MODE_OFF:
|
||||
await self._call_client_api(
|
||||
await self._evo_broker.call_client_api(
|
||||
self._evo_device.set_temperature(self.min_temp, until=None)
|
||||
)
|
||||
else: # HVAC_MODE_HEAT
|
||||
await self._call_client_api(self._evo_device.cancel_temp_override())
|
||||
await self._evo_broker.call_client_api(
|
||||
self._evo_device.cancel_temp_override()
|
||||
)
|
||||
|
||||
async def async_set_preset_mode(self, preset_mode: Optional[str]) -> None:
|
||||
"""Set the preset mode; if None, then revert to following the schedule."""
|
||||
evo_preset_mode = HA_PRESET_TO_EVO.get(preset_mode, EVO_FOLLOW)
|
||||
|
||||
if evo_preset_mode == EVO_FOLLOW:
|
||||
await self._call_client_api(self._evo_device.cancel_temp_override())
|
||||
await self._evo_broker.call_client_api(
|
||||
self._evo_device.cancel_temp_override()
|
||||
)
|
||||
return
|
||||
|
||||
temperature = self._evo_device.setpointStatus["targetHeatTemperature"]
|
||||
|
@ -259,7 +326,7 @@ class EvoZone(EvoChild, EvoClimateDevice):
|
|||
else: # EVO_PERMOVER
|
||||
until = None
|
||||
|
||||
await self._call_client_api(
|
||||
await self._evo_broker.call_client_api(
|
||||
self._evo_device.set_temperature(temperature, until)
|
||||
)
|
||||
|
||||
|
@ -272,14 +339,14 @@ class EvoZone(EvoChild, EvoClimateDevice):
|
|||
|
||||
|
||||
class EvoController(EvoClimateDevice):
|
||||
"""Base for a Honeywell evohome Controller (hub).
|
||||
"""Base for a Honeywell TCC Controller (hub).
|
||||
|
||||
The Controller (aka TCS, temperature control system) is the parent of all
|
||||
the child (CH/DHW) devices. It is also a Climate device.
|
||||
"""
|
||||
|
||||
def __init__(self, evo_broker, evo_device) -> None:
|
||||
"""Initialize a evohome Controller (hub)."""
|
||||
"""Initialize an evohome Controller (hub)."""
|
||||
super().__init__(evo_broker, evo_device)
|
||||
|
||||
self._unique_id = evo_device.systemId
|
||||
|
@ -349,7 +416,7 @@ class EvoController(EvoClimateDevice):
|
|||
|
||||
|
||||
class EvoThermostat(EvoZone):
|
||||
"""Base for a Honeywell Round Thermostat.
|
||||
"""Base for a Honeywell TCC Round Thermostat.
|
||||
|
||||
These are implemented as a combined Controller/Zone.
|
||||
"""
|
||||
|
|
|
@ -13,7 +13,7 @@ EVO_DAYOFF = "DayOff"
|
|||
EVO_CUSTOM = "Custom"
|
||||
EVO_HEATOFF = "HeatingOff"
|
||||
|
||||
# The Childs' operating mode is one of:
|
||||
# The Children's operating mode is one of:
|
||||
EVO_FOLLOW = "FollowSchedule" # the operating mode is 'inherited' from the TCS
|
||||
EVO_TEMPOVER = "TemporaryOverride"
|
||||
EVO_PERMOVER = "PermanentOverride"
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
# Support for (EMEA/EU-based) Honeywell TCC climate systems.
|
||||
# Describes the format for available services
|
||||
|
||||
set_system_mode:
|
||||
description: >-
|
||||
Set the system mode, either indefinitely, or for a specified period of time, after
|
||||
which it will revert to Auto. Not all systems support all modes.
|
||||
fields:
|
||||
mode:
|
||||
description: 'One of: Auto, AutoWithEco, Away, DayOff, HeatingOff, or Custom.'
|
||||
example: Away
|
||||
period:
|
||||
description: >-
|
||||
A period of time in days; used only with Away, DayOff, or Custom. The system
|
||||
will revert to Auto at midnight (up to 99 days, today is day 1).
|
||||
example: '{"days": 28}'
|
||||
duration:
|
||||
description: The duration in hours; used only with AutoWithEco (up to 24 hours).
|
||||
example: '{"hours": 18}'
|
||||
|
||||
reset_system:
|
||||
description: >-
|
||||
Set the system to Auto mode and reset all the zones to follow their schedules.
|
||||
Not all Evohome systems support this feature (i.e. AutoWithReset mode).
|
||||
|
||||
refresh_system:
|
||||
description: >-
|
||||
Pull the latest data from the vendor's servers now, rather than waiting for the
|
||||
next scheduled update.
|
||||
|
||||
set_zone_override:
|
||||
description: >-
|
||||
Override a zone's setpoint, either indefinitely, or for a specified period of
|
||||
time, after which it will revert to following its schedule.
|
||||
fields:
|
||||
entity_id:
|
||||
description: The entity_id of the Evohome zone.
|
||||
example: climate.bathroom
|
||||
setpoint:
|
||||
description: The temperature to be used instead of the scheduled setpoint.
|
||||
example: 5.0
|
||||
duration:
|
||||
description: >-
|
||||
The zone will revert to its schedule after this time. If 0 the change is until
|
||||
the next scheduled setpoint.
|
||||
example: '{"minutes": 135}'
|
||||
|
||||
clear_zone_override:
|
||||
description: Set a zone to follow its schedule.
|
||||
fields:
|
||||
entity_id:
|
||||
description: The entity_id of the zone.
|
||||
example: climate.bathroom
|
|
@ -38,17 +38,16 @@ async def async_setup_platform(
|
|||
broker.tcs.hotwater.zone_type,
|
||||
broker.tcs.hotwater.zoneId,
|
||||
)
|
||||
new_entity = EvoDHW(broker, broker.tcs.hotwater)
|
||||
|
||||
evo_dhw = EvoDHW(broker, broker.tcs.hotwater)
|
||||
|
||||
async_add_entities([evo_dhw], update_before_add=True)
|
||||
async_add_entities([new_entity], update_before_add=True)
|
||||
|
||||
|
||||
class EvoDHW(EvoChild, WaterHeaterDevice):
|
||||
"""Base for a Honeywell evohome DHW controller (aka boiler)."""
|
||||
"""Base for a Honeywell TCC DHW controller (aka boiler)."""
|
||||
|
||||
def __init__(self, evo_broker, evo_device) -> None:
|
||||
"""Initialize a evohome DHW controller."""
|
||||
"""Initialize an evohome DHW controller."""
|
||||
super().__init__(evo_broker, evo_device)
|
||||
|
||||
self._unique_id = evo_device.dhwId
|
||||
|
@ -88,23 +87,27 @@ class EvoDHW(EvoChild, WaterHeaterDevice):
|
|||
Except for Auto, the mode is only until the next SetPoint.
|
||||
"""
|
||||
if operation_mode == STATE_AUTO:
|
||||
await self._call_client_api(self._evo_device.set_dhw_auto())
|
||||
await self._evo_broker.call_client_api(self._evo_device.set_dhw_auto())
|
||||
else:
|
||||
await self._update_schedule()
|
||||
until = parse_datetime(str(self.setpoints.get("next_sp_from")))
|
||||
|
||||
if operation_mode == STATE_ON:
|
||||
await self._call_client_api(self._evo_device.set_dhw_on(until))
|
||||
await self._evo_broker.call_client_api(
|
||||
self._evo_device.set_dhw_on(until)
|
||||
)
|
||||
else: # STATE_OFF
|
||||
await self._call_client_api(self._evo_device.set_dhw_off(until))
|
||||
await self._evo_broker.call_client_api(
|
||||
self._evo_device.set_dhw_off(until)
|
||||
)
|
||||
|
||||
async def async_turn_away_mode_on(self):
|
||||
"""Turn away mode on."""
|
||||
await self._call_client_api(self._evo_device.set_dhw_off())
|
||||
await self._evo_broker.call_client_api(self._evo_device.set_dhw_off())
|
||||
|
||||
async def async_turn_away_mode_off(self):
|
||||
"""Turn away mode off."""
|
||||
await self._call_client_api(self._evo_device.set_dhw_auto())
|
||||
await self._evo_broker.call_client_api(self._evo_device.set_dhw_auto())
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Get the latest state data for a DHW controller."""
|
||||
|
|
Loading…
Reference in New Issue