· 6 years ago · Apr 17, 2020, 05:28 PM
1#!/usr/bin/python3
2
3import json
4import urllib.request
5import time
6from os.path import isfile
7
8# The easiest way to use this module is just to call ndays() with the
9# number of days you want data for. Note this function may return slightly more
10# results than asked for.
11#
12# group_by_day takes consolidated output (as returned by ndays or consolidate)
13# and merges them into daily figures. Note that consumption and net prices
14# when grouped by day include the daily standing charge.
15#
16# So a simple call would be:
17#
18# octopus.group_by_day(octopus.ndays(7))
19#
20# This will give you up to the most recent 7 days of data by day.
21
22octopus_key = "ENTER YOUR API KEY HERE"
23
24outgoing_price = 5.5
25standing_charge = 21
26
27top_level_url = "https://api.octopus.energy/v1/"
28prices_url = "https://api.octopus.energy/v1/FILL IN THE DATA FROM YOUR DASHBOARD"
29
30outgoing_url = "https://api.octopus.energy/v1/electricity-meter-points/OUTGOING_MPAN/meters/METER_SERIAL_NUMBER/consumption/"
31consumption_url = "https://api.octopus.energy/v1/electricity-meter-points/CONSUMPTION_MPAN/meters/METER_SERIAL_NUMBER/consumption/"
32
33# Use results = octopus.get_list_from_url(*octopus.OUTGOING)
34# | octopus.get_list_from_url(*octopus.CONSUMPTION)
35# | octopus.get_list_from_url(*octopus.PRICES)
36OUTGOING = (outgoing_url, 'interval_start', 'consumption', 'outgoing')
37CONSUMPTION = (consumption_url, 'interval_start', 'consumption', 'consumption')
38PRICES = (prices_url, 'valid_from', 'value_inc_vat', 'prices')
39
40def save_data(filename, data):
41 with open(filename, 'w') as f:
42 json.dump(data, f)
43
44
45def load_data(filename):
46 if not isfile(filename):
47 return None
48 with open(filename, 'r') as f:
49 return json.load(f)
50
51
52def get_list_from_url(url, time_field, value_field, name, limit=48):
53 items = []
54 hard_limit = 48 * 366
55 if limit > hard_limit:
56 limit = hard_limit
57 filename = f'octopus_{name}.json'
58 cached_items = load_data(filename)
59
60 # If we already have data from within the last 44 hours,
61 # don't bother Octopus
62 if cached_items is not None and time.time() - 44*3600 < cached_items[0][0]:
63 return cached_items[0:limit]
64
65 print(f'Looking for new {name} data')
66 passman = urllib.request.HTTPPasswordMgrWithDefaultRealm()
67 passman.add_password(None, top_level_url, octopus_key, '')
68 auth_handler = urllib.request.HTTPBasicAuthHandler(passman)
69 opener = urllib.request.build_opener(auth_handler)
70
71 next_url = url
72 while next_url is not None and len(items) < hard_limit:
73 print('.', end='', flush=True)
74 try:
75 opener.open(next_url)
76 except:
77 print(f"Couldn't read data from {next_url}.")
78 if cached_items is not None:
79 return cached_items[0:limit]
80 else:
81 return None
82
83 urllib.request.install_opener(opener)
84
85 try:
86 with urllib.request.urlopen(next_url, timeout=30) as u:
87 data = u.read()
88 except:
89 print(f"Couldn't read data from {next_url}.")
90 if cached_items is not None:
91 return cached_items[0:limit]
92 else:
93 return None
94
95 try:
96 decoded_data = json.loads(data.decode('utf-8'))
97 except:
98 print("Webserver didn't return valid JSON")
99 if cached_items is not None:
100 return cached_items[0:limit]
101 else:
102 return None
103
104 next_url = decoded_data['next']
105 items += process_list(decoded_data['results'], time_field, value_field)
106
107 if cached_items is not None and items[-1][0] < cached_items[0][0]:
108 # This is data we already have. Let's stop hammering poor Octopus
109 new_items = [i for i in items if i[0] > cached_items[0][0]]
110 items = new_items + cached_items
111 next_url = None
112
113 print('')
114 items.sort(key=lambda x:x[0], reverse=True)
115
116 save_data(filename, items[0:hard_limit])
117
118 return items[0:limit]
119
120
121def process_list(data, time_field, value_field):
122 processed = []
123 for l in data:
124 if l[time_field][19] == 'Z':
125 t = int(time.mktime(time.strptime(l[time_field][0:19] + 'UTC',
126 '%Y-%m-%dT%H:%M:%S%Z')))
127 else:
128 t = int(time.mktime(time.strptime(l[time_field][0:19] + 'BST',
129 '%Y-%m-%dT%H:%M:%S%Z')))
130 processed.append((t, l[value_field]))
131 return processed
132
133
134def consolidate(pri, con, out):
135 consolidated = {}
136 for x in pri:
137 consolidated[x[0]] = [x[1]]
138 for x in con:
139 if x[0] in consolidated:
140 consolidated[x[0]].append(x[1])
141 if out is not None:
142 for x in out:
143 if x[0] in consolidated:
144 consolidated[x[0]].append(x[1])
145 else:
146 for x in consolidated:
147 consolidated[x].append(0)
148
149 consolidated_list = [[x] + consolidated[x] for x in consolidated
150 if len(consolidated[x]) == 3]
151 consolidated_list.sort(key=lambda x:x[0], reverse=True)
152
153 for x in consolidated_list:
154 yield {'time' : x[0],
155 'price' : x[1],
156 'consumption' : x[2],
157 'outgoing' : x[3],
158 'c_price' : x[1] * x[2],
159 'o_price' : x[3] * outgoing_price,
160 'net' : x[1] * x[2] - x[3] * outgoing_price}
161
162
163def ndays(days, include_outgoing=True):
164 limit = days * 48
165 if include_outgoing:
166 power = consolidate(get_list_from_url(*PRICES, limit=limit + 192),
167 get_list_from_url(*CONSUMPTION, limit=limit + 96),
168 get_list_from_url(*OUTGOING, limit=limit + 4))
169 else:
170 power = consolidate(get_list_from_url(*PRICES, limit=limit + 192),
171 get_list_from_url(*CONSUMPTION, limit=limit + 4),
172 None)
173 return power
174
175
176def group_by_day(consolidated):
177 days_bill = {}
178
179 for c in consolidated:
180 day = time.strftime('%d %B', time.localtime(c['time']))
181 days_bill[day] = (c['time'], (days_bill[day][1][0] + c['c_price'],
182 days_bill[day][1][1] + c['net'],
183 days_bill[day][1][2] + c['consumption'],
184 days_bill[day][1][3] + c['outgoing'],
185 days_bill[day][1][4] + 1)
186 if day in days_bill else (c['c_price'], c['net'],
187 c['consumption'],
188 c['outgoing'], 0))
189
190 dbl = [{'time' :days_bill[x][0],
191 'day' : x,
192 'c_price' : days_bill[x][1][0] + standing_charge,
193 'o_price' : days_bill[x][1][3] * outgoing_price,
194 'net' : days_bill[x][1][1] + standing_charge,
195 'consumption' : days_bill[x][1][2],
196 'outgoing' : days_bill[x][1][3]}
197 for x in days_bill if days_bill[x][1][4] > 40]
198 dbl.sort(key=lambda x:x['time'])
199
200 return dbl
201
202if __name__ == '__main__':
203 import matplotlib.pyplot as pyplot
204 power = list(ndays(1))
205
206 power.sort(key=lambda x:x['time'])
207 yday = time.localtime(power[-4]['time']).tm_yday
208 power = [x for x in power if time.localtime(x['time']).tm_yday == yday]
209
210 pyplot.clf()
211 pyplot.grid(1)
212 t = time.localtime(power[0]['time'])
213 ax = [time.localtime(x['time']).tm_hour +
214 time.localtime(x['time']).tm_min / 60
215 for x in power]
216 con = [x['consumption'] for x in power]
217 out = [x['outgoing'] for x in power]
218 price = [x['price'] / 10 for x in power]
219
220 pyplot.plot(ax, con, 'r', label='Consumption (kWh)')
221 pyplot.plot(ax, out, 'g', label='Outgoing (kWh)')
222 pyplot.plot(ax, price, 'k', label='Agile price (pence / 10)')
223 pyplot.title(f'Power for {t.tm_mday}/{t.tm_mon}/{t.tm_year}')
224 pyplot.legend()
225 pyplot.show()