diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index 3d903e86e30..b9d3f35964a 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -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 ) diff --git a/homeassistant/components/evohome/climate.py b/homeassistant/components/evohome/climate.py index 3da11bc8087..50155e6dd21 100644 --- a/homeassistant/components/evohome/climate.py +++ b/homeassistant/components/evohome/climate.py @@ -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. """ diff --git a/homeassistant/components/evohome/const.py b/homeassistant/components/evohome/const.py index 444671cf82a..eaa7048e53b 100644 --- a/homeassistant/components/evohome/const.py +++ b/homeassistant/components/evohome/const.py @@ -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" diff --git a/homeassistant/components/evohome/services.yaml b/homeassistant/components/evohome/services.yaml new file mode 100644 index 00000000000..ebc859ed9e3 --- /dev/null +++ b/homeassistant/components/evohome/services.yaml @@ -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 diff --git a/homeassistant/components/evohome/water_heater.py b/homeassistant/components/evohome/water_heater.py index e29dbb49af2..cd4fb2aadce 100644 --- a/homeassistant/components/evohome/water_heater.py +++ b/homeassistant/components/evohome/water_heater.py @@ -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."""