1 """Support for Climate entities of the Evohome integration."""
3 from __future__
import annotations
5 from datetime
import datetime, timedelta
7 from typing
import TYPE_CHECKING, Any
9 import evohomeasync2
as evo
10 from evohomeasync2.schema.const
import (
15 SZ_SYSTEM_MODE_STATUS,
16 SZ_TEMPERATURE_STATUS,
58 from .entity
import EvoChild, EvoDevice
61 from .
import EvoBroker
64 _LOGGER = logging.getLogger(__name__)
66 PRESET_RESET =
"Reset"
67 PRESET_CUSTOM =
"Custom"
70 EVO_AWAY: PRESET_AWAY,
71 EVO_CUSTOM: PRESET_CUSTOM,
72 EVO_AUTOECO: PRESET_ECO,
73 EVO_DAYOFF: PRESET_HOME,
74 EVO_RESET: PRESET_RESET,
77 HA_PRESET_TO_TCS = {v: k
for k, v
in TCS_PRESET_TO_HA.items()}
80 EVO_FOLLOW: PRESET_NONE,
81 EVO_TEMPOVER:
"temporary",
82 EVO_PERMOVER:
"permanent",
84 HA_PRESET_TO_EVO = {v: k
for k, v
in EVO_PRESET_TO_HA.items()}
86 STATE_ATTRS_TCS = [SZ_SYSTEM_ID, SZ_ACTIVE_FAULTS, SZ_SYSTEM_MODE_STATUS]
91 SZ_TEMPERATURE_STATUS,
98 async_add_entities: AddEntitiesCallback,
99 discovery_info: DiscoveryInfoType |
None =
None,
101 """Create the evohome Controller, and its Zones, if any."""
102 if discovery_info
is None:
105 broker: EvoBroker = hass.data[DOMAIN][
"broker"]
108 "Found the Location/Controller (%s), id=%s, name=%s (location_idx=%s)",
109 broker.tcs.modelType,
115 entities: list[EvoClimateEntity] = [
EvoController(broker, broker.tcs)]
117 for zone
in broker.tcs.zones.values():
119 zone.modelType == ZoneModelType.HEATING_ZONE
120 or zone.zoneType == ZoneType.THERMOSTAT
123 "Adding: %s (%s), id=%s, name=%s",
130 new_entity =
EvoZone(broker, zone)
131 entities.append(new_entity)
136 "Ignoring: %s (%s), id=%s, name=%s: unknown/invalid zone type, "
137 "report as an issue if you feel this zone type should be supported"
149 """Base for any evohome-compatible climate entity (controller, zone)."""
151 _attr_hvac_modes = [HVACMode.OFF, HVACMode.HEAT]
152 _attr_temperature_unit = UnitOfTemperature.CELSIUS
153 _enable_turn_on_off_backwards_compatibility =
False
157 """Base for any evohome-compatible heating zone."""
159 _attr_preset_modes =
list(HA_PRESET_TO_EVO)
161 _evo_device: evo.Zone
163 def __init__(self, evo_broker: EvoBroker, evo_device: evo.Zone) ->
None:
164 """Initialize an evohome-compatible heating zone."""
166 super().
__init__(evo_broker, evo_device)
169 if evo_device.modelType.startswith(
"VisionProWifi"):
175 if evo_broker.client_v1:
183 ClimateEntityFeature.PRESET_MODE
184 | ClimateEntityFeature.TARGET_TEMPERATURE
185 | ClimateEntityFeature.TURN_OFF
186 | ClimateEntityFeature.TURN_ON
190 """Process a service request (setpoint override) for a zone."""
191 if service == EvoService.RESET_ZONE_OVERRIDE:
198 if ATTR_DURATION_UNTIL
in data:
199 duration: timedelta = data[ATTR_DURATION_UNTIL]
200 if duration.total_seconds() == 0:
202 until = dt_util.parse_datetime(self.
setpointssetpoints.
get(
"next_sp_from",
""))
204 until = dt_util.now() + data[ATTR_DURATION_UNTIL]
208 until = dt_util.as_utc(until)
if until
else None
215 """Return the name of the evohome entity."""
220 """Return the current operating mode of a Zone."""
221 if self.
_evo_tcs_evo_tcs.system_mode
in (EVO_AWAY, EVO_HEATOFF):
231 """Return the target temperature of a Zone."""
232 return self.
_evo_device_evo_device.target_heat_temperature
236 """Return the current preset mode, e.g., home, away, temp."""
237 if self.
_evo_tcs_evo_tcs.system_mode
in (EVO_AWAY, EVO_HEATOFF):
238 return TCS_PRESET_TO_HA.get(self.
_evo_tcs_evo_tcs.system_mode)
241 return EVO_PRESET_TO_HA.get(self.
_evo_device_evo_device.mode)
245 """Return the minimum target temperature of a Zone.
247 The default is 5, but is user-configurable within 5-21 (in Celsius).
249 if self.
_evo_device_evo_device.min_heat_setpoint
is None:
251 return self.
_evo_device_evo_device.min_heat_setpoint
255 """Return the maximum target temperature of a Zone.
257 The default is 35, but is user-configurable within 21-35 (in Celsius).
259 if self.
_evo_device_evo_device.max_heat_setpoint
is None:
261 return self.
_evo_device_evo_device.max_heat_setpoint
264 """Set a new target temperature."""
266 assert self.
_evo_device_evo_device.setpointStatus
is not None
268 temperature = kwargs[
"temperature"]
270 if (until := kwargs.get(
"until"))
is None:
273 until = dt_util.parse_datetime(self.
setpointssetpoints.
get(
"next_sp_from",
""))
274 elif self.
_evo_device_evo_device.mode == EVO_TEMPOVER:
275 until = dt_util.parse_datetime(
276 self.
_evo_device_evo_device.setpointStatus[SZ_UNTIL]
279 until = dt_util.as_utc(until)
if until
else None
285 """Set a Zone to one of its native EVO_* operating modes.
287 Zones inherit their _effective_ operating mode from their Controller.
289 Usually, Zones are in 'FollowSchedule' mode, where their setpoints are a
290 function of their own schedule and the Controller's operating mode, e.g.
291 'AutoWithEco' mode means their setpoint is (by default) 3C less than scheduled.
293 However, Zones can _override_ these setpoints, either indefinitely,
294 'PermanentOverride' mode, or for a set period of time, 'TemporaryOverride' mode
295 (after which they will revert back to 'FollowSchedule' mode).
297 Finally, some of the Controller's operating modes are _forced_ upon the Zones,
298 regardless of any override mode, e.g. 'HeatingOff', Zones to (by default) 5C,
299 and 'Away', Zones to (by default) 12C.
301 if hvac_mode == HVACMode.OFF:
309 """Set the preset mode; if None, then revert to following the schedule."""
310 evo_preset_mode = HA_PRESET_TO_EVO.get(preset_mode, EVO_FOLLOW)
312 if evo_preset_mode == EVO_FOLLOW:
316 if evo_preset_mode == EVO_TEMPOVER:
318 until = dt_util.parse_datetime(self.
setpointssetpoints.
get(
"next_sp_from",
""))
322 temperature = self.
_evo_device_evo_device.target_heat_temperature
323 assert temperature
is not None
325 until = dt_util.as_utc(until)
if until
else None
331 """Get the latest state data for a Zone."""
334 for attr
in STATE_ATTRS_ZONES:
339 """Base for any evohome-compatible controller.
341 The Controller (aka TCS, temperature control system) is the parent of all the child
342 (CH/DHW) devices. It is implemented as a Climate entity to expose the controller's
343 operating modes to HA.
345 It is assumed there is only one TCS per location, and they are thus synonymous.
348 _attr_icon =
"mdi:thermostat"
349 _attr_precision = PRECISION_TENTHS
351 _evo_device: evo.ControlSystem
353 def __init__(self, evo_broker: EvoBroker, evo_device: evo.ControlSystem) ->
None:
354 """Initialize an evohome-compatible controller."""
356 super().
__init__(evo_broker, evo_device)
362 self.
_evo_modes_evo_modes = [m[SZ_SYSTEM_MODE]
for m
in evo_device.allowedSystemModes]
364 TCS_PRESET_TO_HA[m]
for m
in self.
_evo_modes_evo_modes
if m
in list(TCS_PRESET_TO_HA)
369 ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON
373 """Process a service request (system mode) for a controller.
375 Data validation is not required, it will have been done upstream.
377 if service == EvoService.SET_SYSTEM_MODE:
378 mode = data[ATTR_SYSTEM_MODE]
382 if ATTR_DURATION_DAYS
in data:
383 until = dt_util.start_of_local_day()
384 until += data[ATTR_DURATION_DAYS]
386 elif ATTR_DURATION_HOURS
in data:
387 until = dt_util.now() + data[ATTR_DURATION_HOURS]
394 async
def _set_tcs_mode(self, mode: str, until: datetime |
None =
None) ->
None:
395 """Set a Controller to any of its native EVO_* operating modes."""
396 until = dt_util.as_utc(until)
if until
else None
398 self.
_evo_device_evo_device.set_mode(mode, until=until)
403 """Return the current operating mode of a Controller."""
405 return HVACMode.OFF
if evo_mode
in (EVO_HEATOFF,
"Off")
else HVACMode.HEAT
409 """Return the average current temperature of the heating Zones.
411 Controllers do not have a current temp, but one is expected by HA.
415 for z
in self.
_evo_device_evo_device.zones.values()
416 if z.temperature
is not None
418 return round(sum(temps) / len(temps), 1)
if temps
else None
422 """Return the current preset mode, e.g., home, away, temp."""
425 return TCS_PRESET_TO_HA.get(self.
_evo_device_evo_device.system_mode)
428 """Raise exception as Controllers don't have a target temperature."""
429 raise NotImplementedError(
"Evohome Controllers don't have target temperatures.")
432 """Set an operating mode for a Controller."""
433 if hvac_mode == HVACMode.HEAT:
434 evo_mode = EVO_AUTO
if EVO_AUTO
in self.
_evo_modes_evo_modes
else "Heat"
435 elif hvac_mode == HVACMode.OFF:
436 evo_mode = EVO_HEATOFF
if EVO_HEATOFF
in self.
_evo_modes_evo_modes
else "Off"
442 """Set the preset mode; if None, then revert to 'Auto' mode."""
443 await self.
_set_tcs_mode_set_tcs_mode(HA_PRESET_TO_TCS.get(preset_mode, EVO_AUTO))
446 """Get the latest state data for a Controller."""
450 for attr
in STATE_ATTRS_TCS:
451 if attr == SZ_ACTIVE_FAULTS:
452 attrs[
"activeSystemFaults"] = getattr(self.
_evo_device_evo_device, attr)
454 attrs[attr] = getattr(self.
_evo_device_evo_device, attr)
None set_temperature(self, **Any kwargs)
float|None target_temperature(self)
None async_set_preset_mode(self, str preset_mode)
None __init__(self, EvoBroker evo_broker, evo.ControlSystem evo_device)
str|None preset_mode(self)
float|None current_temperature(self)
None async_tcs_svc_request(self, str service, dict[str, Any] data)
None async_set_hvac_mode(self, HVACMode hvac_mode)
None _set_tcs_mode(self, str mode, datetime|None until=None)
None async_set_temperature(self, **Any kwargs)
None __init__(self, EvoBroker evo_broker, evo.Zone evo_device)
float|None target_temperature(self)
None async_set_hvac_mode(self, HVACMode hvac_mode)
None async_set_temperature(self, **Any kwargs)
str|None preset_mode(self)
None async_zone_svc_request(self, str service, dict[str, Any] data)
None async_set_preset_mode(self, str preset_mode)
None _update_schedule(self)
dict[str, Any] setpoints(self)
web.Response get(self, web.Request request, str config_key)
None async_setup_platform(HomeAssistant hass, ConfigType config, AddEntitiesCallback async_add_entities, DiscoveryInfoType|None discovery_info=None)