1 """Support for (EMEA/EU-based) Honeywell TCC systems.
3 Such systems provide heating/cooling and DHW and include Evohome, Round Thermostat, and
7 from __future__
import annotations
9 from datetime
import timedelta
11 from typing
import Any, Final
13 import evohomeasync
as ev1
14 from evohomeasync.schema
import SZ_SESSION_ID
15 import evohomeasync2
as evo
16 from evohomeasync2.schema.const
import (
22 import voluptuous
as vol
54 SCAN_INTERVAL_DEFAULT,
55 SCAN_INTERVAL_MINIMUM,
61 from .coordinator
import EvoBroker
62 from .helpers
import dt_aware_to_naive, dt_local_to_aware, handle_evo_exception
64 _LOGGER = logging.getLogger(__name__)
66 CONFIG_SCHEMA: Final = vol.Schema(
70 vol.Required(CONF_USERNAME): cv.string,
71 vol.Required(CONF_PASSWORD): cv.string,
72 vol.Optional(CONF_LOCATION_IDX, default=0): cv.positive_int,
74 CONF_SCAN_INTERVAL, default=SCAN_INTERVAL_DEFAULT
75 ): vol.All(cv.time_period, vol.Range(min=SCAN_INTERVAL_MINIMUM)),
79 extra=vol.ALLOW_EXTRA,
85 RESET_ZONE_OVERRIDE_SCHEMA: Final = vol.Schema(
86 {vol.Required(ATTR_ENTITY_ID): cv.entity_id}
88 SET_ZONE_OVERRIDE_SCHEMA: Final = vol.Schema(
90 vol.Required(ATTR_ENTITY_ID): cv.entity_id,
91 vol.Required(ATTR_ZONE_TEMP): vol.All(
92 vol.Coerce(float), vol.Range(min=4.0, max=35.0)
94 vol.Optional(ATTR_DURATION_UNTIL): vol.All(
102 """Class for evohome client instantiation & authentication."""
105 """Initialize the evohome broker and its data structure."""
110 self.
_store_store = Store[dict[str, Any]](hass, STORAGE_VER, STORAGE_KEY)
113 self.
client_v2client_v2: evo.EvohomeClient |
None =
None
114 self.
_tokens_tokens: dict[str, Any] = {}
117 self.
client_v1client_v1: ev1.EvohomeClient |
None =
None
121 """Check the user credentials against the web API.
123 Will raise evo.AuthenticationFailed if the credentials are invalid.
128 or username != self.
client_v2client_v2.username
129 or password != self.
client_v2client_v2.password
133 client_v2 = evo.EvohomeClient(
142 client_v2._user_account =
None
144 await client_v2.login()
157 """Load access tokens and session_id from the store and validate them.
159 Sets self._tokens and self._session_id to the latest values.
164 if app_storage.pop(CONF_USERNAME,
None) != username:
174 if app_storage.get(ACCESS_TOKEN_EXPIRES)
is not None and (
175 expires := dt_util.parse_datetime(app_storage[ACCESS_TOKEN_EXPIRES])
179 user_data: dict[str, str] = app_storage.pop(USER_DATA, {})
or {}
181 self.
session_idsession_id = user_data.get(SZ_SESSION_ID)
182 self.
_tokens_tokens = app_storage
185 """Save access tokens and session_id to the store.
187 Sets self._tokens and self._session_id to the latest values.
196 self.
client_v2client_v2.access_token_expires
200 CONF_USERNAME: self.
client_v2client_v2.username,
201 REFRESH_TOKEN: self.
client_v2client_v2.refresh_token,
202 ACCESS_TOKEN: self.
client_v2client_v2.access_token,
203 ACCESS_TOKEN_EXPIRES: access_token_expires.isoformat(),
208 app_storage = self.
_tokens_tokens
210 app_storage[USER_DATA] = {SZ_SESSION_ID: self.
session_idsession_id}
215 async
def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
216 """Set up the Evohome integration."""
221 await sess.authenticate(
222 config[DOMAIN][CONF_USERNAME],
223 config[DOMAIN][CONF_PASSWORD],
226 except (evo.AuthenticationFailed, evo.RequestFailed)
as err:
231 config[DOMAIN][CONF_PASSWORD] =
"REDACTED"
235 if not broker.validate_location(
236 config[DOMAIN][CONF_LOCATION_IDX],
244 name=f
"{DOMAIN}_coordinator",
245 update_interval=config[DOMAIN][CONF_SCAN_INTERVAL],
246 update_method=broker.async_update,
248 await coordinator.async_register_shutdown()
250 hass.data[DOMAIN] = {
"broker": broker,
"coordinator": coordinator}
253 coordinator.async_add_listener(
lambda:
None)
254 await coordinator.async_refresh()
256 hass.async_create_task(
259 if broker.tcs.hotwater:
260 hass.async_create_task(
271 """Set up the service handlers for the system/zone operating modes.
273 Not all Honeywell TCC-compatible systems support all operating modes. In addition,
274 each mode will require any of four distinct service schemas. This has to be
275 enumerated before registering the appropriate handlers.
277 It appears that all TCC-compatible systems support the same three zones modes.
280 @verify_domain_control(hass, DOMAIN)
281 async
def force_refresh(call: ServiceCall) ->
None:
282 """Obtain the latest state data via the vendor's RESTful API."""
283 await broker.async_update()
285 @verify_domain_control(hass, DOMAIN)
286 async
def set_system_mode(call: ServiceCall) ->
None:
287 """Set the system mode."""
289 "unique_id": broker.tcs.systemId,
290 "service": call.service,
295 @verify_domain_control(hass, DOMAIN)
296 async
def set_zone_override(call: ServiceCall) ->
None:
297 """Set the zone override (setpoint)."""
298 entity_id = call.data[ATTR_ENTITY_ID]
300 registry = er.async_get(hass)
301 registry_entry = registry.async_get(entity_id)
303 if registry_entry
is None or registry_entry.platform != DOMAIN:
304 raise ValueError(f
"'{entity_id}' is not a known {DOMAIN} entity")
306 if registry_entry.domain !=
"climate":
307 raise ValueError(f
"'{entity_id}' is not an {DOMAIN} controller/zone")
310 "unique_id": registry_entry.unique_id,
311 "service": call.service,
317 hass.services.async_register(DOMAIN, EvoService.REFRESH_SYSTEM, force_refresh)
320 modes = broker.tcs.allowedSystemModes
323 if [m[SZ_SYSTEM_MODE]
for m
in modes
if m[SZ_SYSTEM_MODE] == SZ_AUTO_WITH_RESET]:
324 hass.services.async_register(DOMAIN, EvoService.RESET_SYSTEM, set_system_mode)
326 system_mode_schemas = []
327 modes = [m
for m
in modes
if m[SZ_SYSTEM_MODE] != SZ_AUTO_WITH_RESET]
330 perm_modes = [m[SZ_SYSTEM_MODE]
for m
in modes
if not m[SZ_CAN_BE_TEMPORARY]]
332 schema = vol.Schema({vol.Required(ATTR_SYSTEM_MODE): vol.In(perm_modes)})
333 system_mode_schemas.append(schema)
335 modes = [m
for m
in modes
if m[SZ_CAN_BE_TEMPORARY]]
338 temp_modes = [m[SZ_SYSTEM_MODE]
for m
in modes
if m[SZ_TIMING_MODE] ==
"Duration"]
342 vol.Required(ATTR_SYSTEM_MODE): vol.In(temp_modes),
343 vol.Optional(ATTR_DURATION_HOURS): vol.All(
349 system_mode_schemas.append(schema)
352 temp_modes = [m[SZ_SYSTEM_MODE]
for m
in modes
if m[SZ_TIMING_MODE] ==
"Period"]
356 vol.Required(ATTR_SYSTEM_MODE): vol.In(temp_modes),
357 vol.Optional(ATTR_DURATION_DAYS): vol.All(
363 system_mode_schemas.append(schema)
365 if system_mode_schemas:
366 hass.services.async_register(
368 EvoService.SET_SYSTEM_MODE,
370 schema=vol.Schema(vol.Any(*system_mode_schemas)),
374 hass.services.async_register(
376 EvoService.RESET_ZONE_OVERRIDE,
378 schema=RESET_ZONE_OVERRIDE_SCHEMA,
380 hass.services.async_register(
382 EvoService.SET_ZONE_OVERRIDE,
384 schema=SET_ZONE_OVERRIDE_SCHEMA,
None authenticate(self, str username, str password)
None _load_auth_tokens(self, str username)
None __init__(self, HomeAssistant hass)
None save_auth_tokens(self)
None handle_evo_exception(evo.RequestFailed err)
datetime dt_local_to_aware(datetime dt_naive)
datetime dt_aware_to_naive(datetime dt_aware)
bool async_setup(HomeAssistant hass, ConfigType config)
None setup_service_functions(HomeAssistant hass, EvoBroker broker)
aiohttp.ClientSession async_get_clientsession(HomeAssistant hass, bool verify_ssl=True, socket.AddressFamily family=socket.AF_UNSPEC, ssl_util.SSLCipherList ssl_cipher=ssl_util.SSLCipherList.PYTHON_DEFAULT)
None async_load(HomeAssistant hass)
None async_load_platform(core.HomeAssistant hass, Platform|str component, str platform, DiscoveryInfoType|None discovered, ConfigType hass_config)
None async_dispatcher_send(HomeAssistant hass, str signal, *Any args)
None async_save(self, _T data)