Skip to content

Commit 3c3107e

Browse files
authored
Device trends and Python Updates (#68)
* Update to add Scales, more typing, and device trend data * Typing/version updates
1 parent e5379b0 commit 3c3107e

8 files changed

+248
-187
lines changed

sense_energy/__init__.py

+4-7
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
1-
from .sense_api import SenseableBase
1+
from .sense_api import SenseableBase, Scale
22
from .sense_exceptions import *
33

44
from .senseable import Senseable
5-
import sys
6-
7-
if sys.version_info >= (3, 5):
8-
from .asyncsenseable import ASyncSenseable
9-
from .plug_instance import PlugInstance
10-
from .sense_link import SenseLink
5+
from .asyncsenseable import ASyncSenseable
6+
from .plug_instance import PlugInstance
7+
from .sense_link import SenseLink
118

129
__version__ = "{{VERSION_PLACEHOLDER}}"

sense_energy/asyncsenseable.py

+45-28
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import sys
44
from functools import lru_cache
55
from time import time
6+
from datetime import timezone
67

78
import aiohttp
89
import orjson
@@ -56,11 +57,11 @@ def __init__(
5657
device_id=device_id,
5758
)
5859

59-
def set_ssl_context(self, ssl_verify, ssl_cafile):
60+
def set_ssl_context(self, ssl_verify: bool, ssl_cafile: str):
6061
"""Create or set the SSL context. Use custom ssl verification, if specified."""
6162
self.ssl_context = get_ssl_context(ssl_verify, ssl_cafile)
6263

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:
6465
"""Authenticate with username (email) and password. Optionally set SSL context as well.
6566
This or `load_auth` must be called once at the start of the session."""
6667
auth_data = {"email": username, "password": password}
@@ -83,21 +84,22 @@ async def authenticate(self, username, password, ssl_verify=True, ssl_cafile="")
8384
# check for 200 return
8485
if resp.status != 200:
8586
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}"
8788
)
8889

8990
# Build out some common variables
9091
data = await resp.json()
9192
self._set_auth_data(data)
9293
self.set_monitor_id(data["monitors"][0]["id"])
94+
await self.fetch_devices()
9395

94-
async def validate_mfa(self, code):
96+
async def validate_mfa(self, code: str) -> None:
9597
"""Validate a multi-factor authentication code after authenticate raised SenseMFARequiredException.
9698
Authentication process is completed if code is valid."""
9799
mfa_data = {
98100
"totp": code,
99101
"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"),
101103
}
102104

103105
# Get auth token
@@ -115,8 +117,10 @@ async def validate_mfa(self, code):
115117
data = await resp.json()
116118
self._set_auth_data(data)
117119
self.set_monitor_id(data["monitors"][0]["id"])
120+
await self.fetch_discovered_devices()
118121

119-
async def renew_auth(self):
122+
async def renew_auth(self) -> None:
123+
"""Renew the authentication token."""
120124
renew_data = {
121125
"user_id": self.sense_user_id,
122126
"refresh_token": self.refresh_token,
@@ -135,14 +139,15 @@ async def renew_auth(self):
135139

136140
self._set_auth_data(await resp.json())
137141

138-
async def logout(self):
142+
async def logout(self) -> None:
143+
"""Log out of Sense."""
139144
# 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:
141146
# check for 200 return
142147
if resp.status != 200:
143148
raise SenseAPIException(f"API Return Code: {resp.status}")
144149

145-
async def update_realtime(self, retry=True):
150+
async def update_realtime(self, retry: bool = True) -> None:
146151
"""Update the realtime data (device status and current power)."""
147152
# rate limit API calls
148153
now = time()
@@ -158,7 +163,7 @@ async def update_realtime(self, retry=True):
158163
else:
159164
raise e
160165

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:
162167
"""Reads realtime data from websocket. Data is passed to callback if available.
163168
Continues reading realtime stream data forever unless 'single' is set to True.
164169
"""
@@ -186,7 +191,7 @@ async def async_realtime_stream(self, callback=None, single=False):
186191
raise SenseAuthenticationException("Web Socket Unauthorized")
187192
raise SenseWebsocketException(data["error_reason"])
188193

189-
async def get_realtime_future(self, callback):
194+
async def get_realtime_future(self, callback: callable) -> None:
190195
"""Returns an async Future to parse realtime data with callback"""
191196
await self.async_realtime_stream(callback)
192197

@@ -213,39 +218,51 @@ async def _api_call(self, url, payload={}, retry=False):
213218
# timed out
214219
raise SenseAPITimeoutException("API call timed out") from ex
215220

216-
async def get_trend_data(self, scale, dt=None):
221+
async def get_trend_data(self, scale: Scale, dt: datetime = None) -> None:
217222
"""Update trend data for specified scale from API.
218223
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)
221224
if not dt:
222-
dt = datetime.utcnow()
225+
dt = datetime.now(timezone.utc)
223226
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')}"
226229
)
227230
self._trend_data[scale] = await json
231+
self._update_device_trends(scale)
228232

229-
async def update_trend_data(self, dt=None):
233+
async def update_trend_data(self, dt: datetime = None) -> None:
230234
"""Update trend data of all scales from API.
231235
Optionally set a date to fetch data from."""
232-
for scale in valid_scales:
236+
for scale in Scale:
233237
await self.get_trend_data(scale, dt)
234238

235239
async def get_monitor_data(self):
236240
"""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")
238242
if "monitor_overview" in json and "monitor" in json["monitor_overview"]:
239243
self._monitor = json["monitor_overview"]["monitor"]
240244
return self._monitor
241245

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()]
247263

248264
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"]

sense_energy/plug_instance.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import hashlib
44
from time import time
5-
from typing import Optional, Dict, Any
5+
from typing import Optional, Any
66
from functools import lru_cache
77

88

@@ -65,7 +65,7 @@ def __init__(
6565
else:
6666
self.mac = _generate_mac(self.device_id)
6767

68-
def generate_response(self) -> Dict[str, Dict[str, Any]]:
68+
def generate_response(self) -> dict[str, dict[str, Any]]:
6969
"""Generate a response dict for the plug."""
7070
# Response dict
7171
return {

sense_energy/py.typed

Whitespace-only changes.

0 commit comments

Comments
 (0)