1- import aiohttp
2- import asyncio
3- import json
41import logging
52import settings
63
74from datetime import datetime , timedelta
8- from evohomeasync2 import EvohomeClient , ControlSystem , Location , Zone
9- from evohomeasync2 .auth import AbstractTokenManager
10- from evohomeasync2 .exceptions import ApiRequestFailedError , AuthenticationFailedError , BadUserCredentialsError
5+ from evohomeasync2 import ControlSystem , Location , Zone
116from evohomeasync2 .schemas import SystemMode , ZoneMode
7+ from evohome_helper import evohome_client
128from evohome_helper import weather
13- from tenacity import retry , retry_if_exception_type , retry_if_not_exception_type , stop_after_attempt , wait_exponential
149from typing import Generator
1510
1611logger = logging .getLogger (__name__ )
17- _evohome_client : EvohomeClient | None = None
18- _websession : aiohttp .ClientSession | None = None
19-
20- # only retry errors that can resolve on their own; invalid credentials never will
21- _TRANSIENT_ERRORS = (
22- aiohttp .ClientError ,
23- TimeoutError ,
24- ApiRequestFailedError ,
25- AuthenticationFailedError ,
26- )
27- _retry = retry (
28- retry = retry_if_exception_type (_TRANSIENT_ERRORS ) & retry_if_not_exception_type (BadUserCredentialsError ),
29- wait = wait_exponential (),
30- stop = stop_after_attempt (6 ),
31- reraise = True ,
32- )
33-
34- # schedules rarely change; refetching them every cycle wastes the vendor's tight API rate limit
35- _SCHEDULE_REFRESH_INTERVAL = timedelta (hours = 1 )
36- _schedule_refresh_times : dict [str , datetime ] = {}
3712
3813_AWAY_MODE_MAP = {
3914 "auto" : SystemMode .AUTO ,
4722_DAY_NAMES = ["Monday" , "Tuesday" , "Wednesday" , "Thursday" , "Friday" , "Saturday" , "Sunday" ]
4823
4924
50- class LocationNotFound (Exception ):
51- def __init__ (self , location_name : str ):
52- super ().__init__ (f"the location '{ location_name } ' does not exist in the evohome account" )
53-
54-
55- class _TokenManager (AbstractTokenManager ):
56- """Caches auth tokens on disk so restarts reuse them instead of
57- re-authenticating against the heavily rate-limited vendor API."""
58-
59- def __init__ (self , username : str , password : str , websession : aiohttp .ClientSession , token_cache_path : str ):
60- super ().__init__ (username , password , websession )
61- self ._token_cache_path = token_cache_path
62-
63- async def load_access_token (self ) -> None :
64- try :
65- with open (self ._token_cache_path ) as f :
66- self ._import_access_token (json .load (f ))
67- except FileNotFoundError :
68- pass
69- except (KeyError , ValueError ):
70- logger .warning ("ignoring the invalid token cache at '%s'" , self ._token_cache_path )
71-
72- async def save_access_token (self ) -> None :
73- with open (self ._token_cache_path , "w" ) as f :
74- json .dump (self ._export_access_token (), f )
75-
76-
77- async def close () -> None :
78- global _evohome_client , _websession
79-
80- _schedule_refresh_times .clear ()
81- _evohome_client = None
82-
83- if _websession is not None :
84- await _websession .close ()
85- _websession = None
86-
87-
88- # discard the cached client so the next call recreates it from scratch
89- reset_client = close
90-
91-
92- @_retry
93- async def _client () -> EvohomeClient :
94- global _evohome_client , _websession
95-
96- if _evohome_client is None :
97- websession = aiohttp .ClientSession ()
98- try :
99- token_manager = _TokenManager (
100- settings .EVOHOME_USERNAME ,
101- settings .EVOHOME_PASSWORD ,
102- websession ,
103- settings .EVOHOME_TOKEN_CACHE_PATH ,
104- )
105- await token_manager .load_access_token ()
106-
107- new_client = EvohomeClient (token_manager )
108- await new_client .update (dont_update_status = True )
109- except BaseException :
110- await websession .close ()
111- raise
112-
113- _websession = websession
114- _evohome_client = new_client
115-
116- return _evohome_client
117-
118-
11925def get_current_time (location : Location ) -> datetime :
12026 return location .now ().replace (microsecond = 0 )
12127
12228
123- def get_control_systems (location : Location ) -> Generator [ControlSystem , None , None ]:
124- for gateway in location .gateways :
125- for control_system in gateway .systems :
126- yield control_system
127-
128-
129- @_retry
130- async def _update_location (location : Location ) -> None :
131- await location .update ()
132-
133-
134- @_retry
135- async def _fetch_schedules (system : ControlSystem ) -> None :
136- await system .get_schedules ()
137- _schedule_refresh_times [system .id ] = datetime .now ()
138-
139-
140- def _schedules_need_refresh (system : ControlSystem ) -> bool :
141- last_refresh = _schedule_refresh_times .get (system .id )
142- return last_refresh is None or datetime .now () - last_refresh >= _SCHEDULE_REFRESH_INTERVAL
143-
144-
145- async def get_location (location_name : str | None = None ) -> Location :
146- if not location_name :
147- location_name = settings .EVOHOME_LOCATION_NAME
148-
149- client = await _client ()
150- for location in client .locations :
151- if location .name == location_name :
152- await _update_location (location )
153- await asyncio .gather (
154- * [
155- _fetch_schedules (system )
156- for system in get_control_systems (location )
157- if _schedules_need_refresh (system )
158- ],
159- )
160- return location
161-
162- raise LocationNotFound (location_name )
163-
164-
16529def _switchpoint_to_datetime (day_of_week : str , time_of_day : str , now : datetime ) -> datetime :
16630 target_weekday = _DAY_NAMES .index (day_of_week )
16731 days_ago = (now .weekday () - target_weekday ) % 7
@@ -230,7 +94,7 @@ def is_in_schedule_grace_period(location: Location) -> bool:
23094
23195
23296def get_zones (location : Location ) -> Generator [Zone , None , None ]:
233- for control_system in get_control_systems (location ):
97+ for control_system in evohome_client . get_control_systems (location ):
23498 for zone in control_system .zones :
23599 if zone .active_faults :
236100 continue
@@ -298,13 +162,8 @@ def _get_highest_set_point_temp(location: Location) -> float | None:
298162 return max (valid_setpoints , default = None )
299163
300164
301- @_retry
302- async def _set_control_system_mode (control_system : ControlSystem , new_mode : SystemMode ) -> None :
303- await control_system .set_mode (new_mode )
304-
305-
306165async def _set_mode (new_mode : SystemMode , location : Location ) -> None :
307- for control_system in get_control_systems (location ):
166+ for control_system in evohome_client . get_control_systems (location ):
308167 current_mode = control_system .mode
309168 if new_mode == current_mode :
310169 continue
@@ -314,7 +173,7 @@ async def _set_mode(new_mode: SystemMode, location: Location) -> None:
314173 continue
315174
316175 logger .debug ("changing thermostat (%s) mode to '%s'" , control_system .id , new_mode )
317- await _set_control_system_mode (control_system , new_mode )
176+ await evohome_client . set_system_mode (control_system , new_mode )
318177
319178
320179async def set_normal (location : Location ) -> None :
0 commit comments