Home Assistant Unofficial Reference 2024.12.1
coordinator.py
Go to the documentation of this file.
1 """Support for (EMEA/EU-based) Honeywell TCC systems."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Awaitable
6 from datetime import timedelta
7 import logging
8 from typing import TYPE_CHECKING, Any
9 
10 import evohomeasync as ev1
11 from evohomeasync.schema import SZ_ID, SZ_TEMP
12 import evohomeasync2 as evo
13 from evohomeasync2.schema.const import (
14  SZ_GATEWAY_ID,
15  SZ_GATEWAY_INFO,
16  SZ_LOCATION_ID,
17  SZ_LOCATION_INFO,
18  SZ_TIME_ZONE,
19 )
20 
21 from homeassistant.helpers.dispatcher import async_dispatcher_send
22 
23 from .const import CONF_LOCATION_IDX, DOMAIN, GWS, TCS, UTC_OFFSET
24 from .helpers import handle_evo_exception
25 
26 if TYPE_CHECKING:
27  from . import EvoSession
28 
29 _LOGGER = logging.getLogger(__name__.rpartition(".")[0])
30 
31 
32 class EvoBroker:
33  """Broker for evohome client broker."""
34 
35  loc_idx: int
36  loc: evo.Location
37  loc_utc_offset: timedelta
38  tcs: evo.ControlSystem
39 
40  def __init__(self, sess: EvoSession) -> None:
41  """Initialize the evohome broker and its data structure."""
42 
43  self._sess_sess = sess
44  self.hasshass = sess.hass
45 
46  assert sess.client_v2 is not None # mypy
47 
48  self.clientclient = sess.client_v2
49  self.client_v1client_v1 = sess.client_v1
50 
51  self.tempstemps: dict[str, float | None] = {}
52 
53  def validate_location(self, loc_idx: int) -> bool:
54  """Get the default TCS of the specified location."""
55 
56  self.loc_idxloc_idx = loc_idx
57 
58  assert self.clientclient.installation_info is not None # mypy
59 
60  try:
61  loc_config = self.clientclient.installation_info[loc_idx]
62  except IndexError:
63  _LOGGER.error(
64  (
65  "Config error: '%s' = %s, but the valid range is 0-%s. "
66  "Unable to continue. Fix any configuration errors and restart HA"
67  ),
68  CONF_LOCATION_IDX,
69  loc_idx,
70  len(self.clientclient.installation_info) - 1,
71  )
72  return False
73 
74  self.locloc = self.clientclient.locations[loc_idx]
75  self.loc_utc_offsetloc_utc_offset = timedelta(minutes=self.locloc.timeZone[UTC_OFFSET])
76  self.tcstcs = self.locloc._gateways[0]._control_systems[0] # noqa: SLF001
77 
78  if _LOGGER.isEnabledFor(logging.DEBUG):
79  loc_info = {
80  SZ_LOCATION_ID: loc_config[SZ_LOCATION_INFO][SZ_LOCATION_ID],
81  SZ_TIME_ZONE: loc_config[SZ_LOCATION_INFO][SZ_TIME_ZONE],
82  }
83  gwy_info = {
84  SZ_GATEWAY_ID: loc_config[GWS][0][SZ_GATEWAY_INFO][SZ_GATEWAY_ID],
85  TCS: loc_config[GWS][0][TCS],
86  }
87  config = {
88  SZ_LOCATION_INFO: loc_info,
89  GWS: [{SZ_GATEWAY_INFO: gwy_info}],
90  }
91  _LOGGER.debug("Config = %s", config)
92 
93  return True
94 
95  async def call_client_api(
96  self,
97  client_api: Awaitable[dict[str, Any] | None],
98  update_state: bool = True,
99  ) -> dict[str, Any] | None:
100  """Call a client API and update the broker state if required."""
101 
102  try:
103  result = await client_api
104  except evo.RequestFailed as err:
106  return None
107 
108  if update_state: # wait a moment for system to quiesce before updating state
109  await self.hasshass.data[DOMAIN]["coordinator"].async_request_refresh()
110 
111  return result
112 
113  async def _update_v1_api_temps(self) -> None:
114  """Get the latest high-precision temperatures of the default Location."""
115 
116  assert self.client_v1client_v1 is not None # mypy check
117 
118  old_session_id = self._sess_sess.session_id
119 
120  try:
121  temps = await self.client_v1client_v1.get_temperatures()
122 
123  except ev1.InvalidSchema as err:
124  _LOGGER.warning(
125  (
126  "Unable to obtain high-precision temperatures. "
127  "It appears the JSON schema is not as expected, "
128  "so the high-precision feature will be disabled until next restart."
129  "Message is: %s"
130  ),
131  err,
132  )
133  self.client_v1client_v1 = None
134 
135  except ev1.RequestFailed as err:
136  _LOGGER.warning(
137  (
138  "Unable to obtain the latest high-precision temperatures. "
139  "Check your network and the vendor's service status page. "
140  "Proceeding without high-precision temperatures for now. "
141  "Message is: %s"
142  ),
143  err,
144  )
145  self.tempstemps = {} # high-precision temps now considered stale
146 
147  except Exception:
148  self.tempstemps = {} # high-precision temps now considered stale
149  raise
150 
151  else:
152  if str(self.client_v1client_v1.location_id) != self.locloc.locationId:
153  _LOGGER.warning(
154  "The v2 API's configured location doesn't match "
155  "the v1 API's default location (there is more than one location), "
156  "so the high-precision feature will be disabled until next restart"
157  )
158  self.client_v1client_v1 = None
159  else:
160  self.tempstemps = {str(i[SZ_ID]): i[SZ_TEMP] for i in temps}
161 
162  finally:
163  if self.client_v1client_v1 and self.client_v1client_v1.broker.session_id != old_session_id:
164  await self._sess_sess.save_auth_tokens()
165 
166  _LOGGER.debug("Temperatures = %s", self.tempstemps)
167 
168  async def _update_v2_api_state(self, *args: Any) -> None:
169  """Get the latest modes, temperatures, setpoints of a Location."""
170 
171  access_token = self.clientclient.access_token # maybe receive a new token?
172 
173  try:
174  status = await self.locloc.refresh_status()
175  except evo.RequestFailed as err:
177  else:
178  async_dispatcher_send(self.hasshass, DOMAIN)
179  _LOGGER.debug("Status = %s", status)
180  finally:
181  if access_token != self.clientclient.access_token:
182  await self._sess_sess.save_auth_tokens()
183 
184  async def async_update(self, *args: Any) -> None:
185  """Get the latest state data of an entire Honeywell TCC Location.
186 
187  This includes state data for a Controller and all its child devices, such as the
188  operating mode of the Controller and the current temp of its children (e.g.
189  Zones, DHW controller).
190  """
191  await self._update_v2_api_state_update_v2_api_state()
192 
193  if self.client_v1client_v1:
194  await self._update_v1_api_temps_update_v1_api_temps()
dict[str, Any]|None call_client_api(self, Awaitable[dict[str, Any]|None] client_api, bool update_state=True)
Definition: coordinator.py:99
None handle_evo_exception(evo.RequestFailed err)
Definition: helpers.py:66
None async_dispatcher_send(HomeAssistant hass, str signal, *Any args)
Definition: dispatcher.py:193