changeset 0:8d6ba11c1b76

Fetch & graphing code basically works.
author Daniel O'Connor <darius@dons.net.au>
date Fri, 08 Sep 2017 17:51:41 +0930
parents
children 6e3ca5bfda04
files agl.py
diffstat 1 files changed, 208 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /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()