Mercurial > ~darius > hgwebdir.cgi > agl
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 |