Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add coordinator to evohome and prune async_update code #119432

Merged
merged 42 commits into from
Jul 23, 2024
Merged
Show file tree
Hide file tree
Changes from 34 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
fc50a59
functional programming tweak
zxdavb Jun 9, 2024
b85bf2a
doctweak
zxdavb Jun 9, 2024
89dac22
typing hint
zxdavb Jun 9, 2024
de503c3
rename symbol
zxdavb Jun 9, 2024
01fef2d
Switch to DataUpdateCoordinator
zxdavb Jun 9, 2024
878a5bc
move from async_setup to EvoBroker
zxdavb Jun 10, 2024
43c1ea1
tweaks - add v1 back in
zxdavb Jun 11, 2024
80a1757
tidy up
zxdavb Jun 11, 2024
f2bf2f3
tidy up docstring
zxdavb Jun 11, 2024
2687eba
Merge branch 'dev' into evo_coordinator2
zxdavb Jun 11, 2024
586ac19
lint
zxdavb Jun 11, 2024
3f1627b
Merge remote-tracking branch 'upstream/dev' into evo_coordinator2
zxdavb Jun 12, 2024
e06dbaa
remove redundant logging
zxdavb Jun 12, 2024
6bc809b
rename symbol
zxdavb Jun 12, 2024
d37103f
Merge branch 'dev' into evo_coordinator2
zxdavb Jun 15, 2024
cf6fb71
split back to inject authenticator clas
zxdavb Jun 18, 2024
5ee58b1
rename symbols
zxdavb Jun 18, 2024
103b60a
Merge branch 'evo_coordinator2' of https://github.com/zxdavb/hass int…
zxdavb Jun 18, 2024
7eedf45
Merge remote-tracking branch 'upstream/dev' into evo_coordinator2
zxdavb Jun 18, 2024
3748099
rename symbol
zxdavb Jun 19, 2024
2a7e420
Update homeassistant/components/evohome/__init__.py
zxdavb Jun 19, 2024
b407ddb
Merge branch 'evo_coordinator2' of https://github.com/zxdavb/hass int…
zxdavb Jun 19, 2024
f87f3c9
allow exception to pass through
zxdavb Jun 19, 2024
03b7aa2
allow re-authentication with diff credentials
zxdavb Jun 19, 2024
c0ab46f
lint
zxdavb Jun 19, 2024
bb635d4
undo unrelated change
zxdavb Jun 19, 2024
1c49c58
Merge remote-tracking branch 'upstream/dev' into evo_coordinator2
zxdavb Jun 19, 2024
87bb404
use async_refresh instead of async_config_entry_first_refresh
zxdavb Jun 20, 2024
f258f9d
Merge branch 'dev' into evo_coordinator2
zxdavb Jun 21, 2024
dd87ab0
Merge branch 'dev' into evo_coordinator2
elupus Jun 21, 2024
4e6d5c6
Merge branch 'dev' into evo_coordinator2
epenet Jun 21, 2024
ccad70e
Merge branch 'dev' into evo_coordinator2
zxdavb Jun 23, 2024
279be0c
Merge remote-tracking branch 'upstream/dev' into evo_coordinator2
zxdavb Jun 24, 2024
636ac1b
Merge branch 'evo_coordinator2' of https://github.com/zxdavb/hass int…
zxdavb Jun 24, 2024
f4713d4
assign None instead of empty dict as Falsey value
zxdavb Jul 1, 2024
1d7ee48
use class attrs instead of type hints
zxdavb Jul 2, 2024
8cb7ec8
Merge remote-tracking branch 'upstream/dev' into evo_coordinator2
zxdavb Jul 22, 2024
93e66aa
speed up mypy hint
zxdavb Jul 22, 2024
2f0b584
speed up mypy check
zxdavb Jul 22, 2024
e18e4bf
small tidy up
zxdavb Jul 22, 2024
b395a62
Merge remote-tracking branch 'upstream/dev' into evo_coordinator2
zxdavb Jul 22, 2024
ca351df
small tidy up
zxdavb Jul 22, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
211 changes: 135 additions & 76 deletions homeassistant/components/evohome/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,14 @@
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,
)
Expand All @@ -50,13 +44,14 @@
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 (
ACCESS_TOKEN,
ACCESS_TOKEN_EXPIRES,
ATTR_DURATION_DAYS,
ATTR_DURATION_HOURS,
Expand All @@ -65,12 +60,11 @@
ATTR_ZONE_TEMP,
CONF_LOCATION_IDX,
DOMAIN,
GWS,
REFRESH_TOKEN,
SCAN_INTERVAL_DEFAULT,
SCAN_INTERVAL_MINIMUM,
STORAGE_KEY,
STORAGE_VER,
TCS,
USER_DATA,
EvoService,
)
Expand All @@ -79,6 +73,7 @@
convert_dict,
convert_until,
dt_aware_to_naive,
dt_local_to_aware,
handle_evo_exception,
)

Expand Down Expand Up @@ -118,91 +113,159 @@
)


async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Create a (EMEA/EU-based) Honeywell TCC system."""
class EvoSession:
"""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) -> 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
):
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
zxdavb marked this conversation as resolved.
Show resolved Hide resolved

async def load_auth_tokens(store: Store) -> tuple[dict, dict | None]:
app_storage = await store.async_load()
tokens = dict(app_storage or {})
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.
"""

if tokens.pop(CONF_USERNAME, None) != config[DOMAIN][CONF_USERNAME]:
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 store.async_save({})
return ({}, {})
await self._store.async_save({})

self.session_id = None
self._tokens = {}

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])
if app_storage.get(ACCESS_TOKEN_EXPIRES) is not None and (
expires := dt_util.parse_datetime(app_storage[ACCESS_TOKEN_EXPIRES])
):
tokens[ACCESS_TOKEN_EXPIRES] = dt_aware_to_naive(expires)
app_storage[ACCESS_TOKEN_EXPIRES] = dt_aware_to_naive(expires)

user_data = tokens.pop(USER_DATA, {})
return (tokens, user_data)
user_data: dict[str, str] = app_storage.pop(USER_DATA, {})

store = Store[dict[str, Any]](hass, STORAGE_VER, STORAGE_KEY)
tokens, user_data = await load_auth_tokens(store)
self.session_id = user_data.get(SZ_SESSION_ID)
self._tokens = app_storage

client_v2 = evo.EvohomeClient(
config[DOMAIN][CONF_USERNAME],
config[DOMAIN][CONF_PASSWORD],
**tokens,
session=async_get_clientsession(hass),
)
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 client_v2.login()
await sess.authenticate(
config[DOMAIN][CONF_USERNAME],
config[DOMAIN][CONF_PASSWORD],
)

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
broker = EvoBroker(sess)

loc_idx = 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,
)
if not broker.validate_location(
config[DOMAIN][CONF_LOCATION_IDX],
):
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],
}
_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 = EvoBroker(
hass, client_v2, client_v1, store, config[DOMAIN]
hass.data[DOMAIN]["broker"] = broker
zxdavb marked this conversation as resolved.
Show resolved Hide resolved

hass.data[DOMAIN]["coordinator"] = coordinator = DataUpdateCoordinator(
zxdavb marked this conversation as resolved.
Show resolved Hide resolved
hass,
_LOGGER,
name=f"{DOMAIN}_coordinator",
update_interval=config[DOMAIN][CONF_SCAN_INTERVAL],
update_method=broker.async_update,
)
zxdavb marked this conversation as resolved.
Show resolved Hide resolved

await broker.save_auth_tokens()
await broker.async_update() # get initial state
# 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

hass.async_create_task(
async_load_platform(hass, Platform.CLIMATE, DOMAIN, {}, config)
Expand All @@ -212,10 +275,6 @@ async def load_auth_tokens(store: Store) -> tuple[dict, dict | None]:
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
Expand Down Expand Up @@ -272,7 +331,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]:
Expand Down
8 changes: 3 additions & 5 deletions homeassistant/components/evohome/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -44,7 +43,6 @@
ATTR_DURATION_UNTIL,
ATTR_SYSTEM_MODE,
ATTR_ZONE_TEMP,
CONF_LOCATION_IDX,
DOMAIN,
EVO_AUTO,
EVO_AUTOECO,
Expand Down Expand Up @@ -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)]
Expand Down Expand Up @@ -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)
]
Expand Down
Loading