-
-
Notifications
You must be signed in to change notification settings - Fork 32k
/
Copy pathcoordinator.py
194 lines (152 loc) · 6.37 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
192
193
194
"""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 TYPE_CHECKING, Any
import evohomeasync as ev1
from evohomeasync.schema import SZ_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.helpers.dispatcher import async_dispatcher_send
from .const import CONF_LOCATION_IDX, DOMAIN, GWS, TCS, UTC_OFFSET
from .helpers import handle_evo_exception
if TYPE_CHECKING:
from . import EvoSession
_LOGGER = logging.getLogger(__name__.rpartition(".")[0])
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."""
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.temps: dict[str, float | None] = {}
def validate_location(self, loc_idx: int) -> bool:
"""Get the default TCS of the specified location."""
self.loc_idx = loc_idx
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
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
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)
return True
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
await self.hass.data[DOMAIN]["coordinator"].async_request_refresh()
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
old_session_id = self._sess.session_id
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.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), "
"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 self.client_v1.broker.session_id != old_session_id:
await self._sess.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.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._sess.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()