Mercurial > ~darius > hgwebdir.cgi > agl
comparison agl.py @ 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 |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:8d6ba11c1b76 |
---|---|
1 #!/usr/bin/env python | |
2 | |
3 import ConfigParser | |
4 import datetime | |
5 import exceptions | |
6 import json | |
7 import os | |
8 import pytz | |
9 import requests | |
10 import sqlite3 | |
11 import sys | |
12 | |
13 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' | |
15 # ?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' | |
17 | |
18 class UTC(datetime.tzinfo): | |
19 def utcoffset(self, dt): | |
20 return datetime.timedelta(0) | |
21 | |
22 def dst(self, dt): | |
23 return datetime.timedelta(0) | |
24 | |
25 def tzname(self, dt): | |
26 return "UTC" | |
27 | |
28 | |
29 def main(): | |
30 conf = ConfigParser.ConfigParser() | |
31 confname = os.environ['HOME'] + '/.agl.ini' | |
32 conf.read(confname) | |
33 username = conf.get('DEFAULT', 'username') | |
34 password = conf.get('DEFAULT', 'password') | |
35 dbfn = conf.get('DEFAULT', 'db') | |
36 | |
37 if conf.has_option('DEFAULT', 'token'): | |
38 token = conf.get('DEFAULT', 'token') | |
39 else: | |
40 token = gettoken(username, password) | |
41 conf.set('DEFAULT', 'token', token) | |
42 conf.write(file(confname, 'w')) | |
43 | |
44 if len(sys.argv) > 1: | |
45 date = sys.argv[1] | |
46 else: | |
47 date = datetime.datetime.now().strftime('%Y-%m-%d') | |
48 | |
49 dbh = sqlite3.connect(dbfn, detect_types = sqlite3.PARSE_DECLTYPES) | |
50 cur = dbh.cursor() | |
51 data = getdata(token, date, date) | |
52 if data == None: | |
53 token = gettoken(username, password) | |
54 data = getdata(token, date, date) | |
55 if data == None: | |
56 print('Unable to fetch data') | |
57 updatedb(cur, data) | |
58 dbh.commit() | |
59 | |
60 def mkdb(cur): | |
61 cur.execute(''' | |
62 CREATE TABLE IF NOT EXISTS agl ( | |
63 t_stamp TIMESTAMP PRIMARY KEY, | |
64 battery_charge NUMBER, | |
65 battery_power NUMBER, | |
66 power_consumed NUMBER, | |
67 power_expected NUMBER, | |
68 power_exported NUMBER, | |
69 power_generated NUMBER, | |
70 power_imported NUMBER, | |
71 estimated_savings NUMBER, | |
72 pv_forecast NUMBER, | |
73 pv_gen_battery NUMBER, | |
74 pv_gen_grid NUMBER, | |
75 pv_gen_site NUMBER, | |
76 site_cons_battery NUMBER, | |
77 site_cons_grid NUMBER, | |
78 site_cons_pv NUMBER | |
79 )''') | |
80 | |
81 units = { | |
82 'battery_charge' : '%', | |
83 'battery_power' : 'Watt', | |
84 'power_consumed' : 'Watt', | |
85 'power_expected' : 'Watt', | |
86 'power_exported' : 'Watt', | |
87 'power_generated' : 'Watt', | |
88 'power_imported' : 'Watt', | |
89 'estimated_savings' : '$', | |
90 'pv_forecast' : 'Watt', | |
91 'pv_gen_battery' : 'Watt', | |
92 'pv_gen_grid' : 'Watt', | |
93 'pv_gen_site' : 'Watt', | |
94 'site_cons_battery' : 'Watt', | |
95 'site_cons_grid' : 'Watt', | |
96 'site_cons_pv' : 'Watt' | |
97 } | |
98 def graph(cur, cols, start, end): | |
99 import numpy | |
100 import matplotlib | |
101 import matplotlib.dates | |
102 import matplotlib.pylab | |
103 | |
104 #matplotlib.rcParams['timezone'] = pytz.timezone('Australia/Adelaide') | |
105 | |
106 colourlist = ['b','g','r','c','m','y','k'] | |
107 yaxisunits1 = None | |
108 yaxisunits2 = None | |
109 ax1lines = [] | |
110 ax2lines = [] | |
111 colouridx = 0 | |
112 for col in cols: | |
113 unit = units[col] | |
114 if yaxisunits1 == None: | |
115 yaxisunits1 = unit | |
116 if yaxisunits2 == None: | |
117 if unit != yaxisunits1: | |
118 yaxisunits2 = unit | |
119 else: | |
120 if unit != yaxisunits1 and unit != yaxisunits2: | |
121 raise exceptions.Exception('Asked to graph >2 different units') | |
122 | |
123 cur.execute('SELECT t_stamp, ' + reduce(lambda a, b: a + ', ' + b, cols) + ' FROM agl WHERE t_stamp > ? AND t_stamp < ? ORDER BY t_stamp', | |
124 (start, end)) | |
125 ary = numpy.array(cur.fetchall()) | |
126 for idx in range(len(cols)): | |
127 if units[cols[idx]] == yaxisunits1: | |
128 ax1lines.append([ary[:,0], ary[:,idx + 1], cols[idx], colourlist[colouridx]]) | |
129 else: | |
130 ax2lines.append([ary[:,0], ary[:,idx + 1], cols[idx], colourlist[colouridx]]) | |
131 colouridx += 1 | |
132 | |
133 fig = matplotlib.pylab.figure() | |
134 ax1 = fig.add_subplot(111) | |
135 ax1.set_ylabel(yaxisunits1) | |
136 | |
137 for line in ax1lines: | |
138 ax1.plot(line[0], line[1], label = line[2]) | |
139 | |
140 ax1.legend(loc = 'upper left') | |
141 | |
142 if yaxisunits2 != None: | |
143 ax2 = ax1.twinx() | |
144 ax2.set_ylabel(yaxisunits2) | |
145 | |
146 for line in ax2lines: | |
147 ax2.plot(line[0], line[1], label = line[2], color = line[3]) | |
148 ax2.legend(loc = 'upper right') | |
149 | |
150 # Rotate X axis labels | |
151 for ax in fig.get_axes(): | |
152 ax.xaxis.set_major_formatter(matplotlib.dates.DateFormatter('%H:%M')) | |
153 ax.xaxis.set_major_locator(matplotlib.dates.HourLocator(interval = 2)) | |
154 ax.xaxis.set_minor_locator(matplotlib.dates.MinuteLocator(interval = 5)) | |
155 for label in ax.get_xticklabels(): | |
156 label.set_ha('right') | |
157 label.set_rotation(30) | |
158 | |
159 # Fudge margins to give more graph and less space | |
160 fig.subplots_adjust(left = 0.10, right = 0.88, top = 0.95, bottom = 0.15) | |
161 matplotlib.pyplot.show() | |
162 | |
163 def updatedb(cur, data): | |
164 mkdb(cur) | |
165 for d in data['reads']['data']: | |
166 ts = datetime.datetime.strptime(d['t_stamp'], '%Y-%m-%dT%H:%M:%SZ') | |
167 # Note we rename *energy* to *power* here to match what it actually means | |
168 vals = [ts, d['battery_charge'], d['battery_energy'], d['energy_consumed'], d['energy_expected'], d['energy_exported'], d['energy_generated'], | |
169 d['energy_imported'], d['estimated_savings'], d['pv_forecast'], d['pv_generation']['battery_energy'], | |
170 d['pv_generation']['grid_energy'], d['pv_generation']['site_energy'], d['site_consumption']['battery_energy'], | |
171 d['site_consumption']['grid_energy'], d['site_consumption']['pv_energy']] | |
172 skip = True | |
173 for v in vals[1:]: | |
174 if v != None: | |
175 skip = False | |
176 break | |
177 if skip: | |
178 print('Skipping empty record at ' + str(ts)) | |
179 continue | |
180 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) | |
181 | |
182 def gettoken(username, password): | |
183 authblob = json.encoder.JSONEncoder().encode({'email' : username, 'password' : password}) | |
184 reply = requests.request('POST', loginurl, data = authblob, headers = {'Content-Type' : 'application/json'}) | |
185 if reply.status_code != 200: | |
186 return None | |
187 return json.decoder.JSONDecoder().decode(reply.content)['access_token'] | |
188 | |
189 def getdata(token, startdate, enddate): | |
190 reply = requests.request('GET', dataurl, params = { | |
191 'startDate' : startdate, | |
192 'endDate' : enddate, | |
193 'granularity' : 'Minute', | |
194 'metrics' : 'read', | |
195 'units' : 'W', | |
196 }, headers = { 'Authorization' : 'Bearer ' + token}) | |
197 | |
198 if reply.status_code != 200: | |
199 return None | |
200 | |
201 return json.decoder.JSONDecoder().decode(reply.content) | |
202 | |
203 def logout(token): | |
204 reply = requests.request('GET', logouturl, headers = { 'Authorization' : 'Bearer ' + token}) | |
205 return reply.status_code == 200 | |
206 | |
207 if __name__ == '__main__': | |
208 main() |