3
3
import sys
4
4
from functools import lru_cache
5
5
from time import time
6
+ from datetime import timezone
6
7
7
8
import aiohttp
8
9
import orjson
@@ -56,11 +57,11 @@ def __init__(
56
57
device_id = device_id ,
57
58
)
58
59
59
- def set_ssl_context (self , ssl_verify , ssl_cafile ):
60
+ def set_ssl_context (self , ssl_verify : bool , ssl_cafile : str ):
60
61
"""Create or set the SSL context. Use custom ssl verification, if specified."""
61
62
self .ssl_context = get_ssl_context (ssl_verify , ssl_cafile )
62
63
63
- async def authenticate (self , username , password , ssl_verify = True , ssl_cafile = "" ):
64
+ async def authenticate (self , username : str , password : str , ssl_verify : bool = True , ssl_cafile : str = "" ) -> None :
64
65
"""Authenticate with username (email) and password. Optionally set SSL context as well.
65
66
This or `load_auth` must be called once at the start of the session."""
66
67
auth_data = {"email" : username , "password" : password }
@@ -83,21 +84,22 @@ async def authenticate(self, username, password, ssl_verify=True, ssl_cafile="")
83
84
# check for 200 return
84
85
if resp .status != 200 :
85
86
raise SenseAuthenticationException (
86
- "Please check username and password. API Return Code: %s" % resp .status
87
+ f "Please check username and password. API Return Code: { resp .status } "
87
88
)
88
89
89
90
# Build out some common variables
90
91
data = await resp .json ()
91
92
self ._set_auth_data (data )
92
93
self .set_monitor_id (data ["monitors" ][0 ]["id" ])
94
+ await self .fetch_devices ()
93
95
94
- async def validate_mfa (self , code ) :
96
+ async def validate_mfa (self , code : str ) -> None :
95
97
"""Validate a multi-factor authentication code after authenticate raised SenseMFARequiredException.
96
98
Authentication process is completed if code is valid."""
97
99
mfa_data = {
98
100
"totp" : code ,
99
101
"mfa_token" : self ._mfa_token ,
100
- "client_time:" : datetime .utcnow ( ).strftime ("%Y-%m-%dT%H:%M:%SZ" ),
102
+ "client_time:" : datetime .now ( timezone . utc ).strftime ("%Y-%m-%dT%H:%M:%SZ" ),
101
103
}
102
104
103
105
# Get auth token
@@ -115,8 +117,10 @@ async def validate_mfa(self, code):
115
117
data = await resp .json ()
116
118
self ._set_auth_data (data )
117
119
self .set_monitor_id (data ["monitors" ][0 ]["id" ])
120
+ await self .fetch_discovered_devices ()
118
121
119
- async def renew_auth (self ):
122
+ async def renew_auth (self ) -> None :
123
+ """Renew the authentication token."""
120
124
renew_data = {
121
125
"user_id" : self .sense_user_id ,
122
126
"refresh_token" : self .refresh_token ,
@@ -135,14 +139,15 @@ async def renew_auth(self):
135
139
136
140
self ._set_auth_data (await resp .json ())
137
141
138
- async def logout (self ):
142
+ async def logout (self ) -> None :
143
+ """Log out of Sense."""
139
144
# Get auth token
140
- async with self ._client_session .get (API_URL + "logout" , timeout = self .api_timeout , data = renew_data ) as resp :
145
+ async with self ._client_session .get (API_URL + "logout" , timeout = self .api_timeout ) as resp :
141
146
# check for 200 return
142
147
if resp .status != 200 :
143
148
raise SenseAPIException (f"API Return Code: { resp .status } " )
144
149
145
- async def update_realtime (self , retry = True ):
150
+ async def update_realtime (self , retry : bool = True ) -> None :
146
151
"""Update the realtime data (device status and current power)."""
147
152
# rate limit API calls
148
153
now = time ()
@@ -158,7 +163,7 @@ async def update_realtime(self, retry=True):
158
163
else :
159
164
raise e
160
165
161
- async def async_realtime_stream (self , callback = None , single = False ):
166
+ async def async_realtime_stream (self , callback : callable = None , single : bool = False ) -> None :
162
167
"""Reads realtime data from websocket. Data is passed to callback if available.
163
168
Continues reading realtime stream data forever unless 'single' is set to True.
164
169
"""
@@ -186,7 +191,7 @@ async def async_realtime_stream(self, callback=None, single=False):
186
191
raise SenseAuthenticationException ("Web Socket Unauthorized" )
187
192
raise SenseWebsocketException (data ["error_reason" ])
188
193
189
- async def get_realtime_future (self , callback ) :
194
+ async def get_realtime_future (self , callback : callable ) -> None :
190
195
"""Returns an async Future to parse realtime data with callback"""
191
196
await self .async_realtime_stream (callback )
192
197
@@ -213,39 +218,51 @@ async def _api_call(self, url, payload={}, retry=False):
213
218
# timed out
214
219
raise SenseAPITimeoutException ("API call timed out" ) from ex
215
220
216
- async def get_trend_data (self , scale , dt = None ):
221
+ async def get_trend_data (self , scale : Scale , dt : datetime = None ) -> None :
217
222
"""Update trend data for specified scale from API.
218
223
Optionally set a date to fetch data from."""
219
- if scale .upper () not in valid_scales :
220
- raise Exception ("%s not a valid scale" % scale )
221
224
if not dt :
222
- dt = datetime .utcnow ( )
225
+ dt = datetime .now ( timezone . utc )
223
226
json = self ._api_call (
224
- "app/history/trends?monitor_id=%s&scale=%s&start=%s "
225
- % ( self . sense_monitor_id , scale , dt .strftime (" %Y-%m-%dT%H:%M:%S" ))
227
+ f "app/history/trends?monitor_id={ self . sense_monitor_id } "
228
+ + f"&device_id=always_on& scale= { scale . name } &start= { dt .strftime (' %Y-%m-%dT%H:%M:%S' ) } "
226
229
)
227
230
self ._trend_data [scale ] = await json
231
+ self ._update_device_trends (scale )
228
232
229
- async def update_trend_data (self , dt = None ):
233
+ async def update_trend_data (self , dt : datetime = None ) -> None :
230
234
"""Update trend data of all scales from API.
231
235
Optionally set a date to fetch data from."""
232
- for scale in valid_scales :
236
+ for scale in Scale :
233
237
await self .get_trend_data (scale , dt )
234
238
235
239
async def get_monitor_data (self ):
236
240
"""Get monitor overview info from API."""
237
- json = await self ._api_call ("app/monitors/%s/overview" % self .sense_monitor_id )
241
+ json = await self ._api_call (f "app/monitors/{ self .sense_monitor_id } /overview" )
238
242
if "monitor_overview" in json and "monitor" in json ["monitor_overview" ]:
239
243
self ._monitor = json ["monitor_overview" ]["monitor" ]
240
244
return self ._monitor
241
245
242
- async def get_discovered_device_names (self ):
243
- """Get list of device names from API."""
244
- json = self ._api_call ("app/monitors/%s/devices" % self .sense_monitor_id )
245
- self ._devices = await [entry ["name" ] for entry in json ]
246
- return self ._devices
246
+ async def fetch_devices (self ) -> None :
247
+ """Fetch discovered devices from API."""
248
+ json = await self ._api_call (f"app/monitors/{ self .sense_monitor_id } /devices/overview" )
249
+ for device in json ["devices" ]:
250
+ if not device ["tags" ].get ("DeviceListAllowed" , True ):
251
+ continue
252
+ id = device ["id" ]
253
+ if id not in self ._devices :
254
+ self ._devices [id ] = SenseDevice (id )
255
+ self ._devices [id ].name = device ["name" ]
256
+ self ._devices [id ].icon = device ["icon" ]
257
+
258
+ async def get_discovered_device_names (self ) -> list [str ]:
259
+ """Outdated. Get list of device names from API.
260
+ Use fetch_discovered_devices and sense.devices instead."""
261
+ await self .fetch_devices ()
262
+ return [d .name for d in self ._devices .values ()]
247
263
248
264
async def get_discovered_device_data (self ):
249
- """Get list of raw device data from API."""
250
- json = self ._api_call ("monitors/%s/devices" % self .sense_monitor_id )
251
- return await json
265
+ """Outdated. Get list of raw device data from API.
266
+ Use fetch_discovered_devices and sense.devices instead."""
267
+ json = self ._api_call (f"monitors/{ self .sense_monitor_id } /devices/overview" )
268
+ return await json ["devices" ]
0 commit comments