Octopus Energy Agile Tariff

Looks good. How are you pulling the grid intensity and from where? I’ve got a REST sensor setup but I’m not overly happy with it.

I really wish there was a better gauge in HA or Node-RED.

Here’s my sensor. Documentation, including region IDs (pasted below), is here.

- platform: rest
  resource: https://api.carbonintensity.org.uk/regional/regionid/8
  name: "West Midlands carbon intensity"
  unit_of_measurement: 'g/kWh'
  value_template: '{{ value_json.data.0.data.0.intensity.forecast }}'
  scan_interval: 600

Region IDs

1 North Scotland
2 South Scotland
3 North West England
4 North East England
5 South Yorkshire
6 North Wales, Merseyside and Cheshire
7 South Wales
8 West Midlands
9 East Midlands
10 East England
11 South West England
12 South England
13 London
14 South East England

15 England
16 Scotland
17 Wales

1 Like

Thanks - almost exactly the same.

Have you found anywhere that tells you the boundaries for their very low / low / medium etc values?

I pull off the intensity as the state and the forecast as an attribute.

- platform: rest
  name: Carbon Intensity
  resource: https://api.carbonintensity.org.uk/regional/regionid/2
  value_template: '{{ value_json.data[0].data[0].intensity.index }}'
  json_attributes_path: "$.data[0].data[0].intensity"
  json_attributes:
    - "forecast"

TIMTOWTDI

I haven’t, no. I’d imagine this is deliberate on their part to allow them to pull the category thresholds about as more renewables come online and relative performance changes.

For example, the average for 2012 was over 500g/kWh, and this was down to under 200g/kWh in 2019. Progress on the UK grid has been phenomenal and generally under-reported.

1 Like

I’ve added this in now. Setting hours to 0 with start_period set to now in apps.yaml will return the current import or export price. And set sensors sensor.octopus_current_price or sensor.octopus_export_current_price as appropriate.

1 Like

Here’s my script for working out the cheapest n-block cost, which I call octoplan.py. Seems to do broadly the same as @badguy’s octoblock but without needing appdaemon. It’s a bit less “integrated” as a result, of course: it’s read with a command_line sensor to give the cheapest times display as per my screenshot above; and also queried with a bash script (run via a shell_command automation) that writes the immersion start time to a text file that HA reads with another command_line sensor. Clunky, but I understand what it’s doing.

The from and to arguments need to be in recognizable time formats, and it’ll probably fail in all sorts of horrible ways if you feed it invalid arguments.

My most recent addition is the latest flag, which I use for the immersion schedule. If there are two or more periods with exactly the same cost in the time span being evaluated, the default is to return the earliest. If you specify --latest, it’ll instead return the latest block which makes more sense for an overnight immersion run.

The first line refers to octopus_lib. This is Octopus’s David Winterbottom’s client.py from here which I have downloaded and renamed octopus_lib.py to give me half a chance of remembering what it is in three years’ time. You will need to ensure you have installed the Python requests library as well as the imports below (dateutil is installed as py-dateutil).

The final section of the script generates an output list of time/cost dictionaries of cheapest times averaged over 30m in output[0] to 4h in output[7]. The final line prints out the JSON for 30m, 1h, 2h and 4h only, but you can select others if needed.

If you use it and it breaks, you get to keep all the pieces. Please be considerate in querying Octopus’s API: I run this half-hourly via an automation trigger (see earlier posts).

from octopus_lib import APIClient                                               
import argparse                                                                 
import datetime                                                                 
import json                                                                     
from dateutil import parser                                                     
from dateutil.tz import tzlocal                                                 

APIKEY = 'YOUR_API_KEY'
REGIONCODE = 'E'                 # replace with your region code

argparser = argparse.ArgumentParser(description='Finds cheapest Agile chunks')  

argparser.add_argument('-f',                                                    
                       '--from',                                                
                       help='Start time for search',                            
                       required=False)                                          
argparser.add_argument('-t',                                                    
                       '--to',                                                  
                       help='End time for search',                              
                       required=False)                                          
argparser.add_argument('-l',                                                    
                       '--latest',                                              
                       help='Find latest rather than earliest occurrence',      
                       action='store_true',                                     
                       required=False)                                          

args = vars(argparser.parse_args())                                             

from_ts = None                                                                  
to_ts = None                                                                    

if args['from'] is not None:                                                    
    from_ts = parser.parse(args['from']).astimezone(tzlocal())                  

if args['to'] is not None:                                                      
    to_ts = parser.parse(args['to']).astimezone(tzlocal())                      

latest = args['latest']                                                         

# pull in the rates                                                             
octo = APIClient(APIKEY)                                                        
now = datetime.datetime.now()                                                   
rates = octo.agile_tariff_unit_rates(REGIONCODE, period_from=now)                      
# API returns rates as latest-first, fix that...                                
rates['results'].reverse()                                                      

# Extract costs and times into lists                                            
costs = []                                                                      
times = []                                                                      
for result in rates['results']:                                                 
    period_str = result['valid_from']                                           
    time_ts = parser.parse(timestr=period_str).astimezone(tzlocal())            
    if from_ts is not None and time_ts < from_ts:                               
        # start specified and not yet reached: continue searching               
        continue                                                                
    if to_ts is not None and time_ts >= to_ts:                                  
        # end specified and reached: break out                                  
        break                                                                   
    times.append(time_ts)                                                       
    costs.append(result['value_inc_vat'])                                       

# Build a matrix of half-hour periods (vertical) and 1-8 averaging periods      
# (horizontal) (so second column is hour average costs)                         
cost_matrix = []                                                                

for x in range(len(costs)):                                                     
    cost_row = [costs[x]]                                                       
    for y in range(1, 8):                                                       
        if len(costs) - x < y + 1:                                              
            # table ends before averaging period                                
            cost_row.append(None)                                               
        else:                                                                   
            cost_total = 0                                                      
            for z in range(y + 1):                                              
                cost_total = cost_total + costs[x + z]                          
            cost_row.append(round(cost_total / (z + 1), 2))                     
    cost_matrix.append(cost_row)                                                

output = []                                                                     
for x in range(8):                                                              
    # Build column, find minimum                                                
    cost_col = []                                                               
    for y in range(len(costs)):                                                 
        if cost_matrix[y][x] is not None:                                       
            cost_col.append(cost_matrix[y][x])                                  

    if latest:                                                                  
        mindex = len(cost_col) - 1 - cost_col[::-1].index(min(cost_col))        
    else:                                                                       
        mindex = cost_col.index(min(cost_col))                                  

    output.append({"time": "%s" % times[mindex].isoformat(),                    
                   "cost": "%.2f" % cost_col[mindex]})                          

# output a list for cheapest 30m, 1h, 2h and 4h                                 
print(json.dumps([output[0], output[1], output[3], output[7]]))                                                     
1 Like

Just added this in but get the following error:

2020-04-13 10:04:44.829543 WARNING octo_block_now: Traceback (most recent call last):
  File "/usr/lib/python3.8/site-packages/appdaemon/threading.py", line 766, in worker
    funcref(self.AD.sched.sanitize_timer_kwargs(app, args["kwargs"]))
  File "/config/appdaemon/apps/octoblock/octoblock.py", line 42, in period_and_cost_callback
    elif start_period == 'now':
  File "/config/appdaemon/apps/octoblock/octoblock.py", line 94, in get_period_and_cost
ZeroDivisionError: division by zero

@james_hiscott I’m hoping that is just a transient issue due to the way the apps restart and threading in appdaemon, as there is no way the new code should be able to get there if hours = 0.

yup, restart of appdaemon fixed it. Should of tired that first. :+1:

PS. that 1.7 update is a very nice idea. Thanks for all the hard work on this, it is the backbone of lots of my automatons now

1 Like

Been reading this topic with interest because I’m thinking about getting an EV and I’m already with octopus (albeit a normal plan), I was wondering if there’s anyway to dynamically get the best n hours?

I’m thinking about the following potential scenario. I know my ev needs so much power to fill which I dynamically work out means today I need 3 hours charge, then I could query your component to find the cheapest 3 hour charge today. But tomorrow it could be 2 hours etc. Possible?

With the node I wrote, yes you can specify the window size. Just always request a number of windows, they are returned as an array.

However, one of the gotchas currently is that the assumption is the cheapest window will be overnight. Recently (in the UK) that has not been the case.

I really need to add some additional logic so you can specify the window within which you want the cheapest period window, so between 8pm and 8am when the car is home for instance. Currently with the cheapest in the afternoon, the car would not charge overnight!

That’s what my script a couple of posts up does. You can supply the from and to times (within “now” to the end of the available data), and it returns a list of 1–8 block (30m to 4h) cheapest periods within those times, assuming equal power consumption across the window. In reality, an EV will slow its charge down as it nears 100%.

1 Like

Hi all - relative new comer to HA - I’ve been gradually been migrating my domoticz setup and now i’m looking to add Octopus usage data to my new dashboard.

I’m currently pursuing the Octocost option but getting stuck right at the end.

Over the last few days I’ve installed the SSH module, HACS, APPDaemon and installed the Octocost app through HACS and configured my Octopus API settings in Apps.yaml all without issue. But for some reason, I’m not seeing any of the sensor entities show up.

There’s nothing obvious in the log.

Until now, all my integrations have shown up automatically, and that’s what I was expecting having added my API settings and restarted the server, but nothing - no Octopus entities in config/entities at all.

The last step in the Octocost readme shows the attached - as a newcomer I thought this was just a markdown way of illustrating what lovelace UI should be rendering automatically but am I supposed to do something manually? I tried it in configuration.yaml but from the error messages - that is obviously not the correct place for it.

Many thanks

Hi @mcon if you go to developer tools > states and filter on octopus, do you see anything?

Hey @badguy

No i don’t see anything in the list if I do that

Check the AppDaemon logs.

Hey @baz123 - thanks hadn’t thought to look into AppDaemon stuff at all

Logs themselves are empty but the ‘state’ column for the Octocost app shows initialize_error

1 Like

Can you restart AppDaemon, then refresh the AppDaemon logs and see if they have anything useful in. Also could you post your apps.yaml configuration for Octocost, with sensitive information (MPAN, Serial number, API auth key) redacted

1 Like

As @badguy says, if you restart AppDaemon and refresh the logs, you will probably see an error message (if there is one). Need to keep refreshing - doesn’t auto refresh.

getting somewhere now

Logs show this


2020-04-24 10:44:09.362206 WARNING octocost: ------------------------------------------------------------
2020-04-24 10:44:09.360828 WARNING octocost: Traceback (most recent call last):
  File "/usr/lib/python3.8/site-packages/appdaemon/app_management.py", line 145, in initialize_app
    await utils.run_in_executor(self, init)
  File "/usr/lib/python3.8/site-packages/appdaemon/utils.py", line 276, in run_in_executor
    response = future.result()
  File "/usr/lib/python3.8/concurrent/futures/thread.py", line 57, in run
    result = self.fn(*self.args, **self.kwargs)
  File "/config/appdaemon/apps/octocost/octocost.py", line 25, in initialize
    self.run_every(self.cost_and_usage_callback, time, 120 * 60)
  File "/usr/lib/python3.8/site-packages/appdaemon/utils.py", line 191, in inner_sync_wrapper
    f = run_coroutine_threadsafe(self, coro(self, *args, **kwargs))
  File "/usr/lib/python3.8/site-packages/appdaemon/utils.py", line 285, in run_coroutine_threadsafe
    result = future.result(self.AD.internal_function_timeout)
  File "/usr/lib/python3.8/concurrent/futures/_base.py", line 439, in result
    return self.__get_result()
  File "/usr/lib/python3.8/concurrent/futures/_base.py", line 388, in __get_result
    raise self._exception
  File "/usr/lib/python3.8/site-packages/appdaemon/adapi.py", line 2476, in run_every
    raise ValueError("start cannot be in the past")
ValueError: start cannot be in the past

2020-04-24 10:44:09.353797 WARNING octocost: ------------------------------------------------------------
2020-04-24 10:44:09.352734 WARNING octocost: Unexpected error running initialize() for octocost
2020-04-24 10:44:09.351262 WARNING octocost: ------------------------------------------------------------

and the apps.yaml

octocost:
  module: octocost 
  class: OctoCost 
  region: J
  mpan: <my mpan is here>
  serial: <my serial is here>
  auth: <my octo Api key is here>
  startdate: 2020-05-05

Tried the start date in january and in may in case the error was about that.

1 Like