Time Of Use solar comparison script

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:

  1. A feed that contains your accumulated raw consumption in kWh
  2. A feed that contains your accumulated raw generation in kWh
  3. 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)
  4. 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.

2 Likes

Thanks @Greebo. I am looking forward to trying this out. I tried running the $ python -m pip install requests command, but with no luck. I assume that I have to get the source code onto my EmonPi as well ( I thought that Python was already installed as part of the emonsd package), but wanted to check with you before fiddling around with my EmonPi. Apologies for this question, which reveals the world of programming ignorance that I inhabit. Time Of Use app in use and much appreciated.

You’ll need the requests package for python3, so unless you’ve changed the default python to python3, you’ll need to use python3 to install it.

On my Pi, I used the following command to install it:

sudo pip3 install requests