From fc50a59e81d9de5b3160679ac9fd8d0859532b1c Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Sun, 9 Jun 2024 23:12:44 +0100 Subject: [PATCH 01/27] functional programming tweak --- homeassistant/components/evohome/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index 782e4c4e67496..ac64d869c8569 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -121,11 +121,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Create a (EMEA/EU-based) Honeywell TCC system.""" - async def load_auth_tokens(store: Store) -> tuple[dict, dict | None]: + async def load_auth_tokens(store: Store, username: str) -> tuple[dict, dict | None]: app_storage = await store.async_load() tokens = dict(app_storage or {}) - if tokens.pop(CONF_USERNAME, None) != config[DOMAIN][CONF_USERNAME]: + if tokens.pop(CONF_USERNAME, None) != username: # any tokens won't be valid, and store might be corrupt await store.async_save({}) return ({}, {}) @@ -140,7 +140,7 @@ async def load_auth_tokens(store: Store) -> tuple[dict, dict | None]: return (tokens, user_data) store = Store[dict[str, Any]](hass, STORAGE_VER, STORAGE_KEY) - tokens, user_data = await load_auth_tokens(store) + tokens, user_data = await load_auth_tokens(store, config[DOMAIN][CONF_USERNAME]) client_v2 = evo.EvohomeClient( config[DOMAIN][CONF_USERNAME], From b85bf2ad073eddb6addd1a6b7463b1f34e5e59db Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Sun, 9 Jun 2024 23:13:35 +0100 Subject: [PATCH 02/27] doctweak --- homeassistant/components/evohome/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index ac64d869c8569..e6b14bd0c89c9 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -119,7 +119,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Create a (EMEA/EU-based) Honeywell TCC system.""" + """Set up the Evohome integration.""" async def load_auth_tokens(store: Store, username: str) -> tuple[dict, dict | None]: app_storage = await store.async_load() From 89dac224c22ef121ea67a17035af1ae5dd797bf5 Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Sun, 9 Jun 2024 23:14:28 +0100 Subject: [PATCH 03/27] typing hint --- homeassistant/components/evohome/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index e6b14bd0c89c9..c0af61c4ba95c 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -159,7 +159,7 @@ async def load_auth_tokens(store: Store, username: str) -> tuple[dict, dict | No assert isinstance(client_v2.installation_info, list) # mypy - loc_idx = config[DOMAIN][CONF_LOCATION_IDX] + loc_idx: int = config[DOMAIN][CONF_LOCATION_IDX] try: loc_config = client_v2.installation_info[loc_idx] except IndexError: From de503c3a12e9992fc69919c835c9a4a9253cc8f5 Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Sun, 9 Jun 2024 23:15:07 +0100 Subject: [PATCH 04/27] rename symbol --- homeassistant/components/evohome/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index c0af61c4ba95c..5886bfbf35c8b 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -183,11 +183,11 @@ async def load_auth_tokens(store: Store, username: str) -> tuple[dict, dict | No SZ_GATEWAY_ID: loc_config[GWS][0][SZ_GATEWAY_INFO][SZ_GATEWAY_ID], TCS: loc_config[GWS][0][TCS], } - _config = { + loc_config = { SZ_LOCATION_INFO: loc_info, GWS: [{SZ_GATEWAY_INFO: gwy_info, TCS: loc_config[GWS][0][TCS]}], } - _LOGGER.debug("Config = %s", _config) + _LOGGER.debug("Config = %s", loc_config) client_v1 = ev1.EvohomeClient( client_v2.username, From 01fef2dd07a95ea2a5b6b7b7ef256895bb13dc0d Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Sun, 9 Jun 2024 23:15:44 +0100 Subject: [PATCH 05/27] Switch to DataUpdateCoordinator --- homeassistant/components/evohome/__init__.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index 5886bfbf35c8b..0904d7199d1e4 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -50,10 +50,10 @@ async_dispatcher_send, ) from homeassistant.helpers.entity import Entity -from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.service import verify_domain_control from homeassistant.helpers.storage import Store from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator import homeassistant.util.dt as dt_util from .const import ( @@ -202,7 +202,17 @@ async def load_auth_tokens(store: Store, username: str) -> tuple[dict, dict | No ) await broker.save_auth_tokens() - await broker.async_update() # get initial state + + hass.data[DOMAIN]["coordinator"] = coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name=f"{DOMAIN}_coordinator", + update_interval=config[DOMAIN][CONF_SCAN_INTERVAL], + update_method=broker.async_update, + ) + # without a listener, _schedule_refresh() won't be invoked by _async_refresh() + coordinator.async_add_listener(lambda: None) + await coordinator.async_config_entry_first_refresh() # get initial state hass.async_create_task( async_load_platform(hass, Platform.CLIMATE, DOMAIN, {}, config) @@ -212,10 +222,6 @@ async def load_auth_tokens(store: Store, username: str) -> tuple[dict, dict | No async_load_platform(hass, Platform.WATER_HEATER, DOMAIN, {}, config) ) - async_track_time_interval( - hass, broker.async_update, config[DOMAIN][CONF_SCAN_INTERVAL] - ) - setup_service_functions(hass, broker) return True From 878a5bc04d8ff78df987adb733667f8f62594fad Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Mon, 10 Jun 2024 22:31:06 +0100 Subject: [PATCH 06/27] move from async_setup to EvoBroker --- homeassistant/components/evohome/__init__.py | 108 +-------- homeassistant/components/evohome/climate.py | 8 +- .../components/evohome/coordinator.py | 225 ++++++++++++++---- 3 files changed, 198 insertions(+), 143 deletions(-) diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index 0904d7199d1e4..7924b6500d1df 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -10,24 +10,16 @@ import logging from typing import Any, Final -import evohomeasync as ev1 -from evohomeasync.schema import SZ_SESSION_ID import evohomeasync2 as evo from evohomeasync2.schema.const import ( - SZ_ALLOWED_SYSTEM_MODES, SZ_AUTO_WITH_RESET, SZ_CAN_BE_TEMPORARY, - SZ_GATEWAY_ID, - SZ_GATEWAY_INFO, SZ_HEAT_SETPOINT, - SZ_LOCATION_ID, - SZ_LOCATION_INFO, SZ_SETPOINT_STATUS, SZ_STATE_STATUS, SZ_SYSTEM_MODE, SZ_SYSTEM_MODE_STATUS, SZ_TIME_UNTIL, - SZ_TIME_ZONE, SZ_TIMING_MODE, SZ_UNTIL, ) @@ -42,7 +34,6 @@ ) from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.helpers import entity_registry as er -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 ( @@ -51,13 +42,11 @@ ) from homeassistant.helpers.entity import Entity from homeassistant.helpers.service import verify_domain_control -from homeassistant.helpers.storage import Store from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator import homeassistant.util.dt as dt_util from .const import ( - ACCESS_TOKEN_EXPIRES, ATTR_DURATION_DAYS, ATTR_DURATION_HOURS, ATTR_DURATION_UNTIL, @@ -65,22 +54,12 @@ ATTR_ZONE_TEMP, CONF_LOCATION_IDX, DOMAIN, - GWS, SCAN_INTERVAL_DEFAULT, SCAN_INTERVAL_MINIMUM, - STORAGE_KEY, - STORAGE_VER, - TCS, - USER_DATA, EvoService, ) from .coordinator import EvoBroker -from .helpers import ( - convert_dict, - convert_until, - dt_aware_to_naive, - handle_evo_exception, -) +from .helpers import convert_dict, convert_until _LOGGER = logging.getLogger(__name__) @@ -121,87 +100,23 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Evohome integration.""" - async def load_auth_tokens(store: Store, username: str) -> tuple[dict, dict | None]: - app_storage = await store.async_load() - tokens = dict(app_storage or {}) - - if tokens.pop(CONF_USERNAME, None) != username: - # any tokens won't be valid, and store might be corrupt - await store.async_save({}) - return ({}, {}) - - # evohomeasync2 requires naive/local datetimes as strings - if tokens.get(ACCESS_TOKEN_EXPIRES) is not None and ( - expires := dt_util.parse_datetime(tokens[ACCESS_TOKEN_EXPIRES]) - ): - tokens[ACCESS_TOKEN_EXPIRES] = dt_aware_to_naive(expires) - - user_data = tokens.pop(USER_DATA, {}) - return (tokens, user_data) + broker = EvoBroker(hass) - store = Store[dict[str, Any]](hass, STORAGE_VER, STORAGE_KEY) - tokens, user_data = await load_auth_tokens(store, config[DOMAIN][CONF_USERNAME]) - - client_v2 = evo.EvohomeClient( + if not await broker.authenticate( config[DOMAIN][CONF_USERNAME], config[DOMAIN][CONF_PASSWORD], - **tokens, - session=async_get_clientsession(hass), - ) - - try: - await client_v2.login() - except evo.AuthenticationFailed as err: - handle_evo_exception(err) - return False - finally: - config[DOMAIN][CONF_PASSWORD] = "REDACTED" - - assert isinstance(client_v2.installation_info, list) # mypy - - loc_idx: int = config[DOMAIN][CONF_LOCATION_IDX] - try: - loc_config = client_v2.installation_info[loc_idx] - except IndexError: - _LOGGER.error( - ( - "Config error: '%s' = %s, but the valid range is 0-%s. " - "Unable to continue. Fix any configuration errors and restart HA" - ), - CONF_LOCATION_IDX, - loc_idx, - len(client_v2.installation_info) - 1, - ) + ): return False - if _LOGGER.isEnabledFor(logging.DEBUG): - loc_info = { - SZ_LOCATION_ID: loc_config[SZ_LOCATION_INFO][SZ_LOCATION_ID], - SZ_TIME_ZONE: loc_config[SZ_LOCATION_INFO][SZ_TIME_ZONE], - } - gwy_info = { - SZ_GATEWAY_ID: loc_config[GWS][0][SZ_GATEWAY_INFO][SZ_GATEWAY_ID], - TCS: loc_config[GWS][0][TCS], - } - loc_config = { - SZ_LOCATION_INFO: loc_info, - GWS: [{SZ_GATEWAY_INFO: gwy_info, TCS: loc_config[GWS][0][TCS]}], - } - _LOGGER.debug("Config = %s", loc_config) + config[DOMAIN][CONF_PASSWORD] = "REDACTED" - client_v1 = ev1.EvohomeClient( - client_v2.username, - client_v2.password, - session_id=user_data.get(SZ_SESSION_ID) if user_data else None, # STORAGE_VER 1 - session=async_get_clientsession(hass), - ) + if not broker.validate_location( + config[DOMAIN][CONF_LOCATION_IDX], + ): + return False hass.data[DOMAIN] = {} - hass.data[DOMAIN]["broker"] = broker = EvoBroker( - hass, client_v2, client_v1, store, config[DOMAIN] - ) - - await broker.save_auth_tokens() + hass.data[DOMAIN]["broker"] = broker hass.data[DOMAIN]["coordinator"] = coordinator = DataUpdateCoordinator( hass, @@ -210,6 +125,7 @@ async def load_auth_tokens(store: Store, username: str) -> tuple[dict, dict | No update_interval=config[DOMAIN][CONF_SCAN_INTERVAL], update_method=broker.async_update, ) + # without a listener, _schedule_refresh() won't be invoked by _async_refresh() coordinator.async_add_listener(lambda: None) await coordinator.async_config_entry_first_refresh() # get initial state @@ -278,7 +194,7 @@ async def set_zone_override(call: ServiceCall) -> None: hass.services.async_register(DOMAIN, EvoService.REFRESH_SYSTEM, force_refresh) # Enumerate which operating modes are supported by this system - modes = broker.config[SZ_ALLOWED_SYSTEM_MODES] + modes = broker.tcs.allowedSystemModes # Not all systems support "AutoWithReset": register this handler only if required if [m[SZ_SYSTEM_MODE] for m in modes if m[SZ_SYSTEM_MODE] == SZ_AUTO_WITH_RESET]: diff --git a/homeassistant/components/evohome/climate.py b/homeassistant/components/evohome/climate.py index 8b3e8a46e2c45..42ffe84121e0d 100644 --- a/homeassistant/components/evohome/climate.py +++ b/homeassistant/components/evohome/climate.py @@ -9,7 +9,6 @@ import evohomeasync2 as evo from evohomeasync2.schema.const import ( SZ_ACTIVE_FAULTS, - SZ_ALLOWED_SYSTEM_MODES, SZ_SETPOINT_STATUS, SZ_SYSTEM_ID, SZ_SYSTEM_MODE, @@ -44,7 +43,6 @@ ATTR_DURATION_UNTIL, ATTR_SYSTEM_MODE, ATTR_ZONE_TEMP, - CONF_LOCATION_IDX, DOMAIN, EVO_AUTO, EVO_AUTOECO, @@ -112,8 +110,8 @@ async def async_setup_platform( "Found the Location/Controller (%s), id=%s, name=%s (location_idx=%s)", broker.tcs.modelType, broker.tcs.systemId, - broker.tcs.location.name, - broker.params[CONF_LOCATION_IDX], + broker.loc.name, + broker.loc_idx, ) entities: list[EvoClimateEntity] = [EvoController(broker, broker.tcs)] @@ -367,7 +365,7 @@ def __init__(self, evo_broker: EvoBroker, evo_device: evo.ControlSystem) -> None self._attr_unique_id = evo_device.systemId self._attr_name = evo_device.location.name - modes = [m[SZ_SYSTEM_MODE] for m in evo_broker.config[SZ_ALLOWED_SYSTEM_MODES]] + modes = [m[SZ_SYSTEM_MODE] for m in evo_broker.tcs.allowedSystemModes] self._attr_preset_modes = [ TCS_PRESET_TO_HA[m] for m in modes if m in list(TCS_PRESET_TO_HA) ] diff --git a/homeassistant/components/evohome/coordinator.py b/homeassistant/components/evohome/coordinator.py index 6b54c5f464064..df7ab434ae5f2 100644 --- a/homeassistant/components/evohome/coordinator.py +++ b/homeassistant/components/evohome/coordinator.py @@ -10,13 +10,21 @@ import evohomeasync as ev1 from evohomeasync.schema import SZ_ID, SZ_SESSION_ID, SZ_TEMP import evohomeasync2 as evo +from evohomeasync2.schema.const import ( + SZ_GATEWAY_ID, + SZ_GATEWAY_INFO, + SZ_LOCATION_ID, + SZ_LOCATION_INFO, + SZ_TIME_ZONE, +) from homeassistant.const import CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_call_later from homeassistant.helpers.storage import Store -from homeassistant.helpers.typing import ConfigType +import homeassistant.util.dt as dt_util from .const import ( ACCESS_TOKEN, @@ -25,66 +33,199 @@ DOMAIN, GWS, REFRESH_TOKEN, + STORAGE_KEY, + STORAGE_VER, TCS, USER_DATA, UTC_OFFSET, ) -from .helpers import dt_local_to_aware, handle_evo_exception +from .helpers import dt_aware_to_naive, dt_local_to_aware, handle_evo_exception _LOGGER = logging.getLogger(__name__.rpartition(".")[0]) class EvoBroker: - """Container for evohome client and data.""" + """Broker for evohome client and data.""" + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize the evohome broker and its data structure.""" - def __init__( - self, - hass: HomeAssistant, - client: evo.EvohomeClient, - client_v1: ev1.EvohomeClient | None, - store: Store[dict[str, Any]], - params: ConfigType, - ) -> None: - """Initialize the evohome client and its data structure.""" self.hass = hass - self.client = client - self.client_v1 = client_v1 - self._store = store - self.params = params - loc_idx = params[CONF_LOCATION_IDX] - self._location: evo.Location = client.locations[loc_idx] + self._session = async_get_clientsession(hass) + self._store = Store[dict[str, Any]](hass, STORAGE_VER, STORAGE_KEY) + + # the main client, which uses the newer API + self.client_v2: evo.EvohomeClient = None # type: ignore[assignment] + self._tokens: dict[str, Any] = {} + + self.loc_idx: int = None # type: ignore[assignment] + self.loc: evo.Location = None # type: ignore[assignment] + + self.loc_utc_offset: timedelta = None # type: ignore[assignment] + self.tcs: evo.ControlSystem = None # type: ignore[assignment] - assert isinstance(client.installation_info, list) # mypy + # the older client can be used to obtain high-precision temps (only) + self.client_v1: ev1.EvohomeClient | None = None + self._session_id: str | None = None - self.config = client.installation_info[loc_idx][GWS][0][TCS][0] - self.tcs: evo.ControlSystem = self._location._gateways[0]._control_systems[0] # noqa: SLF001 - self.loc_utc_offset = timedelta(minutes=self._location.timeZone[UTC_OFFSET]) self.temps: dict[str, float | None] = {} - async def save_auth_tokens(self) -> None: - """Save access tokens and session IDs to the store for later use.""" + async def authenticate(self, username: str, password: str) -> bool: + """Check the user credentials against the web API.""" + + if ( + self.client_v2 is None + or username != self.client_v2.username + or password != self.client_v2.password + ): + await self._load_auth_tokens(username) # for self._tokens + + self.client_v2 = evo.EvohomeClient( + username, + password, + **self._tokens, + session=self._session, + ) + + else: # force a re-authentication + self.client_v2._user_account = None # noqa: SLF001 + + try: + await self.client_v2.login() + except evo.AuthenticationFailed as err: + handle_evo_exception(err) + return False + + await self._save_auth_tokens() + return True + + async def authenticate_v1(self) -> bool: + """Instantiate a client using the older API.""" + + await self._load_auth_tokens(self.client_v2.username) # for self._session_id + + self.client_v1 = ev1.EvohomeClient( + self.client_v2.username, + self.client_v2.password, + session_id=self._session_id, + session=self._session, + ) + + return True + + async def _load_auth_tokens(self, username: str) -> None: + """Load access tokens and session_id from the store and validate them. + + Sets self._tokens and self._session_id to the latest values. + + app_storage = None -> {} + app_storage = {} -> {} + app_storage = { + 'username': 'spotty.blackcat@gmail.com', + 'refresh_token': 'PlsqGsCd72_0BWl...', + 'access_token': 'W5y0jpNTKIqTmbA...', + 'access_token_expires': '2024-06-10T21:06:09+00:00' + } + """ + + app_storage: dict[str, Any] = dict(await self._store.async_load() or {}) + + if app_storage.pop(CONF_USERNAME, None) != username: + # any tokens won't be valid, and store might be corrupt + await self._store.async_save({}) + + self._session_id = None + self._tokens = {} + + return + + # evohomeasync2 requires naive/local datetimes as strings + if app_storage.get(ACCESS_TOKEN_EXPIRES) is not None and ( + expires := dt_util.parse_datetime(app_storage[ACCESS_TOKEN_EXPIRES]) + ): + app_storage[ACCESS_TOKEN_EXPIRES] = dt_aware_to_naive(expires) + + user_data: dict[str, str] = app_storage.pop(USER_DATA, {}) + + self._session_id = user_data.get(SZ_SESSION_ID) + self._tokens = app_storage + + async def _save_auth_tokens(self) -> None: + """Save access tokens and session_id to the store. + + Sets self._tokens and self._session_id to the latest values. + + { + "version": 1, + "minor_version": 1, + "key": "evohome", + "data": {} + } + """ + # evohomeasync2 uses naive/local datetimes access_token_expires = dt_local_to_aware( - self.client.access_token_expires # type: ignore[arg-type] + self.client_v2.access_token_expires # type: ignore[arg-type] ) - app_storage: dict[str, Any] = { - CONF_USERNAME: self.client.username, - REFRESH_TOKEN: self.client.refresh_token, - ACCESS_TOKEN: self.client.access_token, + self._tokens = { + CONF_USERNAME: self.client_v2.username, + REFRESH_TOKEN: self.client_v2.refresh_token, + ACCESS_TOKEN: self.client_v2.access_token, ACCESS_TOKEN_EXPIRES: access_token_expires.isoformat(), } + self._session_id = self.client_v1.broker.session_id if self.client_v1 else None + + app_storage = self._tokens if self.client_v1: - app_storage[USER_DATA] = { - SZ_SESSION_ID: self.client_v1.broker.session_id, - } # this is the schema for STORAGE_VER == 1 - else: - app_storage[USER_DATA] = {} + app_storage[USER_DATA] = {SZ_SESSION_ID: self._session_id} await self._store.async_save(app_storage) + def validate_location(self, loc_idx: int) -> bool: + """Get the default TCS of the specified location.""" + + self.loc_idx = loc_idx + + assert isinstance(self.client_v2.installation_info, list) # mypy + + try: + loc_config = self.client_v2.installation_info[loc_idx] + except IndexError: + _LOGGER.error( + ( + "Config error: '%s' = %s, but the valid range is 0-%s. " + "Unable to continue. Fix any configuration errors and restart HA" + ), + CONF_LOCATION_IDX, + loc_idx, + len(self.client_v2.installation_info) - 1, + ) + return False + + self.loc = self.client_v2.locations[loc_idx] + self.loc_utc_offset = timedelta(minutes=self.loc.timeZone[UTC_OFFSET]) + self.tcs = self.loc._gateways[0]._control_systems[0] # noqa: SLF001 + + if _LOGGER.isEnabledFor(logging.DEBUG): + loc_info = { + SZ_LOCATION_ID: loc_config[SZ_LOCATION_INFO][SZ_LOCATION_ID], + SZ_TIME_ZONE: loc_config[SZ_LOCATION_INFO][SZ_TIME_ZONE], + } + gwy_info = { + SZ_GATEWAY_ID: loc_config[GWS][0][SZ_GATEWAY_INFO][SZ_GATEWAY_ID], + TCS: loc_config[GWS][0][TCS], + } + config = { + SZ_LOCATION_INFO: loc_info, + GWS: [{SZ_GATEWAY_INFO: gwy_info, TCS: loc_config[GWS][0][TCS]}], + } + _LOGGER.debug("Config = %s", config) + + return True + async def call_client_api( self, client_api: Awaitable[dict[str, Any] | None], @@ -112,7 +253,7 @@ def get_session_id(client_v1: ev1.EvohomeClient) -> str | None: user_data = client_v1.user_data if client_v1 else None return user_data.get(SZ_SESSION_ID) if user_data else None # type: ignore[return-value] - session_id = get_session_id(self.client_v1) + old_session_id = self._session_id try: temps = await self.client_v1.get_temperatures() @@ -146,7 +287,7 @@ def get_session_id(client_v1: ev1.EvohomeClient) -> str | None: raise else: - if str(self.client_v1.location_id) != self._location.locationId: + if str(self.client_v1.location_id) != self.loc.locationId: _LOGGER.warning( "The v2 API's configured location doesn't match " "the v1 API's default location (there is more than one location), " @@ -157,26 +298,26 @@ def get_session_id(client_v1: ev1.EvohomeClient) -> str | None: self.temps = {str(i[SZ_ID]): i[SZ_TEMP] for i in temps} finally: - if self.client_v1 and session_id != self.client_v1.broker.session_id: - await self.save_auth_tokens() + if self.client_v1 and old_session_id != self.client_v1.broker.session_id: + await self._save_auth_tokens() _LOGGER.debug("Temperatures = %s", self.temps) async def _update_v2_api_state(self, *args: Any) -> None: """Get the latest modes, temperatures, setpoints of a Location.""" - access_token = self.client.access_token # maybe receive a new token? + access_token = self.client_v2.access_token # maybe receive a new token? try: - status = await self._location.refresh_status() + status = await self.loc.refresh_status() except evo.RequestFailed as err: handle_evo_exception(err) else: async_dispatcher_send(self.hass, DOMAIN) _LOGGER.debug("Status = %s", status) finally: - if access_token != self.client.access_token: - await self.save_auth_tokens() + if access_token != self.client_v2.access_token: + await self._save_auth_tokens() async def async_update(self, *args: Any) -> None: """Get the latest state data of an entire Honeywell TCC Location. From 43c1ea1c805e7614deed35058ee124c5717eaa45 Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Tue, 11 Jun 2024 21:45:11 +0100 Subject: [PATCH 07/27] tweaks - add v1 back in --- homeassistant/components/evohome/__init__.py | 2 ++ .../components/evohome/coordinator.py | 21 +++++++++++++------ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index 7924b6500d1df..7d7082efa409b 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -115,6 +115,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ): return False + await broker.authenticate_v1() + hass.data[DOMAIN] = {} hass.data[DOMAIN]["broker"] = broker diff --git a/homeassistant/components/evohome/coordinator.py b/homeassistant/components/evohome/coordinator.py index df7ab434ae5f2..d125a34196a59 100644 --- a/homeassistant/components/evohome/coordinator.py +++ b/homeassistant/components/evohome/coordinator.py @@ -120,13 +120,26 @@ async def _load_auth_tokens(self, username: str) -> None: Sets self._tokens and self._session_id to the latest values. app_storage = None -> {} - app_storage = {} -> {} + + app_storage = {} + app_storage = { - 'username': 'spotty.blackcat@gmail.com', + 'username': 'username@email.com', 'refresh_token': 'PlsqGsCd72_0BWl...', 'access_token': 'W5y0jpNTKIqTmbA...', 'access_token_expires': '2024-06-10T21:06:09+00:00' } + + app_storage = { + "username": "username@email.com", + "refresh_token": "jg68ZCKYdxEI3fF...", + "access_token": "1dc7z657UKzbhKA...", + "access_token_expires": "2024-06-10T22:05:54+00:00", + "user_data": { + "sessionId": "F7181186..." + }, + } + """ app_storage: dict[str, Any] = dict(await self._store.async_load() or {}) @@ -249,10 +262,6 @@ async def _update_v1_api_temps(self) -> None: assert self.client_v1 is not None # mypy check - def get_session_id(client_v1: ev1.EvohomeClient) -> str | None: - user_data = client_v1.user_data if client_v1 else None - return user_data.get(SZ_SESSION_ID) if user_data else None # type: ignore[return-value] - old_session_id = self._session_id try: From 80a17579119a5793a916bdf708f4ba17151815d3 Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Tue, 11 Jun 2024 22:53:42 +0100 Subject: [PATCH 08/27] tidy up --- homeassistant/components/evohome/__init__.py | 2 -- .../components/evohome/coordinator.py | 19 ++----------------- 2 files changed, 2 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index 7d7082efa409b..7924b6500d1df 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -115,8 +115,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ): return False - await broker.authenticate_v1() - hass.data[DOMAIN] = {} hass.data[DOMAIN]["broker"] = broker diff --git a/homeassistant/components/evohome/coordinator.py b/homeassistant/components/evohome/coordinator.py index d125a34196a59..b414c9e63cbf9 100644 --- a/homeassistant/components/evohome/coordinator.py +++ b/homeassistant/components/evohome/coordinator.py @@ -89,7 +89,7 @@ async def authenticate(self, username: str, password: str) -> bool: ) else: # force a re-authentication - self.client_v2._user_account = None # noqa: SLF001 + self.client_v2._user_account = {} # noqa: SLF001 try: await self.client_v2.login() @@ -97,14 +97,6 @@ async def authenticate(self, username: str, password: str) -> bool: handle_evo_exception(err) return False - await self._save_auth_tokens() - return True - - async def authenticate_v1(self) -> bool: - """Instantiate a client using the older API.""" - - await self._load_auth_tokens(self.client_v2.username) # for self._session_id - self.client_v1 = ev1.EvohomeClient( self.client_v2.username, self.client_v2.password, @@ -112,6 +104,7 @@ async def authenticate_v1(self) -> bool: session=self._session, ) + await self._save_auth_tokens() return True async def _load_auth_tokens(self, username: str) -> None: @@ -139,7 +132,6 @@ async def _load_auth_tokens(self, username: str) -> None: "sessionId": "F7181186..." }, } - """ app_storage: dict[str, Any] = dict(await self._store.async_load() or {}) @@ -168,13 +160,6 @@ async def _save_auth_tokens(self) -> None: """Save access tokens and session_id to the store. Sets self._tokens and self._session_id to the latest values. - - { - "version": 1, - "minor_version": 1, - "key": "evohome", - "data": {} - } """ # evohomeasync2 uses naive/local datetimes From f2bf2f3233558955b4b37f74b9c4e929bca186d6 Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Tue, 11 Jun 2024 23:13:42 +0100 Subject: [PATCH 09/27] tidy up docstring --- .../components/evohome/coordinator.py | 21 ------------------- 1 file changed, 21 deletions(-) diff --git a/homeassistant/components/evohome/coordinator.py b/homeassistant/components/evohome/coordinator.py index b414c9e63cbf9..648f937894177 100644 --- a/homeassistant/components/evohome/coordinator.py +++ b/homeassistant/components/evohome/coordinator.py @@ -111,27 +111,6 @@ async def _load_auth_tokens(self, username: str) -> None: """Load access tokens and session_id from the store and validate them. Sets self._tokens and self._session_id to the latest values. - - app_storage = None -> {} - - app_storage = {} - - app_storage = { - 'username': 'username@email.com', - 'refresh_token': 'PlsqGsCd72_0BWl...', - 'access_token': 'W5y0jpNTKIqTmbA...', - 'access_token_expires': '2024-06-10T21:06:09+00:00' - } - - app_storage = { - "username": "username@email.com", - "refresh_token": "jg68ZCKYdxEI3fF...", - "access_token": "1dc7z657UKzbhKA...", - "access_token_expires": "2024-06-10T22:05:54+00:00", - "user_data": { - "sessionId": "F7181186..." - }, - } """ app_storage: dict[str, Any] = dict(await self._store.async_load() or {}) From 586ac190129517452645fd72acbc8aa20ecc1b3c Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Tue, 11 Jun 2024 23:40:00 +0100 Subject: [PATCH 10/27] lint --- homeassistant/components/evohome/coordinator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/evohome/coordinator.py b/homeassistant/components/evohome/coordinator.py index 648f937894177..0f15aae80bb02 100644 --- a/homeassistant/components/evohome/coordinator.py +++ b/homeassistant/components/evohome/coordinator.py @@ -271,7 +271,7 @@ async def _update_v1_api_temps(self) -> None: self.temps = {str(i[SZ_ID]): i[SZ_TEMP] for i in temps} finally: - if self.client_v1 and old_session_id != self.client_v1.broker.session_id: + if self.client_v1 and self.client_v1.broker.session_id != old_session_id: await self._save_auth_tokens() _LOGGER.debug("Temperatures = %s", self.temps) From e06dbaaf130074d588fd72b90cdfa19c7242c6b4 Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Wed, 12 Jun 2024 15:09:46 +0100 Subject: [PATCH 11/27] remove redundant logging --- homeassistant/components/evohome/coordinator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/evohome/coordinator.py b/homeassistant/components/evohome/coordinator.py index 0f15aae80bb02..0f7f3533b5d13 100644 --- a/homeassistant/components/evohome/coordinator.py +++ b/homeassistant/components/evohome/coordinator.py @@ -197,7 +197,7 @@ def validate_location(self, loc_idx: int) -> bool: } config = { SZ_LOCATION_INFO: loc_info, - GWS: [{SZ_GATEWAY_INFO: gwy_info, TCS: loc_config[GWS][0][TCS]}], + GWS: [{SZ_GATEWAY_INFO: gwy_info}], } _LOGGER.debug("Config = %s", config) From 6bc809b61f6df3c4eda0a71d3ecafa31b37cc300 Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Wed, 12 Jun 2024 23:55:44 +0100 Subject: [PATCH 12/27] rename symbol --- .../components/evohome/coordinator.py | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/evohome/coordinator.py b/homeassistant/components/evohome/coordinator.py index 0f7f3533b5d13..66e279f06cfe5 100644 --- a/homeassistant/components/evohome/coordinator.py +++ b/homeassistant/components/evohome/coordinator.py @@ -56,7 +56,7 @@ def __init__(self, hass: HomeAssistant) -> None: self._store = Store[dict[str, Any]](hass, STORAGE_VER, STORAGE_KEY) # the main client, which uses the newer API - self.client_v2: evo.EvohomeClient = None # type: ignore[assignment] + self.client: evo.EvohomeClient = None # type: ignore[assignment] self._tokens: dict[str, Any] = {} self.loc_idx: int = None # type: ignore[assignment] @@ -75,13 +75,13 @@ async def authenticate(self, username: str, password: str) -> bool: """Check the user credentials against the web API.""" if ( - self.client_v2 is None - or username != self.client_v2.username - or password != self.client_v2.password + self.client is None + or username != self.client.username + or password != self.client.password ): await self._load_auth_tokens(username) # for self._tokens - self.client_v2 = evo.EvohomeClient( + self.client = evo.EvohomeClient( username, password, **self._tokens, @@ -89,17 +89,17 @@ async def authenticate(self, username: str, password: str) -> bool: ) else: # force a re-authentication - self.client_v2._user_account = {} # noqa: SLF001 + self.client._user_account = {} # noqa: SLF001 try: - await self.client_v2.login() + await self.client.login() except evo.AuthenticationFailed as err: handle_evo_exception(err) return False self.client_v1 = ev1.EvohomeClient( - self.client_v2.username, - self.client_v2.password, + self.client.username, + self.client.password, session_id=self._session_id, session=self._session, ) @@ -143,13 +143,13 @@ async def _save_auth_tokens(self) -> None: # evohomeasync2 uses naive/local datetimes access_token_expires = dt_local_to_aware( - self.client_v2.access_token_expires # type: ignore[arg-type] + self.client.access_token_expires # type: ignore[arg-type] ) self._tokens = { - CONF_USERNAME: self.client_v2.username, - REFRESH_TOKEN: self.client_v2.refresh_token, - ACCESS_TOKEN: self.client_v2.access_token, + CONF_USERNAME: self.client.username, + REFRESH_TOKEN: self.client.refresh_token, + ACCESS_TOKEN: self.client.access_token, ACCESS_TOKEN_EXPIRES: access_token_expires.isoformat(), } @@ -166,10 +166,10 @@ def validate_location(self, loc_idx: int) -> bool: self.loc_idx = loc_idx - assert isinstance(self.client_v2.installation_info, list) # mypy + assert isinstance(self.client.installation_info, list) # mypy try: - loc_config = self.client_v2.installation_info[loc_idx] + loc_config = self.client.installation_info[loc_idx] except IndexError: _LOGGER.error( ( @@ -178,11 +178,11 @@ def validate_location(self, loc_idx: int) -> bool: ), CONF_LOCATION_IDX, loc_idx, - len(self.client_v2.installation_info) - 1, + len(self.client.installation_info) - 1, ) return False - self.loc = self.client_v2.locations[loc_idx] + self.loc = self.client.locations[loc_idx] self.loc_utc_offset = timedelta(minutes=self.loc.timeZone[UTC_OFFSET]) self.tcs = self.loc._gateways[0]._control_systems[0] # noqa: SLF001 @@ -279,7 +279,7 @@ async def _update_v1_api_temps(self) -> None: async def _update_v2_api_state(self, *args: Any) -> None: """Get the latest modes, temperatures, setpoints of a Location.""" - access_token = self.client_v2.access_token # maybe receive a new token? + access_token = self.client.access_token # maybe receive a new token? try: status = await self.loc.refresh_status() @@ -289,7 +289,7 @@ async def _update_v2_api_state(self, *args: Any) -> None: async_dispatcher_send(self.hass, DOMAIN) _LOGGER.debug("Status = %s", status) finally: - if access_token != self.client_v2.access_token: + if access_token != self.client.access_token: await self._save_auth_tokens() async def async_update(self, *args: Any) -> None: From cf6fb71c661f5461b7d7d4950aee0de3aa548fca Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Wed, 19 Jun 2024 00:22:08 +0100 Subject: [PATCH 13/27] split back to inject authenticator clas --- homeassistant/components/evohome/__init__.py | 138 +++++++++++++++- .../components/evohome/coordinator.py | 148 +++--------------- 2 files changed, 154 insertions(+), 132 deletions(-) diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index 7924b6500d1df..743b1408a2b3a 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -10,6 +10,8 @@ import logging from typing import Any, Final +import evohomeasync as ev1 +from evohomeasync.schema import SZ_SESSION_ID import evohomeasync2 as evo from evohomeasync2.schema.const import ( SZ_AUTO_WITH_RESET, @@ -34,6 +36,7 @@ ) from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.helpers import entity_registry as er +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 ( @@ -42,11 +45,14 @@ ) from homeassistant.helpers.entity import Entity from homeassistant.helpers.service import verify_domain_control +from homeassistant.helpers.storage import Store from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator import homeassistant.util.dt as dt_util from .const import ( + ACCESS_TOKEN, + ACCESS_TOKEN_EXPIRES, ATTR_DURATION_DAYS, ATTR_DURATION_HOURS, ATTR_DURATION_UNTIL, @@ -54,12 +60,22 @@ ATTR_ZONE_TEMP, CONF_LOCATION_IDX, DOMAIN, + REFRESH_TOKEN, SCAN_INTERVAL_DEFAULT, SCAN_INTERVAL_MINIMUM, + STORAGE_KEY, + STORAGE_VER, + USER_DATA, EvoService, ) from .coordinator import EvoBroker -from .helpers import convert_dict, convert_until +from .helpers import ( + convert_dict, + convert_until, + dt_aware_to_naive, + dt_local_to_aware, + handle_evo_exception, +) _LOGGER = logging.getLogger(__name__) @@ -97,12 +113,126 @@ ) +class EvoClient: + """Class for evohome client instantiation & authentication.""" + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize the evohome broker and its data structure.""" + + self.hass = hass + + self._session = async_get_clientsession(hass) + self._store = Store[dict[str, Any]](hass, STORAGE_VER, STORAGE_KEY) + + # the main client, which uses the newer API + self.client_v2: evo.EvohomeClient | None = None + self._tokens: dict[str, Any] = {} + + # the older client can be used to obtain high-precision temps (only) + self.client_v1: ev1.EvohomeClient | None = None + self.session_id: str | None = None + + async def authenticate(self, username: str, password: str) -> bool: + """Check the user credentials against the web API.""" + + if self.client_v2 is None: + await self._load_auth_tokens(username) + + client_v2 = evo.EvohomeClient( + username, + password, + **self._tokens, + session=self._session, + ) + + else: # force a re-authentication + client_v2 = self.client_v2 + client_v2._user_account = {} # noqa: SLF001 + + try: + await client_v2.login() + except evo.AuthenticationFailed as err: + handle_evo_exception(err) + return False + + await self.save_auth_tokens() + + self.client_v2 = client_v2 + + self.client_v1 = ev1.EvohomeClient( + self.client_v2.username, + self.client_v2.password, + session_id=self.session_id, + session=self._session, + ) + + return True + + async def _load_auth_tokens(self, username: str) -> None: + """Load access tokens and session_id from the store and validate them. + + Sets self._tokens and self._session_id to the latest values. + """ + + app_storage: dict[str, Any] = dict(await self._store.async_load() or {}) + + if app_storage.pop(CONF_USERNAME, None) != username: + # any tokens won't be valid, and store might be corrupt + await self._store.async_save({}) + + self.session_id = None + self._tokens = {} + + return + + # evohomeasync2 requires naive/local datetimes as strings + if app_storage.get(ACCESS_TOKEN_EXPIRES) is not None and ( + expires := dt_util.parse_datetime(app_storage[ACCESS_TOKEN_EXPIRES]) + ): + app_storage[ACCESS_TOKEN_EXPIRES] = dt_aware_to_naive(expires) + + user_data: dict[str, str] = app_storage.pop(USER_DATA, {}) + + self.session_id = user_data.get(SZ_SESSION_ID) + self._tokens = app_storage + + async def save_auth_tokens(self) -> None: + """Save access tokens and session_id to the store. + + Sets self._tokens and self._session_id to the latest values. + """ + + if self.client_v2 is None: + await self._store.async_save({}) + return + + # evohomeasync2 uses naive/local datetimes + access_token_expires = dt_local_to_aware( + self.client_v2.access_token_expires # type: ignore[arg-type] + ) + + self._tokens = { + CONF_USERNAME: self.client_v2.username, + REFRESH_TOKEN: self.client_v2.refresh_token, + ACCESS_TOKEN: self.client_v2.access_token, + ACCESS_TOKEN_EXPIRES: access_token_expires.isoformat(), + } + + self.session_id = self.client_v1.broker.session_id if self.client_v1 else None + + app_storage = self._tokens + if self.client_v1: + app_storage[USER_DATA] = {SZ_SESSION_ID: self.session_id} + + await self._store.async_save(app_storage) + + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Evohome integration.""" - broker = EvoBroker(hass) + client = EvoClient(hass) - if not await broker.authenticate( + if not await client.authenticate( config[DOMAIN][CONF_USERNAME], config[DOMAIN][CONF_PASSWORD], ): @@ -110,6 +240,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: config[DOMAIN][CONF_PASSWORD] = "REDACTED" + broker = EvoBroker(client) + if not broker.validate_location( config[DOMAIN][CONF_LOCATION_IDX], ): diff --git a/homeassistant/components/evohome/coordinator.py b/homeassistant/components/evohome/coordinator.py index 66e279f06cfe5..c09f12c8e7b76 100644 --- a/homeassistant/components/evohome/coordinator.py +++ b/homeassistant/components/evohome/coordinator.py @@ -5,10 +5,10 @@ from collections.abc import Awaitable from datetime import timedelta import logging -from typing import Any +from typing import TYPE_CHECKING, Any import evohomeasync as ev1 -from evohomeasync.schema import SZ_ID, SZ_SESSION_ID, SZ_TEMP +from evohomeasync.schema import SZ_ID, SZ_TEMP import evohomeasync2 as evo from evohomeasync2.schema.const import ( SZ_GATEWAY_ID, @@ -18,46 +18,30 @@ SZ_TIME_ZONE, ) -from homeassistant.const import CONF_USERNAME -from homeassistant.core import HomeAssistant -from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_call_later -from homeassistant.helpers.storage import Store -import homeassistant.util.dt as dt_util - -from .const import ( - ACCESS_TOKEN, - ACCESS_TOKEN_EXPIRES, - CONF_LOCATION_IDX, - DOMAIN, - GWS, - REFRESH_TOKEN, - STORAGE_KEY, - STORAGE_VER, - TCS, - USER_DATA, - UTC_OFFSET, -) -from .helpers import dt_aware_to_naive, dt_local_to_aware, handle_evo_exception + +from .const import CONF_LOCATION_IDX, DOMAIN, GWS, TCS, UTC_OFFSET +from .helpers import handle_evo_exception + +if TYPE_CHECKING: + from . import EvoClient _LOGGER = logging.getLogger(__name__.rpartition(".")[0]) class EvoBroker: - """Broker for evohome client and data.""" + """Broker for evohome client broker.""" - def __init__(self, hass: HomeAssistant) -> None: + def __init__(self, client: EvoClient) -> None: """Initialize the evohome broker and its data structure.""" - self.hass = hass + self._client = client - self._session = async_get_clientsession(hass) - self._store = Store[dict[str, Any]](hass, STORAGE_VER, STORAGE_KEY) + assert isinstance(client.client_v2, evo.EvohomeClient) # mypy - # the main client, which uses the newer API - self.client: evo.EvohomeClient = None # type: ignore[assignment] - self._tokens: dict[str, Any] = {} + self.client = client.client_v2 + self.client_v1 = client.client_v1 self.loc_idx: int = None # type: ignore[assignment] self.loc: evo.Location = None # type: ignore[assignment] @@ -65,102 +49,8 @@ def __init__(self, hass: HomeAssistant) -> None: self.loc_utc_offset: timedelta = None # type: ignore[assignment] self.tcs: evo.ControlSystem = None # type: ignore[assignment] - # the older client can be used to obtain high-precision temps (only) - self.client_v1: ev1.EvohomeClient | None = None - self._session_id: str | None = None - self.temps: dict[str, float | None] = {} - async def authenticate(self, username: str, password: str) -> bool: - """Check the user credentials against the web API.""" - - if ( - self.client is None - or username != self.client.username - or password != self.client.password - ): - await self._load_auth_tokens(username) # for self._tokens - - self.client = evo.EvohomeClient( - username, - password, - **self._tokens, - session=self._session, - ) - - else: # force a re-authentication - self.client._user_account = {} # noqa: SLF001 - - try: - await self.client.login() - except evo.AuthenticationFailed as err: - handle_evo_exception(err) - return False - - self.client_v1 = ev1.EvohomeClient( - self.client.username, - self.client.password, - session_id=self._session_id, - session=self._session, - ) - - await self._save_auth_tokens() - return True - - async def _load_auth_tokens(self, username: str) -> None: - """Load access tokens and session_id from the store and validate them. - - Sets self._tokens and self._session_id to the latest values. - """ - - app_storage: dict[str, Any] = dict(await self._store.async_load() or {}) - - if app_storage.pop(CONF_USERNAME, None) != username: - # any tokens won't be valid, and store might be corrupt - await self._store.async_save({}) - - self._session_id = None - self._tokens = {} - - return - - # evohomeasync2 requires naive/local datetimes as strings - if app_storage.get(ACCESS_TOKEN_EXPIRES) is not None and ( - expires := dt_util.parse_datetime(app_storage[ACCESS_TOKEN_EXPIRES]) - ): - app_storage[ACCESS_TOKEN_EXPIRES] = dt_aware_to_naive(expires) - - user_data: dict[str, str] = app_storage.pop(USER_DATA, {}) - - self._session_id = user_data.get(SZ_SESSION_ID) - self._tokens = app_storage - - async def _save_auth_tokens(self) -> None: - """Save access tokens and session_id to the store. - - Sets self._tokens and self._session_id to the latest values. - """ - - # evohomeasync2 uses naive/local datetimes - access_token_expires = dt_local_to_aware( - self.client.access_token_expires # type: ignore[arg-type] - ) - - self._tokens = { - CONF_USERNAME: self.client.username, - REFRESH_TOKEN: self.client.refresh_token, - ACCESS_TOKEN: self.client.access_token, - ACCESS_TOKEN_EXPIRES: access_token_expires.isoformat(), - } - - self._session_id = self.client_v1.broker.session_id if self.client_v1 else None - - app_storage = self._tokens - if self.client_v1: - app_storage[USER_DATA] = {SZ_SESSION_ID: self._session_id} - - await self._store.async_save(app_storage) - def validate_location(self, loc_idx: int) -> bool: """Get the default TCS of the specified location.""" @@ -217,7 +107,7 @@ async def call_client_api( return None if update_state: # wait a moment for system to quiesce before updating state - async_call_later(self.hass, 1, self._update_v2_api_state) + async_call_later(self._client.hass, 1, self._update_v2_api_state) return result @@ -226,7 +116,7 @@ async def _update_v1_api_temps(self) -> None: assert self.client_v1 is not None # mypy check - old_session_id = self._session_id + old_session_id = self._client.session_id try: temps = await self.client_v1.get_temperatures() @@ -272,7 +162,7 @@ async def _update_v1_api_temps(self) -> None: finally: if self.client_v1 and self.client_v1.broker.session_id != old_session_id: - await self._save_auth_tokens() + await self._client.save_auth_tokens() _LOGGER.debug("Temperatures = %s", self.temps) @@ -286,11 +176,11 @@ async def _update_v2_api_state(self, *args: Any) -> None: except evo.RequestFailed as err: handle_evo_exception(err) else: - async_dispatcher_send(self.hass, DOMAIN) + async_dispatcher_send(self._client.hass, DOMAIN) _LOGGER.debug("Status = %s", status) finally: if access_token != self.client.access_token: - await self._save_auth_tokens() + await self._client.save_auth_tokens() async def async_update(self, *args: Any) -> None: """Get the latest state data of an entire Honeywell TCC Location. From 5ee58b13481d0a21812df52246f9b1caa690a750 Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Wed, 19 Jun 2024 00:28:45 +0100 Subject: [PATCH 14/27] rename symbols --- homeassistant/components/evohome/__init__.py | 8 +++---- .../components/evohome/coordinator.py | 22 +++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index 743b1408a2b3a..b862dfd6e764f 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -113,7 +113,7 @@ ) -class EvoClient: +class EvoAuthenticator: """Class for evohome client instantiation & authentication.""" def __init__(self, hass: HomeAssistant) -> None: @@ -230,9 +230,9 @@ async def save_auth_tokens(self) -> None: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Evohome integration.""" - client = EvoClient(hass) + auth = EvoAuthenticator(hass) - if not await client.authenticate( + if not await auth.authenticate( config[DOMAIN][CONF_USERNAME], config[DOMAIN][CONF_PASSWORD], ): @@ -240,7 +240,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: config[DOMAIN][CONF_PASSWORD] = "REDACTED" - broker = EvoBroker(client) + broker = EvoBroker(auth) if not broker.validate_location( config[DOMAIN][CONF_LOCATION_IDX], diff --git a/homeassistant/components/evohome/coordinator.py b/homeassistant/components/evohome/coordinator.py index c09f12c8e7b76..21df478bcb260 100644 --- a/homeassistant/components/evohome/coordinator.py +++ b/homeassistant/components/evohome/coordinator.py @@ -25,7 +25,7 @@ from .helpers import handle_evo_exception if TYPE_CHECKING: - from . import EvoClient + from . import EvoAuthenticator _LOGGER = logging.getLogger(__name__.rpartition(".")[0]) @@ -33,15 +33,15 @@ class EvoBroker: """Broker for evohome client broker.""" - def __init__(self, client: EvoClient) -> None: + def __init__(self, auth: EvoAuthenticator) -> None: """Initialize the evohome broker and its data structure.""" - self._client = client + self._auth = auth - assert isinstance(client.client_v2, evo.EvohomeClient) # mypy + assert isinstance(auth.client_v2, evo.EvohomeClient) # mypy - self.client = client.client_v2 - self.client_v1 = client.client_v1 + self.client = auth.client_v2 + self.client_v1 = auth.client_v1 self.loc_idx: int = None # type: ignore[assignment] self.loc: evo.Location = None # type: ignore[assignment] @@ -107,7 +107,7 @@ async def call_client_api( return None if update_state: # wait a moment for system to quiesce before updating state - async_call_later(self._client.hass, 1, self._update_v2_api_state) + async_call_later(self._auth.hass, 1, self._update_v2_api_state) return result @@ -116,7 +116,7 @@ async def _update_v1_api_temps(self) -> None: assert self.client_v1 is not None # mypy check - old_session_id = self._client.session_id + old_session_id = self._auth.session_id try: temps = await self.client_v1.get_temperatures() @@ -162,7 +162,7 @@ async def _update_v1_api_temps(self) -> None: finally: if self.client_v1 and self.client_v1.broker.session_id != old_session_id: - await self._client.save_auth_tokens() + await self._auth.save_auth_tokens() _LOGGER.debug("Temperatures = %s", self.temps) @@ -176,11 +176,11 @@ async def _update_v2_api_state(self, *args: Any) -> None: except evo.RequestFailed as err: handle_evo_exception(err) else: - async_dispatcher_send(self._client.hass, DOMAIN) + async_dispatcher_send(self._auth.hass, DOMAIN) _LOGGER.debug("Status = %s", status) finally: if access_token != self.client.access_token: - await self._client.save_auth_tokens() + await self._auth.save_auth_tokens() async def async_update(self, *args: Any) -> None: """Get the latest state data of an entire Honeywell TCC Location. From 374809960ecfb41d227e844febccdae5b3c264e8 Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Wed, 19 Jun 2024 07:33:40 +0100 Subject: [PATCH 15/27] rename symbol --- homeassistant/components/evohome/__init__.py | 8 ++--- .../components/evohome/coordinator.py | 30 +++++++++---------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index b862dfd6e764f..01ac52645337f 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -113,7 +113,7 @@ ) -class EvoAuthenticator: +class EvoSession: """Class for evohome client instantiation & authentication.""" def __init__(self, hass: HomeAssistant) -> None: @@ -230,9 +230,9 @@ async def save_auth_tokens(self) -> None: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Evohome integration.""" - auth = EvoAuthenticator(hass) + sess = EvoSession(hass) - if not await auth.authenticate( + if not await sess.authenticate( config[DOMAIN][CONF_USERNAME], config[DOMAIN][CONF_PASSWORD], ): @@ -240,7 +240,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: config[DOMAIN][CONF_PASSWORD] = "REDACTED" - broker = EvoBroker(auth) + broker = EvoBroker(sess) if not broker.validate_location( config[DOMAIN][CONF_LOCATION_IDX], diff --git a/homeassistant/components/evohome/coordinator.py b/homeassistant/components/evohome/coordinator.py index 21df478bcb260..08baeb1cd7262 100644 --- a/homeassistant/components/evohome/coordinator.py +++ b/homeassistant/components/evohome/coordinator.py @@ -16,6 +16,7 @@ SZ_LOCATION_ID, SZ_LOCATION_INFO, SZ_TIME_ZONE, + SZ_USE_DAYLIGHT_SAVE_SWITCHING, ) from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -25,7 +26,7 @@ from .helpers import handle_evo_exception if TYPE_CHECKING: - from . import EvoAuthenticator + from . import EvoSession _LOGGER = logging.getLogger(__name__.rpartition(".")[0]) @@ -33,15 +34,15 @@ class EvoBroker: """Broker for evohome client broker.""" - def __init__(self, auth: EvoAuthenticator) -> None: + def __init__(self, sess: EvoSession) -> None: """Initialize the evohome broker and its data structure.""" - self._auth = auth + self._sess = sess - assert isinstance(auth.client_v2, evo.EvohomeClient) # mypy + assert isinstance(sess.client_v2, evo.EvohomeClient) # mypy - self.client = auth.client_v2 - self.client_v1 = auth.client_v1 + self.client = sess.client_v2 + self.client_v1 = sess.client_v1 self.loc_idx: int = None # type: ignore[assignment] self.loc: evo.Location = None # type: ignore[assignment] @@ -78,16 +79,15 @@ def validate_location(self, loc_idx: int) -> bool: if _LOGGER.isEnabledFor(logging.DEBUG): loc_info = { - SZ_LOCATION_ID: loc_config[SZ_LOCATION_INFO][SZ_LOCATION_ID], - SZ_TIME_ZONE: loc_config[SZ_LOCATION_INFO][SZ_TIME_ZONE], + k: loc_config[SZ_LOCATION_INFO][k] + for k in (SZ_LOCATION_ID, SZ_USE_DAYLIGHT_SAVE_SWITCHING, SZ_TIME_ZONE) } gwy_info = { SZ_GATEWAY_ID: loc_config[GWS][0][SZ_GATEWAY_INFO][SZ_GATEWAY_ID], - TCS: loc_config[GWS][0][TCS], } config = { SZ_LOCATION_INFO: loc_info, - GWS: [{SZ_GATEWAY_INFO: gwy_info}], + GWS: [{SZ_GATEWAY_INFO: gwy_info, TCS: loc_config[GWS][0][TCS]}], } _LOGGER.debug("Config = %s", config) @@ -107,7 +107,7 @@ async def call_client_api( return None if update_state: # wait a moment for system to quiesce before updating state - async_call_later(self._auth.hass, 1, self._update_v2_api_state) + async_call_later(self._sess.hass, 1, self._update_v2_api_state) return result @@ -116,7 +116,7 @@ async def _update_v1_api_temps(self) -> None: assert self.client_v1 is not None # mypy check - old_session_id = self._auth.session_id + old_session_id = self._sess.session_id try: temps = await self.client_v1.get_temperatures() @@ -162,7 +162,7 @@ async def _update_v1_api_temps(self) -> None: finally: if self.client_v1 and self.client_v1.broker.session_id != old_session_id: - await self._auth.save_auth_tokens() + await self._sess.save_auth_tokens() _LOGGER.debug("Temperatures = %s", self.temps) @@ -176,11 +176,11 @@ async def _update_v2_api_state(self, *args: Any) -> None: except evo.RequestFailed as err: handle_evo_exception(err) else: - async_dispatcher_send(self._auth.hass, DOMAIN) + async_dispatcher_send(self._sess.hass, DOMAIN) _LOGGER.debug("Status = %s", status) finally: if access_token != self.client.access_token: - await self._auth.save_auth_tokens() + await self._sess.save_auth_tokens() async def async_update(self, *args: Any) -> None: """Get the latest state data of an entire Honeywell TCC Location. From 2a7e420dc2544e1cd2dbc8874f1e9bb2eb78ca71 Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Wed, 19 Jun 2024 07:40:00 +0100 Subject: [PATCH 16/27] Update homeassistant/components/evohome/__init__.py Co-authored-by: Joakim Plate --- homeassistant/components/evohome/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index b862dfd6e764f..bc5517794fc32 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -160,8 +160,8 @@ async def authenticate(self, username: str, password: str) -> bool: self.client_v2 = client_v2 self.client_v1 = ev1.EvohomeClient( - self.client_v2.username, - self.client_v2.password, + username, + password, session_id=self.session_id, session=self._session, ) From f87f3c90e62040375bc8a07806dff2e4e7496ab5 Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Wed, 19 Jun 2024 08:00:38 +0100 Subject: [PATCH 17/27] allow exception to pass through --- homeassistant/components/evohome/__init__.py | 31 ++++++++++---------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index aaaf537f04acc..a3dd27fb62460 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -132,8 +132,11 @@ def __init__(self, hass: HomeAssistant) -> None: self.client_v1: ev1.EvohomeClient | None = None self.session_id: str | None = None - async def authenticate(self, username: str, password: str) -> bool: - """Check the user credentials against the web API.""" + async def authenticate(self, username: str, password: str) -> None: + """Check the user credentials against the web API. + + Will raise evo.AuthenticationFailed if the credentials are invalid. + """ if self.client_v2 is None: await self._load_auth_tokens(username) @@ -149,12 +152,7 @@ async def authenticate(self, username: str, password: str) -> bool: client_v2 = self.client_v2 client_v2._user_account = {} # noqa: SLF001 - try: - await client_v2.login() - except evo.AuthenticationFailed as err: - handle_evo_exception(err) - return False - + await client_v2.login() await self.save_auth_tokens() self.client_v2 = client_v2 @@ -166,8 +164,6 @@ async def authenticate(self, username: str, password: str) -> bool: session=self._session, ) - return True - async def _load_auth_tokens(self, username: str) -> None: """Load access tokens and session_id from the store and validate them. @@ -232,13 +228,18 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: sess = EvoSession(hass) - if not await sess.authenticate( - config[DOMAIN][CONF_USERNAME], - config[DOMAIN][CONF_PASSWORD], - ): + try: + await sess.authenticate( + config[DOMAIN][CONF_USERNAME], + config[DOMAIN][CONF_PASSWORD], + ) + + except evo.AuthenticationFailed as err: + handle_evo_exception(err) return False - config[DOMAIN][CONF_PASSWORD] = "REDACTED" + finally: + config[DOMAIN][CONF_PASSWORD] = "REDACTED" broker = EvoBroker(sess) From 03b7aa269cb33b529aee5536ddf378d9337a870f Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Wed, 19 Jun 2024 08:14:00 +0100 Subject: [PATCH 18/27] allow re-authentication with diff credentials --- homeassistant/components/evohome/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index a3dd27fb62460..2895973145e5d 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -138,7 +138,11 @@ async def authenticate(self, username: str, password: str) -> None: Will raise evo.AuthenticationFailed if the credentials are invalid. """ - if self.client_v2 is None: + if ( + self.client_v2 is None + or username != self.client_v2.username + or password != self.client_v2.password + ): await self._load_auth_tokens(username) client_v2 = evo.EvohomeClient( From c0ab46faec7e7b5ce1aec0d87b1f297f6a3acdc9 Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Wed, 19 Jun 2024 08:33:27 +0100 Subject: [PATCH 19/27] lint --- homeassistant/components/evohome/coordinator.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/evohome/coordinator.py b/homeassistant/components/evohome/coordinator.py index 08baeb1cd7262..07309eefd4575 100644 --- a/homeassistant/components/evohome/coordinator.py +++ b/homeassistant/components/evohome/coordinator.py @@ -38,6 +38,7 @@ def __init__(self, sess: EvoSession) -> None: """Initialize the evohome broker and its data structure.""" self._sess = sess + self.hass = sess.hass assert isinstance(sess.client_v2, evo.EvohomeClient) # mypy @@ -107,7 +108,7 @@ async def call_client_api( return None if update_state: # wait a moment for system to quiesce before updating state - async_call_later(self._sess.hass, 1, self._update_v2_api_state) + async_call_later(self.hass, 1, self._update_v2_api_state) return result @@ -176,7 +177,7 @@ async def _update_v2_api_state(self, *args: Any) -> None: except evo.RequestFailed as err: handle_evo_exception(err) else: - async_dispatcher_send(self._sess.hass, DOMAIN) + async_dispatcher_send(self.hass, DOMAIN) _LOGGER.debug("Status = %s", status) finally: if access_token != self.client.access_token: From bb635d4a9b3fd700091d06e707db90de94b6d0b8 Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Wed, 19 Jun 2024 08:42:31 +0100 Subject: [PATCH 20/27] undo unrelated change --- homeassistant/components/evohome/coordinator.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/evohome/coordinator.py b/homeassistant/components/evohome/coordinator.py index 07309eefd4575..222f51f877253 100644 --- a/homeassistant/components/evohome/coordinator.py +++ b/homeassistant/components/evohome/coordinator.py @@ -16,7 +16,6 @@ SZ_LOCATION_ID, SZ_LOCATION_INFO, SZ_TIME_ZONE, - SZ_USE_DAYLIGHT_SAVE_SWITCHING, ) from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -80,15 +79,16 @@ def validate_location(self, loc_idx: int) -> bool: if _LOGGER.isEnabledFor(logging.DEBUG): loc_info = { - k: loc_config[SZ_LOCATION_INFO][k] - for k in (SZ_LOCATION_ID, SZ_USE_DAYLIGHT_SAVE_SWITCHING, SZ_TIME_ZONE) + SZ_LOCATION_ID: loc_config[SZ_LOCATION_INFO][SZ_LOCATION_ID], + SZ_TIME_ZONE: loc_config[SZ_LOCATION_INFO][SZ_TIME_ZONE], } gwy_info = { SZ_GATEWAY_ID: loc_config[GWS][0][SZ_GATEWAY_INFO][SZ_GATEWAY_ID], + TCS: loc_config[GWS][0][TCS], } config = { SZ_LOCATION_INFO: loc_info, - GWS: [{SZ_GATEWAY_INFO: gwy_info, TCS: loc_config[GWS][0][TCS]}], + GWS: [{SZ_GATEWAY_INFO: gwy_info}], } _LOGGER.debug("Config = %s", config) From 87bb404560719b22a9dcf383af56a586b41d47e0 Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Thu, 20 Jun 2024 17:27:01 +0100 Subject: [PATCH 21/27] use async_refresh instead of async_config_entry_first_refresh --- homeassistant/components/evohome/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index 2895973145e5d..c4dfdb65f3a9a 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -265,7 +265,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # without a listener, _schedule_refresh() won't be invoked by _async_refresh() coordinator.async_add_listener(lambda: None) - await coordinator.async_config_entry_first_refresh() # get initial state + await coordinator.async_refresh() # get initial state hass.async_create_task( async_load_platform(hass, Platform.CLIMATE, DOMAIN, {}, config) From f4713d402ad4d3b9dc667667750f7c76ea888b0a Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Mon, 1 Jul 2024 18:04:15 +0100 Subject: [PATCH 22/27] assign None instead of empty dict as Falsey value --- homeassistant/components/evohome/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index c4dfdb65f3a9a..4e63a2fd8b9cf 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -154,7 +154,7 @@ async def authenticate(self, username: str, password: str) -> None: else: # force a re-authentication client_v2 = self.client_v2 - client_v2._user_account = {} # noqa: SLF001 + client_v2._user_account = None # noqa: SLF001 await client_v2.login() await self.save_auth_tokens() From 1d7ee48ee31578a002d4129f8ebfdeec7e515356 Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Tue, 2 Jul 2024 21:51:59 +0100 Subject: [PATCH 23/27] use class attrs instead of type hints --- homeassistant/components/evohome/coordinator.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/evohome/coordinator.py b/homeassistant/components/evohome/coordinator.py index 222f51f877253..29eb07cb2290c 100644 --- a/homeassistant/components/evohome/coordinator.py +++ b/homeassistant/components/evohome/coordinator.py @@ -33,6 +33,11 @@ class EvoBroker: """Broker for evohome client broker.""" + loc_idx: int + loc: evo.Location + loc_utc_offset: timedelta + tcs: evo.ControlSystem + def __init__(self, sess: EvoSession) -> None: """Initialize the evohome broker and its data structure.""" @@ -44,12 +49,6 @@ def __init__(self, sess: EvoSession) -> None: self.client = sess.client_v2 self.client_v1 = sess.client_v1 - self.loc_idx: int = None # type: ignore[assignment] - self.loc: evo.Location = None # type: ignore[assignment] - - self.loc_utc_offset: timedelta = None # type: ignore[assignment] - self.tcs: evo.ControlSystem = None # type: ignore[assignment] - self.temps: dict[str, float | None] = {} def validate_location(self, loc_idx: int) -> bool: From 93e66aa1a3433e1a190c4a4b5d001dc397ec0c0d Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Mon, 22 Jul 2024 09:01:30 +0100 Subject: [PATCH 24/27] speed up mypy hint --- homeassistant/components/evohome/coordinator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/evohome/coordinator.py b/homeassistant/components/evohome/coordinator.py index 29eb07cb2290c..d51c531bddc70 100644 --- a/homeassistant/components/evohome/coordinator.py +++ b/homeassistant/components/evohome/coordinator.py @@ -56,7 +56,7 @@ def validate_location(self, loc_idx: int) -> bool: self.loc_idx = loc_idx - assert isinstance(self.client.installation_info, list) # mypy + assert self.client.installation_info is not None # mypy try: loc_config = self.client.installation_info[loc_idx] From 2f0b584a95eb4692a496e2aaddb957dee7d436d9 Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Mon, 22 Jul 2024 09:03:56 +0100 Subject: [PATCH 25/27] speed up mypy check --- homeassistant/components/evohome/coordinator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/evohome/coordinator.py b/homeassistant/components/evohome/coordinator.py index d51c531bddc70..b83d2d20c6a66 100644 --- a/homeassistant/components/evohome/coordinator.py +++ b/homeassistant/components/evohome/coordinator.py @@ -44,7 +44,7 @@ def __init__(self, sess: EvoSession) -> None: self._sess = sess self.hass = sess.hass - assert isinstance(sess.client_v2, evo.EvohomeClient) # mypy + assert sess.client_v2 is not None # mypy self.client = sess.client_v2 self.client_v1 = sess.client_v1 From e18e4bf4949ca7973e3fb445080cd157e118cbad Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Mon, 22 Jul 2024 09:16:56 +0100 Subject: [PATCH 26/27] small tidy up --- homeassistant/components/evohome/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index 4e63a2fd8b9cf..270693002ae66 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -252,8 +252,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ): return False - hass.data[DOMAIN] = {} - hass.data[DOMAIN]["broker"] = broker + hass.data[DOMAIN] = {"broker": broker} hass.data[DOMAIN]["coordinator"] = coordinator = DataUpdateCoordinator( hass, From ca351df471e3028bde2469244cb07f92f6440ea1 Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Mon, 22 Jul 2024 09:37:42 +0100 Subject: [PATCH 27/27] small tidy up --- homeassistant/components/evohome/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index 270693002ae66..4cf8561fc3b2b 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -252,9 +252,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ): return False - hass.data[DOMAIN] = {"broker": broker} - - hass.data[DOMAIN]["coordinator"] = coordinator = DataUpdateCoordinator( + coordinator = DataUpdateCoordinator( hass, _LOGGER, name=f"{DOMAIN}_coordinator", @@ -262,6 +260,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: update_method=broker.async_update, ) + hass.data[DOMAIN] = {"broker": broker, "coordinator": coordinator} + # without a listener, _schedule_refresh() won't be invoked by _async_refresh() coordinator.async_add_listener(lambda: None) await coordinator.async_refresh() # get initial state