-
-
Notifications
You must be signed in to change notification settings - Fork 32.7k
/
Copy pathcoordinator.py
191 lines (154 loc) · 6.6 KB
/
coordinator.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
"""Support for (EMEA/EU-based) Honeywell TCC systems."""
from __future__ import annotations
from collections.abc import Awaitable
from datetime import timedelta
import logging
from typing import Any
import evohomeasync as ev1
from evohomeasync.schema import SZ_ID, SZ_SESSION_ID, SZ_TEMP
import evohomeasync2 as evo
from homeassistant.const import CONF_USERNAME
from homeassistant.core import HomeAssistant
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
from .const import (
ACCESS_TOKEN,
ACCESS_TOKEN_EXPIRES,
CONF_LOCATION_IDX,
DOMAIN,
GWS,
REFRESH_TOKEN,
TCS,
USER_DATA,
UTC_OFFSET,
)
from .helpers import dt_local_to_aware, handle_evo_exception
_LOGGER = logging.getLogger(__name__.rpartition(".")[0])
class EvoBroker:
"""Container for evohome client and data."""
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]
assert isinstance(client.installation_info, list) # mypy
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."""
# evohomeasync2 uses naive/local datetimes
access_token_expires = dt_local_to_aware(
self.client.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,
ACCESS_TOKEN_EXPIRES: access_token_expires.isoformat(),
}
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] = {}
await self._store.async_save(app_storage)
async def call_client_api(
self,
client_api: Awaitable[dict[str, Any] | None],
update_state: bool = True,
) -> dict[str, Any] | None:
"""Call a client API and update the broker state if required."""
try:
result = await client_api
except evo.RequestFailed as err:
handle_evo_exception(err)
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)
return result
async def _update_v1_api_temps(self) -> None:
"""Get the latest high-precision temperatures of the default Location."""
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]
session_id = get_session_id(self.client_v1)
try:
temps = await self.client_v1.get_temperatures()
except ev1.InvalidSchema as err:
_LOGGER.warning(
(
"Unable to obtain high-precision temperatures. "
"It appears the JSON schema is not as expected, "
"so the high-precision feature will be disabled until next restart."
"Message is: %s"
),
err,
)
self.client_v1 = None
except ev1.RequestFailed as err:
_LOGGER.warning(
(
"Unable to obtain the latest high-precision temperatures. "
"Check your network and the vendor's service status page. "
"Proceeding without high-precision temperatures for now. "
"Message is: %s"
),
err,
)
self.temps = {} # high-precision temps now considered stale
except Exception:
self.temps = {} # high-precision temps now considered stale
raise
else:
if str(self.client_v1.location_id) != self._location.locationId:
_LOGGER.warning(
"The v2 API's configured location doesn't match "
"the v1 API's default location (there is more than one location), "
"so the high-precision feature will be disabled until next restart"
)
self.client_v1 = None
else:
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()
_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?
try:
status = await self._location.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()
async def async_update(self, *args: Any) -> None:
"""Get the latest state data of an entire Honeywell TCC Location.
This includes state data for a Controller and all its child devices, such as the
operating mode of the Controller and the current temp of its children (e.g.
Zones, DHW controller).
"""
await self._update_v2_api_state()
if self.client_v1:
await self._update_v1_api_temps()