diff --git a/README.md b/README.md index 8bbc7a6..189efef 100644 --- a/README.md +++ b/README.md @@ -27,13 +27,16 @@ pip install sense_energy ``` ### Example Usage: -``` -(pyvienv) ~/code/sense/sense_energy   stable ●  python sense_api.py -Please enter you Sense username (email address): -Please enter your Sense password: -('Active:', 2917.29736328125, 'W') -('Active Solar:', 0, 'W') -('Active Devices:', u'Other, Always On') +```python + sense = Senseable() + sense.authenticate(username, password) + sense.update_realtime() + sense.update_trend_data() + print ("Active:",sense.active_power,"W") + print ("Active Solar:",sense.active_solar_power,"W") + print ("Daily:",sense.daily_usage,"KW") + print ("Daily Solar:",sense.daily_production,"KW") + print ("Active Devices:",", ".join(sense.active_devices)) ``` There are plenty of methods for you to call so modify however you see fit diff --git a/sense_energy/__init__.py b/sense_energy/__init__.py index 9e25f0c..484a143 100644 --- a/sense_energy/__init__.py +++ b/sense_energy/__init__.py @@ -1,4 +1,9 @@ -from .sense_api import * +from .sense_api import SenseableBase from .sense_exceptions import * -__version__ = "0.6.0" +from .senseable import Senseable +import sys +if sys.version_info >= (3, 5): + from .asyncsenseable import ASyncSenseable + +__version__ = "0.7.0" diff --git a/sense_energy/asyncsenseable.py b/sense_energy/asyncsenseable.py new file mode 100644 index 0000000..471cb8d --- /dev/null +++ b/sense_energy/asyncsenseable.py @@ -0,0 +1,101 @@ +import asyncio +import aiohttp +import json +import websockets + +from .sense_api import * +from .sense_exceptions import * + +class ASyncSenseable(SenseableBase): + + async def authenticate(self, username, password): + auth_data = { + "email": username, + "password": password + } + + # Get auth token + try: + async with aiohttp.ClientSession() as session: + async with session.post(API_URL+'authenticate', + data=auth_data) as resp: + + # check for 200 return + if resp.status != 200: + raise SenseAuthenticationException( + "Please check username and password. API Return Code: %s" % + resp.status) + + # Build out some common variables + self.set_auth_data(await resp.json()) + except Exception as e: + raise Exception('Connection failure: %s' % e) + + # Update the realtime data for asyncio + async def update_realtime(self): + # rate limit API calls + if self._realtime and self.rate_limit and \ + self.last_realtime_call + self.rate_limit > time(): + return self._realtime + self.last_realtime_call = time() + await self.async_realtime_stream(single=True) + + async def async_realtime_stream(self, callback=None, single=False): + """ Reads realtime data from websocket""" + url = WS_URL % (self.sense_monitor_id, self.sense_access_token) + # hello, features, [updates,] data + async with websockets.connect(url) as ws: + while True: + try: + message = await asyncio.wait_for( + ws.recv(), timeout=self.wss_timeout) + except asyncio.TimeoutError: + raise SenseAPITimeoutException("API websocket timed out") + + result = json.loads(message) + if result.get('type') == 'realtime_update': + data = result['payload'] + self.set_realtime(data) + if callback: callback(data) + if single: return + + async def get_realtime_future(self, callback): + """ Returns an async Future to parse realtime data with callback""" + await self.async_realtime_stream(callback) + + async def api_call(self, url, payload={}): + timeout = aiohttp.ClientTimeout(total=self.api_timeout) + async with aiohttp.ClientSession() as session: + async with session.get(API_URL + url, + headers=self.headers, + timeout=timeout, + data=payload) as resp: + return await resp.json() + # timed out + raise SenseAPITimeoutException("API call timed out") + + async def get_trend_data(self, scale): + if scale.upper() not in valid_scales: + raise Exception("%s not a valid scale" % scale) + t = datetime.now().replace(hour=12) + json = self.api_call( + 'app/history/trends?monitor_id=%s&scale=%s&start=%s' % + (self.sense_monitor_id, scale, t.isoformat())) + self._trend_data[scale] = await json + + async def update_trend_data(self): + for scale in valid_scales: + await self.get_trend_data(scale) + + async def get_discovered_device_names(self): + # lots more info in here to be parsed out + json = self.api_call('app/monitors/%s/devices' % + self.sense_monitor_id) + self._devices = await [entry['name'] for entry in json] + return self._devices + + async def get_discovered_device_data(self): + json = self.api_call('monitors/%s/devices' % + self.sense_monitor_id) + return await json + diff --git a/sense_energy/sense_api.py b/sense_energy/sense_api.py index 0d6cdfd..d4c4b6d 100644 --- a/sense_energy/sense_api.py +++ b/sense_energy/sense_api.py @@ -1,16 +1,9 @@ import json -import requests import sys from time import time from datetime import datetime -from requests.exceptions import ReadTimeout from .sense_exceptions import * -from . import ws_sync -if sys.version_info < (3, 6): - from . import ws_sync as websocket -else: - from . import ws_async as websocket API_URL = 'https://api.sense.com/apiservice/api/v1/' WS_URL = "wss://clientrt.sense.com/monitors/%s/realtimefeed?access_token=%s" @@ -22,7 +15,7 @@ valid_scales = ['HOUR', 'DAY', 'WEEK', 'MONTH', 'YEAR'] -class Senseable(object): +class SenseableBase(object): def __init__(self, username=None, password=None, api_timeout=API_TIMEOUT, wss_timeout=WSS_TIMEOUT): @@ -39,96 +32,42 @@ def __init__(self, username=None, password=None, if username and password: self.authenticate(username, password) - def authenticate(self, username, password): - auth_data = { - "email": username, - "password": password - } - - # Create session - self.s = requests.session() - - # Get auth token - try: - response = self.s.post(API_URL+'authenticate', - auth_data, timeout=self.api_timeout) - except Exception as e: - raise Exception('Connection failure: %s' % e) - - # check for 200 return - if response.status_code != 200: - raise SenseAuthenticationException( - "Please check username and password. API Return Code: %s" % - response.status_code) - - # Build out some common variables - self.sense_access_token = response.json()['access_token'] - self.sense_user_id = response.json()['user_id'] - self.sense_monitor_id = response.json()['monitors'][0]['id'] + def set_auth_data(self, data): + self.sense_access_token = data['access_token'] + self.sense_user_id = data['user_id'] + self.sense_monitor_id = data['monitors'][0]['id'] # create the auth header self.headers = {'Authorization': 'bearer {}'.format( self.sense_access_token)} + @property def devices(self): """Return devices.""" return self._devices - - def get_realtime(self): - # rate limit API calls - if self._realtime and self.rate_limit and \ - self.last_realtime_call + self.rate_limit > time(): - return self._realtime - url = WS_URL % (self.sense_monitor_id, self.sense_access_token) - self._realtime = websocket.get_realtime(url, self.wss_timeout) - self.last_realtime_call = time() - return self._realtime - def get_realtime_stream(self): - """ Reads realtime data from websocket - Continues until loop broken""" - url = WS_URL % (self.sense_monitor_id, self.sense_access_token) - stream = ws_sync.get_realtime_stream(url, self.wss_timeout) - while True: - self._realtime = next(stream) - yield self._realtime - - def get_realtime_future(self, callback): - """ Returns an async Future to parse realtime data with callback""" - def cb(data): - self._realtime = data - callback(data) - url = WS_URL % (self.sense_monitor_id, self.sense_access_token) - return websocket.get_realtime_future(url, self.wss_timeout, cb) - - def api_call(self, url, payload={}): - try: - return self.s.get(API_URL + url, - headers=self.headers, - timeout=self.api_timeout, - data=payload) - except ReadTimeout: - raise SenseAPITimeoutException("API call timed out") + def set_realtime(self, data): + self._realtime = data + self.last_realtime_call = time() + + def get_realtime(self): + return self._realtime @property def active_power(self): - if not self._realtime: self.get_realtime() return self._realtime.get('w', 0) @property def active_solar_power(self): - if not self._realtime: self.get_realtime() return self._realtime.get('solar_w', 0) @property def active_voltage(self): - if not self._realtime: self.get_realtime() return self._realtime.get('voltage', 0) @property def active_frequency(self): - if not self._realtime: self.get_realtime() return self._realtime.get('hz', 0) @property @@ -171,12 +110,10 @@ def yeary_production(self): @property def active_devices(self): - if not self._realtime: self.get_realtime() return [d['name'] for d in self._realtime.get('devices', {})] def get_trend(self, scale, is_production): - key = "production" if is_production else "consumption" - if not self._trend_data[scale]: self.get_trend_data(scale) + key = "production" if is_production else "consumption" if key not in self._trend_data[scale]: return 0 total = self._trend_data[scale][key].get('total', 0) if scale == 'WEEK' or scale == 'MONTH': @@ -184,75 +121,3 @@ def get_trend(self, scale, is_production): if scale == 'YEAR': return total + self.get_trend('MONTH', is_production) return total - - def get_discovered_device_names(self): - # lots more info in here to be parsed out - response = self.api_call('app/monitors/%s/devices' % - self.sense_monitor_id) - self._devices = [entry['name'] for entry in response.json()] - return self._devices - - def get_discovered_device_data(self): - response = self.api_call('monitors/%s/devices' % - self.sense_monitor_id) - return response.json() - - def always_on_info(self): - # Always on info - pretty generic similar to the web page - response = self.api_call('app/monitors/%s/devices/always_on' % - self.sense_monitor_id) - return response.json() - - def get_monitor_info(self): - # View info on your monitor & device detection status - response = self.api_call('app/monitors/%s/status' % - self.sense_monitor_id) - return response.json() - - def get_device_info(self, device_id): - # Get specific informaton about a device - response = self.api_call('app/monitors/%s/devices/%s' % - (self.sense_monitor_id, device_id)) - return response.json() - - def get_notification_preferences(self): - # Get notification preferences - payload = {'monitor_id': '%s' % self.sense_monitor_id} - response = self.api_call('users/%s/notifications' % - self.sense_user_id, payload) - return response.json() - - def get_trend_data(self, scale): - if scale.upper() not in valid_scales: - raise Exception("%s not a valid scale" % scale) - t = datetime.now().replace(hour=12) - response = self.api_call( - 'app/history/trends?monitor_id=%s&scale=%s&start=%s' % - (self.sense_monitor_id, scale, t.isoformat())) - self._trend_data[scale] = response.json() - - def update_trend_data(self): - for scale in valid_scales: - self.get_trend_data(scale) - - def get_all_usage_data(self): - payload = {'n_items': 30} - # lots of info in here to be parsed out - response = self.s.get('users/%s/timeline' % - self.sense_user_id, payload) - return response.json() - - -if __name__ == "__main__": - import pprint - import getpass - - # collect authn data - username = raw_input("Please enter you Sense username (email address): ") - password = getpass.getpass("Please enter your Sense password: ") - sense = Senseable(username, password) - print ("Active:", sense.active_power, "W") - print ("Active Solar:", sense.active_solar_power, "W") - print ("Active Devices:", ", ".join(sense.active_devices)) - print ("Active Voltage:", sense.active_voltage, "V") - print ("Active Frequency:", sense.active_frequency, "Hz") diff --git a/sense_energy/senseable.py b/sense_energy/senseable.py new file mode 100644 index 0000000..cd2e686 --- /dev/null +++ b/sense_energy/senseable.py @@ -0,0 +1,122 @@ +import json +import requests +from requests.exceptions import ReadTimeout +from websocket import create_connection +from websocket._exceptions import WebSocketTimeoutException + +from .sense_api import * +from .sense_exceptions import * + +class Senseable(SenseableBase): + + def authenticate(self, username, password): + auth_data = { + "email": username, + "password": password + } + + # Create session + self.s = requests.session() + + # Get auth token + try: + response = self.s.post(API_URL+'authenticate', + auth_data, timeout=self.api_timeout) + except Exception as e: + raise Exception('Connection failure: %s' % e) + + # check for 200 return + if response.status_code != 200: + raise SenseAuthenticationException( + "Please check username and password. API Return Code: %s" % + response.status_code) + + self.set_auth_data(response.json()) + + # Update the realtime data + def update_realtime(self): + # rate limit API calls + if self._realtime and self.rate_limit and \ + self.last_realtime_call + self.rate_limit > time(): + return self._realtime + url = WS_URL % (self.sense_monitor_id, self.sense_access_token) + next(self.get_realtime_stream()) + + def get_realtime_stream(self): + """ Reads realtime data from websocket + Continues until loop broken""" + ws = 0 + url = WS_URL % (self.sense_monitor_id, self.sense_access_token) + try: + ws = create_connection(url, timeout=self.wss_timeout) + while True: # hello, features, [updates,] data + result = json.loads(ws.recv()) + if result.get('type') == 'realtime_update': + data = result['payload'] + self.set_realtime(data) + yield data + except WebSocketTimeoutException: + raise SenseAPITimeoutException("API websocket timed out") + finally: + if ws: ws.close() + + def get_trend_data(self, scale): + if scale.upper() not in valid_scales: + raise Exception("%s not a valid scale" % scale) + t = datetime.now().replace(hour=12) + self._trend_data[scale] = self.api_call( + 'app/history/trends?monitor_id=%s&scale=%s&start=%s' % + (self.sense_monitor_id, scale, t.isoformat())) + + def update_trend_data(self): + for scale in valid_scales: + self.get_trend_data(scale) + + def api_call(self, url, payload={}): + try: + return self.s.get(API_URL + url, + headers=self.headers, + timeout=self.api_timeout, + data=payload).json() + except ReadTimeout: + raise SenseAPITimeoutException("API call timed out") + + def get_discovered_device_names(self): + # lots more info in here to be parsed out + json = self.api_call('app/monitors/%s/devices' % + self.sense_monitor_id) + self._devices = [entry['name'] for entry in json] + return self._devices + + def get_discovered_device_data(self): + return self.api_call('monitors/%s/devices' % + self.sense_monitor_id) + + def always_on_info(self): + # Always on info - pretty generic similar to the web page + return self.api_call('app/monitors/%s/devices/always_on' % + self.sense_monitor_id) + + def get_monitor_info(self): + # View info on your monitor & device detection status + return self.api_call('app/monitors/%s/status' % + self.sense_monitor_id) + + def get_device_info(self, device_id): + # Get specific informaton about a device + return self.api_call('app/monitors/%s/devices/%s' % + (self.sense_monitor_id, device_id)) + + def get_notification_preferences(self): + # Get notification preferences + payload = {'monitor_id': '%s' % self.sense_monitor_id} + return self.api_call('users/%s/notifications' % + self.sense_user_id, payload) + + def get_all_usage_data(self): + payload = {'n_items': 30} + # lots of info in here to be parsed out + return self.s.get('users/%s/timeline' % + self.sense_user_id, payload) + + diff --git a/sense_energy/ws_async.py b/sense_energy/ws_async.py deleted file mode 100644 index 017d266..0000000 --- a/sense_energy/ws_async.py +++ /dev/null @@ -1,31 +0,0 @@ -import asyncio -import json -import websockets -from .sense_exceptions import SenseAPITimeoutException - -data = 0 -async def get_realtime_stream(url, callback): - """ Reads realtime data from websocket""" - global data - # hello, features, [updates,] data - async with websockets.connect(url) as ws: - while True: - message = await ws.recv() - result = json.loads(message) - if result.get('type') == 'realtime_update': - data = result['payload'] - if callback: callback(data) - else: return - -def get_realtime(url, timeout): - global data - try: - data = 0 - asyncio.get_event_loop().run_until_complete( - asyncio.wait_for(get_realtime_stream(url, None), timeout)) - return data - except asyncio.TimeoutError: - raise SenseAPITimeoutException("API websocket timed out") - -def get_realtime_future(url, timeout, callback): - return get_realtime_stream(url, callback) diff --git a/sense_energy/ws_sync.py b/sense_energy/ws_sync.py deleted file mode 100644 index a827785..0000000 --- a/sense_energy/ws_sync.py +++ /dev/null @@ -1,26 +0,0 @@ -import json -from websocket import create_connection -from websocket._exceptions import WebSocketTimeoutException - -from .sense_exceptions import SenseAPITimeoutException - -def get_realtime_stream(url, timeout): - """ Reads realtime data from websocket - Continues until loop broken""" - ws = 0 - try: - ws = create_connection(url, timeout=timeout) - while True: # hello, features, [updates,] data - result = json.loads(ws.recv()) - if result.get('type') == 'realtime_update': - yield result['payload'] - except WebSocketTimeoutException: - raise SenseAPITimeoutException("API websocket timed out") - finally: - if ws: ws.close() - -def get_realtime(url, timeout): - return next(get_realtime_stream(url, timeout)) - -def get_realtime_future(url, timeout, callback): - raise NotImplementedError("Not available in Python < 3.6") diff --git a/setup.py b/setup.py index f074d16..3f65ec9 100644 --- a/setup.py +++ b/setup.py @@ -9,16 +9,16 @@ install_requires=[ 'requests', 'websocket-client', - 'websockets;python_version>="3.6"', + 'websockets;python_version>="3.5"', ], - version = '0.6.0', + version = '0.7.0', description = 'API for the Sense Energy Monitor', long_description=long_description, long_description_content_type="text/markdown", author = 'scottbonline', author_email = 'scottbonline@gmail.com', url = 'https://github.com/scottbonline/sense', - download_url = 'https://github.com/scottbonline/sense/archive/0.6.0.tar.gz', + download_url = 'https://github.com/scottbonline/sense/archive/0.7.0.tar.gz', keywords = ['sense', 'energy', 'api', 'pytest'], classifiers = [ 'Programming Language :: Python :: 2',