# HG changeset patch # User Daniel O'Connor # Date 1504858901 -34200 # Node ID 8d6ba11c1b76d8c3be0a57f3db38a7e986fabbf8 Fetch & graphing code basically works. diff -r 000000000000 -r 8d6ba11c1b76 agl.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/agl.py Fri Sep 08 17:51:41 2017 +0930 @@ -0,0 +1,208 @@ +#!/usr/bin/env python + +import ConfigParser +import datetime +import exceptions +import json +import os +import pytz +import requests +import sqlite3 +import sys + +loginurl = 'https://command.aglsolar.com.au/api/v2/Account/LoginUser' +dataurl = 'https://command.aglsolar.com.au/api/v2/graph/b8e08afb-818f-4d2d-9d28-5afe8fc76a32' +# ?endDate=2017-08-23&granularity=Minute&metrics=read&startDate=2017-08-23&units=W' +logouturl = 'https://command.aglsolar.com.au/api/v2/Account/Logout' + +class UTC(datetime.tzinfo): + def utcoffset(self, dt): + return datetime.timedelta(0) + + def dst(self, dt): + return datetime.timedelta(0) + + def tzname(self, dt): + return "UTC" + + +def main(): + conf = ConfigParser.ConfigParser() + confname = os.environ['HOME'] + '/.agl.ini' + conf.read(confname) + username = conf.get('DEFAULT', 'username') + password = conf.get('DEFAULT', 'password') + dbfn = conf.get('DEFAULT', 'db') + + if conf.has_option('DEFAULT', 'token'): + token = conf.get('DEFAULT', 'token') + else: + token = gettoken(username, password) + conf.set('DEFAULT', 'token', token) + conf.write(file(confname, 'w')) + + if len(sys.argv) > 1: + date = sys.argv[1] + else: + date = datetime.datetime.now().strftime('%Y-%m-%d') + + dbh = sqlite3.connect(dbfn, detect_types = sqlite3.PARSE_DECLTYPES) + cur = dbh.cursor() + data = getdata(token, date, date) + if data == None: + token = gettoken(username, password) + data = getdata(token, date, date) + if data == None: + print('Unable to fetch data') + updatedb(cur, data) + dbh.commit() + +def mkdb(cur): + cur.execute(''' +CREATE TABLE IF NOT EXISTS agl ( + t_stamp TIMESTAMP PRIMARY KEY, + battery_charge NUMBER, + battery_power NUMBER, + power_consumed NUMBER, + power_expected NUMBER, + power_exported NUMBER, + power_generated NUMBER, + power_imported NUMBER, + estimated_savings NUMBER, + pv_forecast NUMBER, + pv_gen_battery NUMBER, + pv_gen_grid NUMBER, + pv_gen_site NUMBER, + site_cons_battery NUMBER, + site_cons_grid NUMBER, + site_cons_pv NUMBER +)''') + +units = { + 'battery_charge' : '%', + 'battery_power' : 'Watt', + 'power_consumed' : 'Watt', + 'power_expected' : 'Watt', + 'power_exported' : 'Watt', + 'power_generated' : 'Watt', + 'power_imported' : 'Watt', + 'estimated_savings' : '$', + 'pv_forecast' : 'Watt', + 'pv_gen_battery' : 'Watt', + 'pv_gen_grid' : 'Watt', + 'pv_gen_site' : 'Watt', + 'site_cons_battery' : 'Watt', + 'site_cons_grid' : 'Watt', + 'site_cons_pv' : 'Watt' + } +def graph(cur, cols, start, end): + import numpy + import matplotlib + import matplotlib.dates + import matplotlib.pylab + + #matplotlib.rcParams['timezone'] = pytz.timezone('Australia/Adelaide') + + colourlist = ['b','g','r','c','m','y','k'] + yaxisunits1 = None + yaxisunits2 = None + ax1lines = [] + ax2lines = [] + colouridx = 0 + for col in cols: + unit = units[col] + if yaxisunits1 == None: + yaxisunits1 = unit + if yaxisunits2 == None: + if unit != yaxisunits1: + yaxisunits2 = unit + else: + if unit != yaxisunits1 and unit != yaxisunits2: + raise exceptions.Exception('Asked to graph >2 different units') + + cur.execute('SELECT t_stamp, ' + reduce(lambda a, b: a + ', ' + b, cols) + ' FROM agl WHERE t_stamp > ? AND t_stamp < ? ORDER BY t_stamp', + (start, end)) + ary = numpy.array(cur.fetchall()) + for idx in range(len(cols)): + if units[cols[idx]] == yaxisunits1: + ax1lines.append([ary[:,0], ary[:,idx + 1], cols[idx], colourlist[colouridx]]) + else: + ax2lines.append([ary[:,0], ary[:,idx + 1], cols[idx], colourlist[colouridx]]) + colouridx += 1 + + fig = matplotlib.pylab.figure() + ax1 = fig.add_subplot(111) + ax1.set_ylabel(yaxisunits1) + + for line in ax1lines: + ax1.plot(line[0], line[1], label = line[2]) + + ax1.legend(loc = 'upper left') + + if yaxisunits2 != None: + ax2 = ax1.twinx() + ax2.set_ylabel(yaxisunits2) + + for line in ax2lines: + ax2.plot(line[0], line[1], label = line[2], color = line[3]) + ax2.legend(loc = 'upper right') + + # Rotate X axis labels + for ax in fig.get_axes(): + ax.xaxis.set_major_formatter(matplotlib.dates.DateFormatter('%H:%M')) + ax.xaxis.set_major_locator(matplotlib.dates.HourLocator(interval = 2)) + ax.xaxis.set_minor_locator(matplotlib.dates.MinuteLocator(interval = 5)) + for label in ax.get_xticklabels(): + label.set_ha('right') + label.set_rotation(30) + + # Fudge margins to give more graph and less space + fig.subplots_adjust(left = 0.10, right = 0.88, top = 0.95, bottom = 0.15) + matplotlib.pyplot.show() + +def updatedb(cur, data): + mkdb(cur) + for d in data['reads']['data']: + ts = datetime.datetime.strptime(d['t_stamp'], '%Y-%m-%dT%H:%M:%SZ') + # Note we rename *energy* to *power* here to match what it actually means + vals = [ts, d['battery_charge'], d['battery_energy'], d['energy_consumed'], d['energy_expected'], d['energy_exported'], d['energy_generated'], + d['energy_imported'], d['estimated_savings'], d['pv_forecast'], d['pv_generation']['battery_energy'], + d['pv_generation']['grid_energy'], d['pv_generation']['site_energy'], d['site_consumption']['battery_energy'], + d['site_consumption']['grid_energy'], d['site_consumption']['pv_energy']] + skip = True + for v in vals[1:]: + if v != None: + skip = False + break + if skip: + print('Skipping empty record at ' + str(ts)) + continue + cur.execute('INSERT OR IGNORE INTO agl(t_stamp, battery_charge, battery_power, power_consumed, power_expected, power_exported, power_generated, power_imported, estimated_savings, pv_forecast, pv_gen_battery, pv_gen_grid, pv_gen_site, site_cons_battery, site_cons_grid, site_cons_pv) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', vals) + +def gettoken(username, password): + authblob = json.encoder.JSONEncoder().encode({'email' : username, 'password' : password}) + reply = requests.request('POST', loginurl, data = authblob, headers = {'Content-Type' : 'application/json'}) + if reply.status_code != 200: + return None + return json.decoder.JSONDecoder().decode(reply.content)['access_token'] + +def getdata(token, startdate, enddate): + reply = requests.request('GET', dataurl, params = { + 'startDate' : startdate, + 'endDate' : enddate, + 'granularity' : 'Minute', + 'metrics' : 'read', + 'units' : 'W', + }, headers = { 'Authorization' : 'Bearer ' + token}) + + if reply.status_code != 200: + return None + + return json.decoder.JSONDecoder().decode(reply.content) + +def logout(token): + reply = requests.request('GET', logouturl, headers = { 'Authorization' : 'Bearer ' + token}) + return reply.status_code == 200 + +if __name__ == '__main__': + main()