Hi all, following on from a script I created here, I’ve updated it to generate a csv “report” with a summary at the end that can be used to compare with a bill, and to show how much difference between your bill with solar, and what your bill might be from your consumption.
It handles time of use billing (using the same logic as the flexible TOU app I created a few years ago) and requires you to have 4 specific feeds:
- A feed that contains your accumulated raw consumption in kWh
- A feed that contains your accumulated raw generation in kWh
- A feed that contains your accumulated import kWh (in my case, I get this by subtracting consumption from generation and only logging absolute value of any negative values)
- A feed that contains your accumulated export kWh (in my case, I get this by subtracting consumption from generation and logging only positive values)
I don’t have a “controlled load” (off-peak hot water) feed anymore, so I didn’t include that in the calculations. My controlled load circuit is on its own meter (as they would normally be), so I can’t use self generation to feed it and I’m always going to be billed for 100% of it. As such, its a constant that I have to mentally add when comparing with my bill.
You need to edit the top of the file and replace values where indicated with values appropriate to your EmonCMS instance, and it produces an output like this:
day,kWh-Cons,kWh-Imp,kWh-Gen,kWh-Exp,$-Cons,$-Imp,$-Exp
20201205,30.188,13.486,31.367,14.667,9.729,3.689,2.493
20201206,37.465,14.122,57.875,34.532,11.502,2.503,5.870
20201207,28.984,13.154,53.028,37.198,8.065,2.942,6.324
20201208,22.855,7.464,60.738,45.346,6.357,1.285,7.709
20201209,21.051,6.942,57.663,43.555,5.533,1.114,7.404
20201210,24.832,9.379,55.046,39.592,6.951,1.903,6.731
20201211,22.965,12.253,17.829,7.118,5.901,2.431,1.210
20201212,26.801,8.703,51.486,33.389,8.076,1.373,5.676
20201213,29.918,12.503,45.054,27.637,9.043,2.729,4.698
...
20210830,27.398,22.431,33.776,28.808,6.751,5.190,4.897
20210831,24.945,19.489,41.741,36.285,5.631,3.916,6.168
20210901,19.839,15.312,41.240,36.715,4.581,3.118,6.241
Totals: Generation = 8159.432 kWh, Consumption = 8499.511 kWh, Import = 6165.657, Export = 5825.096
Consumption = $2171.25
Import = $1366.22
Export = $990.27
Supply = $235.77
No-solar bill (Consumption + Supply) = $2407.02
Bill (Import - Export + Supply) = $611.72
The header row explains the values for each day of the date range requested, followed by a summary showing the total kWh values for Generation, Consumption, Import and Export followed by some cost comparison data, which is (hopefully) self-explanatory.
From my output above, I can see that in the 9 months of data I processed, it has cost me $611.72 with solar instead of $2407.02 if I had consumed the same electricity without solar.
The script uses Python3, and needs the requests
module installed (instructions at
Installation of Requests — Requests 2.26.0 documentation)
Here’s the script, copy this into a file - in my case I’ve called it solarsaving-tou.py
and then you can run it with:
pi@emon-pi:~ $ ./solarsaving-tou.py -s 2020-12-05 -e 2021-09-01
if you don’t specify the -e <end date>
it uses now()
and if you don’t specify a -s <start date>
, it uses the 1st of the current month.
#!/usr/bin/env python3
import datetime
import sys
import getopt
import json
import requests
import time
emon_readKey = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXX' # Enter readAPIkey here
emon_url = 'http://emon-pi.local/feed/data.json'
feed_id_con = XX # kWh feed of consumed energy
feed_id_gen = XX # kWh feed of generated energy
feed_id_imp = XX # kWh feed of imported energy
feed_id_exp = XX # kWh feed of exported energy
# These should be the various tarrifs charged by your retailer
tarrif_rates = [0.1422, 0.2155, 0.4993] # CHANGE THESE AS REQUIRED
# If you have solar, update this with your feed-in rate
tarrif_feedin = 0.17 # CHANGE THIS AS REQUIRED
daily_supply = 0.87 # CHANGE THIS AS REQUIRED
# update these with the index into the rates above, starting at 0
# First is the weekday times
tarrif_times_wd = [
0, # 00:00 - 00:59
0, # 01:00 - 01:59
0, # 02:00 - 02:59
0, # 03:00 - 03:59
0, # 04:00 - 04:59
0, # 05:00 - 05:59
0, # 06:00 - 06:59
1, # 07:00 - 07:59
1, # 08:00 - 08:59
1, # 09:00 - 09:59
1, # 10:00 - 10:59
1, # 11:00 - 11:59
1, # 12:00 - 12:59
1, # 13:00 - 13:59
2, # 14:00 - 14:59
2, # 15:00 - 15:59
2, # 16:00 - 16:59
2, # 17:00 - 17:59
2, # 18:00 - 18:59
2, # 19:00 - 19:59
1, # 20:00 - 20:59
1, # 21:00 - 21:59
0, # 22:00 - 22:59
0 # 23:00 - 23:59
]
# now for the weekend/pub holiday times
tarrif_times_we = [
0, # 00:00 - 00:59
0, # 01:00 - 01:59
0, # 02:00 - 02:59
0, # 03:00 - 03:59
0, # 04:00 - 04:59
0, # 05:00 - 05:59
0, # 06:00 - 06:59
1, # 07:00 - 07:59
1, # 08:00 - 08:59
1, # 09:00 - 09:59
1, # 10:00 - 10:59
1, # 11:00 - 11:59
1, # 12:00 - 12:59
1, # 13:00 - 13:59
1, # 14:00 - 14:59
1, # 15:00 - 15:59
1, # 16:00 - 16:59
1, # 17:00 - 17:59
1, # 18:00 - 18:59
1, # 19:00 - 19:59
1, # 20:00 - 20:59
1, # 21:00 - 21:59
0, # 22:00 - 22:59
0 # 23:00 - 23:59
]
### Nothing to change below here
try:
opts, args = getopt.getopt(sys.argv[1:], "s:e:", ["start=","end="])
except getopt.GetoptError:
print('dailycost.py -s <startdate> -e <enddate>')
sys.exit(2)
for opt, arg in opts:
if opt in ("-s", "--start"):
start = arg
elif opt in ("-e", "--end"):
end = arg
# lets do this month's costs per day unless specified
if 'start' in locals():
start_dt = datetime.datetime.fromisoformat(start)
else:
start_dt = datetime.datetime.today().replace(day=1,hour=0,minute=59,second=59,microsecond=0)
# Start at 23:59 on the previous day, that gives us a starting value and then values up to hh:59:59
# for each hour period - this makes the claculations easier
start_dt = (start_dt + datetime.timedelta(hours=-1)).timestamp() * 1000
if 'end' in locals():
end_dt = datetime.datetime.fromisoformat(end).timestamp() * 1000
else:
end_dt = datetime.datetime.now().timestamp() * 1000
def calc_costs(feed_id):
costs = {}
payload = { 'apikey': emon_readKey, 'id': feed_id, 'start': start_dt, 'end': end_dt, 'interval': 3600 }
try:
r = requests.get(emon_url, params=payload)
if r.status_code != 200:
print('HTTP response code is [' + str(r.status_code) + ']')
else:
last_kwh = 0
last_day = 0
day_total_kwh = 0
day_total_cost = 0
# need to iterate through the values returned, the timestamp is for the START of the period
# so we need to drop everything back by 1 hr.
for val in r.json():
if last_kwh == 0:
# Just starting
last_kwh = val[1]
else:
val_date = datetime.datetime.fromtimestamp(val[0]/1000)
day = val_date.strftime("%Y%m%d")
if day != last_day:
costs[last_day] = (day_total_kwh, day_total_cost)
day_total_kwh = 0
day_total_cost = 0
last_day = day
dow = val_date.strftime("%w") # day of Week, Sunday is 0
hr = int(val_date.strftime("%H"))
interval_kwh = val[1] - last_kwh
last_kwh = val[1]
day_total_kwh += interval_kwh
if dow == 0 or dow == 6:
# Use the weekend rates
kwh_rate = tarrif_rates[tarrif_times_we[hr]]
else:
# use the weekday rates
kwh_rate = tarrif_rates[tarrif_times_wd[hr]]
hr_cost = kwh_rate * interval_kwh
day_total_cost += hr_cost
#print(day, '-', hr, ': ', interval_kwh, ' $', hr_cost, '[$', kwh_rate, '/kWh]')
costs[last_day] = (day_total_kwh, day_total_cost)
except ValueError as err:
print('Caught http exception: ' + str(err))
r.close()
return costs
def calc_gen(feed_id):
generation = {}
payload = { 'apikey': emon_readKey, 'id': feed_id, 'start': start_dt, 'end': end_dt, 'interval': 86400 }
try:
r = requests.get(emon_url, params=payload)
if r.status_code != 200:
print('HTTP response code is [' + str(r.status_code) + ']')
else:
last_kwh = 0
last_day = 0
day_total_kwh = 0
# need to iterate through the values returned, the timestamp is for the START of the period
# so we need to drop everything back by 1 hr.
for val in r.json():
if last_kwh == 0:
# Just starting
last_kwh = val[1]
else:
val_date = datetime.datetime.fromtimestamp(val[0]/1000)
day = val_date.strftime("%Y%m%d")
if day != last_day:
generation[last_day] = day_total_kwh
day_total_kwh = 0
last_day = day
interval_kwh = val[1] - last_kwh
last_kwh = val[1]
day_total_kwh += interval_kwh
generation[last_day] = day_total_kwh
except ValueError as err:
print('Caught http exception: ' + str(err))
r.close()
return generation
def calc_rebate(feed_id):
rebate = {}
payload = { 'apikey': emon_readKey, 'id': feed_id, 'start': start_dt, 'end': end_dt, 'interval': 86400 }
try:
r = requests.get(emon_url, params=payload)
if r.status_code != 200:
print('HTTP response code is [' + str(r.status_code) + ']')
else:
last_kwh = 0
last_day = 0
day_total_kwh = 0
day_total_cost = 0
# need to iterate through the values returned, the timestamp is for the START of the period
# so we need to drop everything back by 1 hr.
for val in r.json():
if last_kwh == 0:
# Just starting
last_kwh = val[1]
else:
val_date = datetime.datetime.fromtimestamp(val[0]/1000)
day = val_date.strftime("%Y%m%d")
if day != last_day:
rebate[last_day] = (day_total_kwh, day_total_cost)
day_total_kwh = 0
day_total_cost = 0
last_day = day
interval_kwh = val[1] - last_kwh
last_kwh = val[1]
day_total_kwh += interval_kwh
kwh_rate = tarrif_feedin
hr_cost = kwh_rate * interval_kwh
day_total_cost += hr_cost
rebate[last_day] = (day_total_kwh, day_total_cost)
except ValueError as err:
print('Caught http exception: ' + str(err))
r.close()
return rebate
imp = calc_costs(feed_id_imp)
con = calc_costs(feed_id_con)
#print(con_costs)
gen = calc_gen(feed_id_gen)
exp = calc_rebate(feed_id_exp)
print("day,kWh-Cons,kWh-Imp,kWh-Gen,kWh-Exp,$-Cons,$-Imp,$-Exp")
imp_cost = 0
con_cost = 0
exp_reb = 0
con_kWh = 0
gen_kWh = 0
imp_kWh = 0
exp_kWh = 0
supply = 0
for k in imp:
if k != 0:
print(f'{k},{con[k][0]:.3f},{imp[k][0]:.3f},{gen[k]:.3f},{exp[k][0]:.3f},{con[k][1]:.3f},{imp[k][1]:.3f},{exp[k][1]:.3f}')
imp_cost += imp[k][1]
con_cost += con[k][1]
exp_reb += exp[k][1]
con_kWh += con[k][0]
imp_kWh += imp[k][0]
gen_kWh += gen[k]
exp_kWh += exp[k][0]
supply += daily_supply
print()
print(f'Totals: Generation = {gen_kWh:.3f} kWh, Consumption = {con_kWh:.3f} kWh, Import = {imp_kWh:.3f}, Export = {exp_kWh:.3f}')
print(f' Consumption = ${con_cost:.2f}')
print(f' Import = ${imp_cost:.2f}')
print(f' Export = ${exp_reb:.2f}')
print(f' Supply = ${supply:.2f}')
print(f' No-solar bill (Consumption + Supply) = ${(con_cost + supply):.2f}')
print(f' Bill (Import - Export + Supply) = ${(imp_cost - exp_reb + supply):.2f}')
The tarrif rates are specified in two lists, where each value represents the index into the tarrif_rates
list for the hour period specified.
For example, assuming these tarrif rates: tarrif_rates = [0.1422, 0.2155, 0.4993]
this part:
tarrif_times_wd = [
0, # 00:00 - 00:59
...
1, # 07:00 - 07:59
...
tells the code that on weekdays from 00:00 to 00:59, use the 0.1422
cost per kWh (in $) rate, as it is index 0
in the tarrif_rates
list, and from 07:00 to 07:59, use the 0.2155
cost per kWh (in $) rate, as it is index 1
in the tarrif_rates
list.
There’s a separate list for weekends in the code below it.
The provided tarrif_times_wd
and tarrif_times_we
lists are probably right for any NSW, Australia customers, and you’ll just need to adjust the tarrif_rates
list - which is defined as off-peak, shoulder, peak as the tarrif_times_*
lists currently stand.
Hopefully someone else will find this useful.