comparison agl.py @ 2:2b7fb26f9114

- Make an actual usable command line program. - Label things better. - Use tzlocal to determine local time zone. - Scale hour/minute ticks with data length.
author Daniel O'Connor <darius@dons.net.au>
date Mon, 11 Sep 2017 10:28:05 +0930
parents 6e3ca5bfda04
children 525a66486282
comparison
equal deleted inserted replaced
1:6e3ca5bfda04 2:2b7fb26f9114
1 #!/usr/bin/env python 1 #!/usr/bin/env python
2 2
3 import argparse
3 import ConfigParser 4 import ConfigParser
4 import datetime 5 import datetime
5 import dateutil 6 import dateutil
6 import exceptions 7 import exceptions
7 import json 8 import json
8 import os 9 import os
9 import requests 10 import requests
10 import sqlite3 11 import sqlite3
11 import sys 12 import sys
13 import tzlocal
12 14
13 loginurl = 'https://command.aglsolar.com.au/api/v2/Account/LoginUser' 15 loginurl = 'https://command.aglsolar.com.au/api/v2/Account/LoginUser'
14 dataurl = 'https://command.aglsolar.com.au/api/v2/graph/b8e08afb-818f-4d2d-9d28-5afe8fc76a32' 16 dataurl = 'https://command.aglsolar.com.au/api/v2/graph/b8e08afb-818f-4d2d-9d28-5afe8fc76a32'
15 # ?endDate=2017-08-23&granularity=Minute&metrics=read&startDate=2017-08-23&units=W' 17 # ?endDate=2017-08-23&granularity=Minute&metrics=read&startDate=2017-08-23&units=W'
16 logouturl = 'https://command.aglsolar.com.au/api/v2/Account/Logout' 18 logouturl = 'https://command.aglsolar.com.au/api/v2/Account/Logout'
24 26
25 def tzname(self, dt): 27 def tzname(self, dt):
26 return "UTC" 28 return "UTC"
27 29
28 30
31 def valid_date(s):
32 try:
33 return datetime.datetime.strptime(s, "%Y-%m-%d")
34 except ValueError:
35 raise argparse.ArgumentTypeError("Not a valid date: '{0}'.".format(s))
36
29 def main(): 37 def main():
38 parser = argparse.ArgumentParser()
39 parser.add_argument('-u', '--update', help = 'Update data', action="store_true")
40 parser.add_argument('-g', '--graph', help = 'Produce graph', action="store_true")
41 parser.add_argument('-s', '--start', help = 'Start date for graph (YYYY-MM-DD)', type = valid_date)
42 parser.add_argument('-e', '--end', help = 'End date for graph (YYYY-MM-DD)', type = valid_date)
43
44 args = parser.parse_args()
45
30 conf = ConfigParser.ConfigParser() 46 conf = ConfigParser.ConfigParser()
31 confname = os.environ['HOME'] + '/.agl.ini' 47 confname = os.environ['HOME'] + '/.agl.ini'
32 conf.read(confname) 48 conf.read(confname)
33 username = conf.get('DEFAULT', 'username') 49 username = conf.get('DEFAULT', 'username')
34 password = conf.get('DEFAULT', 'password') 50 password = conf.get('DEFAULT', 'password')
35 dbfn = conf.get('DEFAULT', 'db') 51 dbfn = conf.get('DEFAULT', 'db')
36 52
37 if conf.has_option('DEFAULT', 'token'): 53 if (args.start is None) ^ (args.end is None):
38 token = conf.get('DEFAULT', 'token') 54 parser.error('Must specify start and end or neither')
39 else: 55
40 token = gettoken(username, password) 56 if not args.update and not args.graph:
41 conf.set('DEFAULT', 'token', token) 57 parser.error('Nothing to do')
42 conf.write(file(confname, 'w')) 58
43 59 start = args.start
44 if len(sys.argv) > 1: 60 if start is None:
45 date = sys.argv[1] 61 start = datetime.date.today()
46 else: 62 start = datetime.datetime(start.year, start.month, start.day)
47 date = datetime.datetime.now().strftime('%Y-%m-%d') 63
64 end = args.end
65 if end is None:
66 end = start + datetime.timedelta(days = 1)
67 end = datetime.datetime(end.year, end.month, end.day)
48 68
49 dbh = sqlite3.connect(dbfn, detect_types = sqlite3.PARSE_DECLTYPES) 69 dbh = sqlite3.connect(dbfn, detect_types = sqlite3.PARSE_DECLTYPES)
50 cur = dbh.cursor() 70 cur = dbh.cursor()
51 data = getdata(token, date, date) 71 if args.update:
52 if data == None: 72 date = start
53 token = gettoken(username, password) 73 while date < end:
54 data = getdata(token, date, date) 74 if conf.has_option('DEFAULT', 'token'):
55 if data == None: 75 token = conf.get('DEFAULT', 'token')
56 print('Unable to fetch data') 76 else:
57 updatedb(cur, data) 77 token = gettoken(username, password)
58 dbh.commit() 78 conf.set('DEFAULT', 'token', token)
79 conf.write(file(confname, 'w'))
80
81 data = getdata(token, date, date)
82 if data == None:
83 #print('Getting new token')
84 token = gettoken(username, password)
85 data = getdata(token, date, date)
86 if data == None:
87 print('Unable to fetch data')
88 updatedb(cur, data)
89 dbh.commit()
90 date += datetime.timedelta(days = 1)
91
92 if args.graph:
93 graph(cur, ['battery_charge', 'power_imported', 'power_exported', 'power_consumed', 'power_generated'], start, end)
59 94
60 def mkdb(cur): 95 def mkdb(cur):
61 cur.execute(''' 96 cur.execute('''
62 CREATE TABLE IF NOT EXISTS agl ( 97 CREATE TABLE IF NOT EXISTS agl (
63 t_stamp TIMESTAMP PRIMARY KEY, 98 t_stamp TIMESTAMP PRIMARY KEY,
92 'pv_gen_grid' : 'Watt', 127 'pv_gen_grid' : 'Watt',
93 'pv_gen_site' : 'Watt', 128 'pv_gen_site' : 'Watt',
94 'site_cons_battery' : 'Watt', 129 'site_cons_battery' : 'Watt',
95 'site_cons_grid' : 'Watt', 130 'site_cons_grid' : 'Watt',
96 'site_cons_pv' : 'Watt' 131 'site_cons_pv' : 'Watt'
97 } 132 }
133
134 names = {
135 'battery_charge' : 'Battery Charge',
136 'battery_power' : 'Battery Power',
137 'power_consumed' : 'Power Consumed',
138 'power_expected' : 'Power Expected',
139 'power_exported' : 'Power Expected',
140 'power_generated' : 'Power Generated',
141 'power_imported' : 'Power Imported',
142 'estimated_savings' : 'Estimated Savings',
143 'pv_forecast' : 'PV Forecast',
144 'pv_gen_battery' : 'PV Generation Battery',
145 'pv_gen_grid' : 'PV Generation Grid',
146 'pv_gen_site' : 'PV Generation Site',
147 'site_cons_battery' : 'Site Consumption Batter',
148 'site_cons_grid' : 'Site Consumption Grid',
149 'site_cons_pv' : 'Site Consumption PV'
150 }
151
98 def graph(cur, cols, start, end): 152 def graph(cur, cols, start, end):
99 import numpy 153 import numpy
100 import matplotlib 154 import matplotlib
101 import matplotlib.dates 155 import matplotlib.dates
102 import matplotlib.pylab 156 import matplotlib.pylab
118 yaxisunits2 = unit 172 yaxisunits2 = unit
119 else: 173 else:
120 if unit != yaxisunits1 and unit != yaxisunits2: 174 if unit != yaxisunits1 and unit != yaxisunits2:
121 raise exceptions.Exception('Asked to graph >2 different units') 175 raise exceptions.Exception('Asked to graph >2 different units')
122 176
123 ltname = 'Australia/Adelaide' 177 ltname = tzlocal.get_localzone().zone # Why is this so hard..
124 lt = dateutil.tz.gettz(ltname) 178 lt = dateutil.tz.gettz(ltname)
125 utc = dateutil.tz.gettz('UTC') 179 utc = dateutil.tz.gettz('UTC')
126 matplotlib.rcParams['timezone'] = ltname 180 matplotlib.rcParams['timezone'] = ltname
127 181
128 if start.tzinfo == None: 182 if start.tzinfo == None:
137 colstr = reduce(lambda a, b: a + ', ' + b, cols) 191 colstr = reduce(lambda a, b: a + ', ' + b, cols)
138 # Data is stored as naive datetime's which are in UTC so convert the requested time here 192 # Data is stored as naive datetime's which are in UTC so convert the requested time here
139 cur.execute('SELECT t_stamp, ' + colstr + ' FROM agl WHERE t_stamp > ? AND t_stamp < ? ORDER BY t_stamp', 193 cur.execute('SELECT t_stamp, ' + colstr + ' FROM agl WHERE t_stamp > ? AND t_stamp < ? ORDER BY t_stamp',
140 (start, end)) 194 (start, end))
141 ary = numpy.array(cur.fetchall()) 195 ary = numpy.array(cur.fetchall())
196 if ary.shape[0] == 0:
197 print('No data')
198 return
142 # Convert naive UTC to proper UTC then adjust to local time 199 # Convert naive UTC to proper UTC then adjust to local time
143 xdata = map(lambda f: f.replace(tzinfo = utc).astimezone(lt), ary[:,0]) 200 xdata = map(lambda f: f.replace(tzinfo = utc).astimezone(lt), ary[:,0])
144 for idx in range(len(cols)): 201 for idx in range(len(cols)):
145 if units[cols[idx]] == yaxisunits1: 202 if units[cols[idx]] == yaxisunits1:
146 ax1lines.append([xdata, ary[:,idx + 1], cols[idx], colourlist[colouridx]]) 203 ax1lines.append([xdata, ary[:,idx + 1], names[cols[idx]], colourlist[colouridx]])
147 else: 204 else:
148 ax2lines.append([xdata, ary[:,idx + 1], cols[idx], colourlist[colouridx]]) 205 ax2lines.append([xdata, ary[:,idx + 1], names[cols[idx]], colourlist[colouridx]])
149 colouridx += 1 206 colouridx += 1
150 207
151 fig = matplotlib.pylab.figure() 208 fig = matplotlib.pylab.figure()
152 ax1 = fig.add_subplot(111) 209 ax1 = fig.add_subplot(111)
153 ax1.set_ylabel(yaxisunits1) 210 ax1.set_ylabel(yaxisunits1)
163 220
164 for line in ax2lines: 221 for line in ax2lines:
165 ax2.plot(line[0], line[1], label = line[2], color = line[3]) 222 ax2.plot(line[0], line[1], label = line[2], color = line[3])
166 ax2.legend(loc = 'upper right') 223 ax2.legend(loc = 'upper right')
167 224
168 # Rotate X axis labels 225 ndays = int(max(1, round(((end - start).total_seconds()) / 86400)))
169 for ax in fig.get_axes(): 226 for ax in fig.get_axes():
170 ax.set_xlim([start, end]) 227 ax.set_xlim([start, end])
171 ax.xaxis.grid(True) 228 ax.xaxis.grid(True)
172 ax.xaxis.set_major_formatter(matplotlib.dates.DateFormatter('%d %b\n%H:%M')) 229 ax.xaxis.set_major_formatter(matplotlib.dates.DateFormatter('%d %b\n%H:%M'))
173 ax.xaxis.set_major_locator(matplotlib.dates.HourLocator(interval = 2)) 230 ax.xaxis.set_major_locator(matplotlib.dates.HourLocator(interval = 2 * ndays))
174 ax.xaxis.set_minor_locator(matplotlib.dates.MinuteLocator(interval = 5)) 231 ax.xaxis.set_minor_locator(matplotlib.dates.MinuteLocator(interval = 5 * ndays))
175 for label in ax.get_xticklabels(): 232 for label in ax.get_xticklabels():
176 label.set_ha('center') 233 label.set_ha('center')
177 label.set_rotation(90) 234 label.set_rotation(90)
178 235
179 # Fudge margins to give more graph and less space 236 # Fudge margins to give more graph and less space
205 if reply.status_code != 200: 262 if reply.status_code != 200:
206 return None 263 return None
207 return json.decoder.JSONDecoder().decode(reply.content)['access_token'] 264 return json.decoder.JSONDecoder().decode(reply.content)['access_token']
208 265
209 def getdata(token, startdate, enddate): 266 def getdata(token, startdate, enddate):
267 #print('getting ' + startdate.strftime('%Y-%m-%d'))
210 reply = requests.request('GET', dataurl, params = { 268 reply = requests.request('GET', dataurl, params = {
211 'startDate' : startdate, 269 'startDate' : startdate.strftime('%Y-%m-%d'),
212 'endDate' : enddate, 270 'endDate' : enddate.strftime('%Y-%m-%d'),
213 'granularity' : 'Minute', 271 'granularity' : 'Minute',
214 'metrics' : 'read', 272 'metrics' : 'read',
215 'units' : 'W', 273 'units' : 'W',
216 }, headers = { 'Authorization' : 'Bearer ' + token}) 274 }, headers = { 'Authorization' : 'Bearer ' + token})
217 275