mirror of
https://github.com/home-assistant/core.git
synced 2025-07-22 12:47:08 +00:00
Add coordinator to evohome and prune async_update code (#119432)
* functional programming tweak * doctweak * typing hint * rename symbol * Switch to DataUpdateCoordinator * move from async_setup to EvoBroker * tweaks - add v1 back in * tidy up * tidy up docstring * lint * remove redundant logging * rename symbol * split back to inject authenticator clas * rename symbols * rename symbol * Update homeassistant/components/evohome/__init__.py Co-authored-by: Joakim Plate <elupus@ecce.se> * allow exception to pass through * allow re-authentication with diff credentials * lint * undo unrelated change * use async_refresh instead of async_config_entry_first_refresh * assign None instead of empty dict as Falsey value * use class attrs instead of type hints * speed up mypy hint * speed up mypy check * small tidy up * small tidy up --------- Co-authored-by: Joakim Plate <elupus@ecce.se> Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
This commit is contained in:
parent
da6a7ebd42
commit
42b9c0448c
@ -14,20 +14,14 @@ import evohomeasync as ev1
|
|||||||
from evohomeasync.schema import SZ_SESSION_ID
|
from evohomeasync.schema import SZ_SESSION_ID
|
||||||
import evohomeasync2 as evo
|
import evohomeasync2 as evo
|
||||||
from evohomeasync2.schema.const import (
|
from evohomeasync2.schema.const import (
|
||||||
SZ_ALLOWED_SYSTEM_MODES,
|
|
||||||
SZ_AUTO_WITH_RESET,
|
SZ_AUTO_WITH_RESET,
|
||||||
SZ_CAN_BE_TEMPORARY,
|
SZ_CAN_BE_TEMPORARY,
|
||||||
SZ_GATEWAY_ID,
|
|
||||||
SZ_GATEWAY_INFO,
|
|
||||||
SZ_HEAT_SETPOINT,
|
SZ_HEAT_SETPOINT,
|
||||||
SZ_LOCATION_ID,
|
|
||||||
SZ_LOCATION_INFO,
|
|
||||||
SZ_SETPOINT_STATUS,
|
SZ_SETPOINT_STATUS,
|
||||||
SZ_STATE_STATUS,
|
SZ_STATE_STATUS,
|
||||||
SZ_SYSTEM_MODE,
|
SZ_SYSTEM_MODE,
|
||||||
SZ_SYSTEM_MODE_STATUS,
|
SZ_SYSTEM_MODE_STATUS,
|
||||||
SZ_TIME_UNTIL,
|
SZ_TIME_UNTIL,
|
||||||
SZ_TIME_ZONE,
|
|
||||||
SZ_TIMING_MODE,
|
SZ_TIMING_MODE,
|
||||||
SZ_UNTIL,
|
SZ_UNTIL,
|
||||||
)
|
)
|
||||||
@ -50,13 +44,14 @@ from homeassistant.helpers.dispatcher import (
|
|||||||
async_dispatcher_send,
|
async_dispatcher_send,
|
||||||
)
|
)
|
||||||
from homeassistant.helpers.entity import Entity
|
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.service import verify_domain_control
|
||||||
from homeassistant.helpers.storage import Store
|
from homeassistant.helpers.storage import Store
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
|
ACCESS_TOKEN,
|
||||||
ACCESS_TOKEN_EXPIRES,
|
ACCESS_TOKEN_EXPIRES,
|
||||||
ATTR_DURATION_DAYS,
|
ATTR_DURATION_DAYS,
|
||||||
ATTR_DURATION_HOURS,
|
ATTR_DURATION_HOURS,
|
||||||
@ -65,12 +60,11 @@ from .const import (
|
|||||||
ATTR_ZONE_TEMP,
|
ATTR_ZONE_TEMP,
|
||||||
CONF_LOCATION_IDX,
|
CONF_LOCATION_IDX,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
GWS,
|
REFRESH_TOKEN,
|
||||||
SCAN_INTERVAL_DEFAULT,
|
SCAN_INTERVAL_DEFAULT,
|
||||||
SCAN_INTERVAL_MINIMUM,
|
SCAN_INTERVAL_MINIMUM,
|
||||||
STORAGE_KEY,
|
STORAGE_KEY,
|
||||||
STORAGE_VER,
|
STORAGE_VER,
|
||||||
TCS,
|
|
||||||
USER_DATA,
|
USER_DATA,
|
||||||
EvoService,
|
EvoService,
|
||||||
)
|
)
|
||||||
@ -79,6 +73,7 @@ from .helpers import (
|
|||||||
convert_dict,
|
convert_dict,
|
||||||
convert_until,
|
convert_until,
|
||||||
dt_aware_to_naive,
|
dt_aware_to_naive,
|
||||||
|
dt_local_to_aware,
|
||||||
handle_evo_exception,
|
handle_evo_exception,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -118,91 +113,158 @@ SET_ZONE_OVERRIDE_SCHEMA: Final = vol.Schema(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
class EvoSession:
|
||||||
"""Create a (EMEA/EU-based) Honeywell TCC system."""
|
"""Class for evohome client instantiation & authentication."""
|
||||||
|
|
||||||
async def load_auth_tokens(store: Store) -> tuple[dict, dict | None]:
|
def __init__(self, hass: HomeAssistant) -> None:
|
||||||
app_storage = await store.async_load()
|
"""Initialize the evohome broker and its data structure."""
|
||||||
tokens = dict(app_storage or {})
|
|
||||||
|
|
||||||
if tokens.pop(CONF_USERNAME, None) != config[DOMAIN][CONF_USERNAME]:
|
self.hass = hass
|
||||||
# any tokens won't be valid, and store might be corrupt
|
|
||||||
await store.async_save({})
|
|
||||||
return ({}, {})
|
|
||||||
|
|
||||||
# evohomeasync2 requires naive/local datetimes as strings
|
self._session = async_get_clientsession(hass)
|
||||||
if tokens.get(ACCESS_TOKEN_EXPIRES) is not None and (
|
self._store = Store[dict[str, Any]](hass, STORAGE_VER, STORAGE_KEY)
|
||||||
expires := dt_util.parse_datetime(tokens[ACCESS_TOKEN_EXPIRES])
|
|
||||||
|
# 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) -> None:
|
||||||
|
"""Check the user credentials against the web API.
|
||||||
|
|
||||||
|
Will raise evo.AuthenticationFailed if the credentials are invalid.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if (
|
||||||
|
self.client_v2 is None
|
||||||
|
or username != self.client_v2.username
|
||||||
|
or password != self.client_v2.password
|
||||||
):
|
):
|
||||||
tokens[ACCESS_TOKEN_EXPIRES] = dt_aware_to_naive(expires)
|
await self._load_auth_tokens(username)
|
||||||
|
|
||||||
user_data = tokens.pop(USER_DATA, {})
|
|
||||||
return (tokens, user_data)
|
|
||||||
|
|
||||||
store = Store[dict[str, Any]](hass, STORAGE_VER, STORAGE_KEY)
|
|
||||||
tokens, user_data = await load_auth_tokens(store)
|
|
||||||
|
|
||||||
client_v2 = evo.EvohomeClient(
|
client_v2 = evo.EvohomeClient(
|
||||||
config[DOMAIN][CONF_USERNAME],
|
username,
|
||||||
config[DOMAIN][CONF_PASSWORD],
|
password,
|
||||||
**tokens,
|
**self._tokens,
|
||||||
session=async_get_clientsession(hass),
|
session=self._session,
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
else: # force a re-authentication
|
||||||
|
client_v2 = self.client_v2
|
||||||
|
client_v2._user_account = None # noqa: SLF001
|
||||||
|
|
||||||
await client_v2.login()
|
await client_v2.login()
|
||||||
|
await self.save_auth_tokens()
|
||||||
|
|
||||||
|
self.client_v2 = client_v2
|
||||||
|
|
||||||
|
self.client_v1 = ev1.EvohomeClient(
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
session_id=self.session_id,
|
||||||
|
session=self._session,
|
||||||
|
)
|
||||||
|
|
||||||
|
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."""
|
||||||
|
|
||||||
|
sess = EvoSession(hass)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await sess.authenticate(
|
||||||
|
config[DOMAIN][CONF_USERNAME],
|
||||||
|
config[DOMAIN][CONF_PASSWORD],
|
||||||
|
)
|
||||||
|
|
||||||
except evo.AuthenticationFailed as err:
|
except evo.AuthenticationFailed as err:
|
||||||
handle_evo_exception(err)
|
handle_evo_exception(err)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
config[DOMAIN][CONF_PASSWORD] = "REDACTED"
|
config[DOMAIN][CONF_PASSWORD] = "REDACTED"
|
||||||
|
|
||||||
assert isinstance(client_v2.installation_info, list) # mypy
|
broker = EvoBroker(sess)
|
||||||
|
|
||||||
loc_idx = config[DOMAIN][CONF_LOCATION_IDX]
|
if not broker.validate_location(
|
||||||
try:
|
config[DOMAIN][CONF_LOCATION_IDX],
|
||||||
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
|
return False
|
||||||
|
|
||||||
if _LOGGER.isEnabledFor(logging.DEBUG):
|
coordinator = DataUpdateCoordinator(
|
||||||
loc_info = {
|
hass,
|
||||||
SZ_LOCATION_ID: loc_config[SZ_LOCATION_INFO][SZ_LOCATION_ID],
|
_LOGGER,
|
||||||
SZ_TIME_ZONE: loc_config[SZ_LOCATION_INFO][SZ_TIME_ZONE],
|
name=f"{DOMAIN}_coordinator",
|
||||||
}
|
update_interval=config[DOMAIN][CONF_SCAN_INTERVAL],
|
||||||
gwy_info = {
|
update_method=broker.async_update,
|
||||||
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}],
|
|
||||||
}
|
|
||||||
_LOGGER.debug("Config = %s", _config)
|
|
||||||
|
|
||||||
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),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
hass.data[DOMAIN] = {}
|
hass.data[DOMAIN] = {"broker": broker, "coordinator": coordinator}
|
||||||
hass.data[DOMAIN]["broker"] = broker = EvoBroker(
|
|
||||||
hass, client_v2, client_v1, store, config[DOMAIN]
|
|
||||||
)
|
|
||||||
|
|
||||||
await broker.save_auth_tokens()
|
# without a listener, _schedule_refresh() won't be invoked by _async_refresh()
|
||||||
await broker.async_update() # get initial state
|
coordinator.async_add_listener(lambda: None)
|
||||||
|
await coordinator.async_refresh() # get initial state
|
||||||
|
|
||||||
hass.async_create_task(
|
hass.async_create_task(
|
||||||
async_load_platform(hass, Platform.CLIMATE, DOMAIN, {}, config)
|
async_load_platform(hass, Platform.CLIMATE, DOMAIN, {}, config)
|
||||||
@ -212,10 +274,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
async_load_platform(hass, Platform.WATER_HEATER, DOMAIN, {}, config)
|
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)
|
setup_service_functions(hass, broker)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
@ -272,7 +330,7 @@ def setup_service_functions(hass: HomeAssistant, broker: EvoBroker) -> None:
|
|||||||
hass.services.async_register(DOMAIN, EvoService.REFRESH_SYSTEM, force_refresh)
|
hass.services.async_register(DOMAIN, EvoService.REFRESH_SYSTEM, force_refresh)
|
||||||
|
|
||||||
# Enumerate which operating modes are supported by this system
|
# 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
|
# 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]:
|
if [m[SZ_SYSTEM_MODE] for m in modes if m[SZ_SYSTEM_MODE] == SZ_AUTO_WITH_RESET]:
|
||||||
|
@ -9,7 +9,6 @@ from typing import TYPE_CHECKING, Any
|
|||||||
import evohomeasync2 as evo
|
import evohomeasync2 as evo
|
||||||
from evohomeasync2.schema.const import (
|
from evohomeasync2.schema.const import (
|
||||||
SZ_ACTIVE_FAULTS,
|
SZ_ACTIVE_FAULTS,
|
||||||
SZ_ALLOWED_SYSTEM_MODES,
|
|
||||||
SZ_SETPOINT_STATUS,
|
SZ_SETPOINT_STATUS,
|
||||||
SZ_SYSTEM_ID,
|
SZ_SYSTEM_ID,
|
||||||
SZ_SYSTEM_MODE,
|
SZ_SYSTEM_MODE,
|
||||||
@ -44,7 +43,6 @@ from .const import (
|
|||||||
ATTR_DURATION_UNTIL,
|
ATTR_DURATION_UNTIL,
|
||||||
ATTR_SYSTEM_MODE,
|
ATTR_SYSTEM_MODE,
|
||||||
ATTR_ZONE_TEMP,
|
ATTR_ZONE_TEMP,
|
||||||
CONF_LOCATION_IDX,
|
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
EVO_AUTO,
|
EVO_AUTO,
|
||||||
EVO_AUTOECO,
|
EVO_AUTOECO,
|
||||||
@ -112,8 +110,8 @@ async def async_setup_platform(
|
|||||||
"Found the Location/Controller (%s), id=%s, name=%s (location_idx=%s)",
|
"Found the Location/Controller (%s), id=%s, name=%s (location_idx=%s)",
|
||||||
broker.tcs.modelType,
|
broker.tcs.modelType,
|
||||||
broker.tcs.systemId,
|
broker.tcs.systemId,
|
||||||
broker.tcs.location.name,
|
broker.loc.name,
|
||||||
broker.params[CONF_LOCATION_IDX],
|
broker.loc_idx,
|
||||||
)
|
)
|
||||||
|
|
||||||
entities: list[EvoClimateEntity] = [EvoController(broker, broker.tcs)]
|
entities: list[EvoClimateEntity] = [EvoController(broker, broker.tcs)]
|
||||||
@ -367,7 +365,7 @@ class EvoController(EvoClimateEntity):
|
|||||||
self._attr_unique_id = evo_device.systemId
|
self._attr_unique_id = evo_device.systemId
|
||||||
self._attr_name = evo_device.location.name
|
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 = [
|
self._attr_preset_modes = [
|
||||||
TCS_PRESET_TO_HA[m] for m in modes if m in list(TCS_PRESET_TO_HA)
|
TCS_PRESET_TO_HA[m] for m in modes if m in list(TCS_PRESET_TO_HA)
|
||||||
]
|
]
|
||||||
|
@ -5,85 +5,93 @@ from __future__ import annotations
|
|||||||
from collections.abc import Awaitable
|
from collections.abc import Awaitable
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
import evohomeasync as ev1
|
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
|
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.dispatcher import async_dispatcher_send
|
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||||
from homeassistant.helpers.event import async_call_later
|
from homeassistant.helpers.event import async_call_later
|
||||||
from homeassistant.helpers.storage import Store
|
|
||||||
from homeassistant.helpers.typing import ConfigType
|
|
||||||
|
|
||||||
from .const import (
|
from .const import CONF_LOCATION_IDX, DOMAIN, GWS, TCS, UTC_OFFSET
|
||||||
ACCESS_TOKEN,
|
from .helpers import handle_evo_exception
|
||||||
ACCESS_TOKEN_EXPIRES,
|
|
||||||
CONF_LOCATION_IDX,
|
if TYPE_CHECKING:
|
||||||
DOMAIN,
|
from . import EvoSession
|
||||||
GWS,
|
|
||||||
REFRESH_TOKEN,
|
|
||||||
TCS,
|
|
||||||
USER_DATA,
|
|
||||||
UTC_OFFSET,
|
|
||||||
)
|
|
||||||
from .helpers import dt_local_to_aware, handle_evo_exception
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__.rpartition(".")[0])
|
_LOGGER = logging.getLogger(__name__.rpartition(".")[0])
|
||||||
|
|
||||||
|
|
||||||
class EvoBroker:
|
class EvoBroker:
|
||||||
"""Container for evohome client and data."""
|
"""Broker for evohome client broker."""
|
||||||
|
|
||||||
def __init__(
|
loc_idx: int
|
||||||
self,
|
loc: evo.Location
|
||||||
hass: HomeAssistant,
|
loc_utc_offset: timedelta
|
||||||
client: evo.EvohomeClient,
|
tcs: evo.ControlSystem
|
||||||
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]
|
def __init__(self, sess: EvoSession) -> None:
|
||||||
self._location: evo.Location = client.locations[loc_idx]
|
"""Initialize the evohome broker and its data structure."""
|
||||||
|
|
||||||
assert isinstance(client.installation_info, list) # mypy
|
self._sess = sess
|
||||||
|
self.hass = sess.hass
|
||||||
|
|
||||||
|
assert sess.client_v2 is not None # mypy
|
||||||
|
|
||||||
|
self.client = sess.client_v2
|
||||||
|
self.client_v1 = sess.client_v1
|
||||||
|
|
||||||
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] = {}
|
self.temps: dict[str, float | None] = {}
|
||||||
|
|
||||||
async def save_auth_tokens(self) -> None:
|
def validate_location(self, loc_idx: int) -> bool:
|
||||||
"""Save access tokens and session IDs to the store for later use."""
|
"""Get the default TCS of the specified location."""
|
||||||
# evohomeasync2 uses naive/local datetimes
|
|
||||||
access_token_expires = dt_local_to_aware(
|
self.loc_idx = loc_idx
|
||||||
self.client.access_token_expires # type: ignore[arg-type]
|
|
||||||
|
assert self.client.installation_info is not None # mypy
|
||||||
|
|
||||||
|
try:
|
||||||
|
loc_config = self.client.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.installation_info) - 1,
|
||||||
)
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
app_storage: dict[str, Any] = {
|
self.loc = self.client.locations[loc_idx]
|
||||||
CONF_USERNAME: self.client.username,
|
self.loc_utc_offset = timedelta(minutes=self.loc.timeZone[UTC_OFFSET])
|
||||||
REFRESH_TOKEN: self.client.refresh_token,
|
self.tcs = self.loc._gateways[0]._control_systems[0] # noqa: SLF001
|
||||||
ACCESS_TOKEN: self.client.access_token,
|
|
||||||
ACCESS_TOKEN_EXPIRES: access_token_expires.isoformat(),
|
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}],
|
||||||
|
}
|
||||||
|
_LOGGER.debug("Config = %s", config)
|
||||||
|
|
||||||
if self.client_v1:
|
return True
|
||||||
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] = {}
|
|
||||||
|
|
||||||
await self._store.async_save(app_storage)
|
|
||||||
|
|
||||||
async def call_client_api(
|
async def call_client_api(
|
||||||
self,
|
self,
|
||||||
@ -108,11 +116,7 @@ class EvoBroker:
|
|||||||
|
|
||||||
assert self.client_v1 is not None # mypy check
|
assert self.client_v1 is not None # mypy check
|
||||||
|
|
||||||
def get_session_id(client_v1: ev1.EvohomeClient) -> str | None:
|
old_session_id = self._sess.session_id
|
||||||
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)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
temps = await self.client_v1.get_temperatures()
|
temps = await self.client_v1.get_temperatures()
|
||||||
@ -146,7 +150,7 @@ class EvoBroker:
|
|||||||
raise
|
raise
|
||||||
|
|
||||||
else:
|
else:
|
||||||
if str(self.client_v1.location_id) != self._location.locationId:
|
if str(self.client_v1.location_id) != self.loc.locationId:
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
"The v2 API's configured location doesn't match "
|
"The v2 API's configured location doesn't match "
|
||||||
"the v1 API's default location (there is more than one location), "
|
"the v1 API's default location (there is more than one location), "
|
||||||
@ -157,8 +161,8 @@ class EvoBroker:
|
|||||||
self.temps = {str(i[SZ_ID]): i[SZ_TEMP] for i in temps}
|
self.temps = {str(i[SZ_ID]): i[SZ_TEMP] for i in temps}
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
if self.client_v1 and 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()
|
await self._sess.save_auth_tokens()
|
||||||
|
|
||||||
_LOGGER.debug("Temperatures = %s", self.temps)
|
_LOGGER.debug("Temperatures = %s", self.temps)
|
||||||
|
|
||||||
@ -168,7 +172,7 @@ class EvoBroker:
|
|||||||
access_token = self.client.access_token # maybe receive a new token?
|
access_token = self.client.access_token # maybe receive a new token?
|
||||||
|
|
||||||
try:
|
try:
|
||||||
status = await self._location.refresh_status()
|
status = await self.loc.refresh_status()
|
||||||
except evo.RequestFailed as err:
|
except evo.RequestFailed as err:
|
||||||
handle_evo_exception(err)
|
handle_evo_exception(err)
|
||||||
else:
|
else:
|
||||||
@ -176,7 +180,7 @@ class EvoBroker:
|
|||||||
_LOGGER.debug("Status = %s", status)
|
_LOGGER.debug("Status = %s", status)
|
||||||
finally:
|
finally:
|
||||||
if access_token != self.client.access_token:
|
if access_token != self.client.access_token:
|
||||||
await self.save_auth_tokens()
|
await self._sess.save_auth_tokens()
|
||||||
|
|
||||||
async def async_update(self, *args: Any) -> None:
|
async def async_update(self, *args: Any) -> None:
|
||||||
"""Get the latest state data of an entire Honeywell TCC Location.
|
"""Get the latest state data of an entire Honeywell TCC Location.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user