Skip to content

Commit

Permalink
Async (#28)
Browse files Browse the repository at this point in the history
* Added new example

* Split into entirely different class for async on python 3

* Added device fetchers

* Import updates

* Renamed

* Updated case

* Version update
  • Loading branch information
kbickar authored Mar 6, 2019
1 parent ad3035e commit b7d3be2
Show file tree
Hide file tree
Showing 8 changed files with 256 additions and 217 deletions.
17 changes: 10 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 7 additions & 2 deletions sense_energy/__init__.py
Original file line number Diff line number Diff line change
@@ -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"
101 changes: 101 additions & 0 deletions sense_energy/asyncsenseable.py
Original file line number Diff line number Diff line change
@@ -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

161 changes: 13 additions & 148 deletions sense_energy/sense_api.py
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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):
Expand All @@ -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
Expand Down Expand Up @@ -171,88 +110,14 @@ 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':
return total + self.get_trend('DAY', 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")
Loading

0 comments on commit b7d3be2

Please sign in to comment.