Octopus Energy Agile Tariff

I’m on a roll :grinning:

Generates 2 sensors, one the current price, the next the minimum value in next 24Hrs

sensor:
  - platform: rest
    name: Octopus
    resource_template: >-
      https://api.octopus.energy/v1/products/AGILE-18-02-21/electricity-tariffs/E-1R-AGILE-18-02-21-N/standard-unit-rates/?period_from=
      {% set ts_now = ((as_timestamp(now())/1800)|round(0,'floor')|int * 1800) %}
      {{ ts_now |timestamp_custom ('%Y-%m-%dT%H:%M:%SZ') }}&period_to=
      {{ (ts_now + 24*60*60) |timestamp_custom ('%Y-%m-%dT%H:%M:%SZ') }}
    value_template: 'OK'
    json_attributes:
      - "results"
  - platform: template
    sensors:
      octopus_current_price: 
        unit_of_measurement: 'p/kWh'
        value_template: "{{ states.sensor.octopus.attributes.results|map(attribute='value_inc_vat')|list|last|round(2) }}"
      octopus_min_price: 
        unit_of_measurement: 'p/kWh'
        value_template: "{{ states.sensor.octopus.attributes.results|map(attribute='value_inc_vat')|list|min|round(2) }}"
4 Likes

I’ve put some code together that finds the cheapest “n” hour block and works out the price. It works with https://www.home-assistant.io/docs/ecosystem/appdaemon/

Once AppDaemon is installed, edit /config/appdaemon/apps.yaml - the module and class sections need to remain as below, but the title, and number of hours can be set as you like.
It is only coded to work with whole hours, not halves, i.e. not 2.5, but you can have as many different ones as you need. Also I haven’t considered daylight savings changes, so I’m not sure how that will work at the end of the month.

octo_block_2hour:
  module: octoblock
  class: OctoBlock
  auth: <your Octopus API key>
  hour: 2

octo_block_3hour:
  module: octoblock
  class: OctoBlock
  auth: <your Octopus API key>
  hour: 3

Create the file /config/appdaemon/apps/octoblock.py with the following in, if you are not in region “H” you will need to update the “E-1R-AGILE-18-02-21-H” part of the url that is pulled back with the prices.

import appdaemon.plugins.hass.hassapi as hass
import requests
import json
import datetime

class OctoBlock(hass.Hass):
    def initialize(self):
        time = datetime.datetime.now()
        time = time + datetime.timedelta(seconds=5)
        self.run_every(self.get_best_period_and_cost, time, 30 * 60)

    def get_best_period_and_cost(self, kwargs):
        hours = self.args['hour']
        auth = self.args['auth']

        d = datetime.datetime.now().isoformat()
        r = requests.get('https://api.octopus.energy/v1/products/AGILE-18-02-21/electricity-tariffs/E-1R-AGILE-18-02-21-H/standard-unit-rates/?period_from=' + d, auth=(auth, ''))

        tariff = json.loads(r.text)
        tariffresults = tariff[u'results']
        tariffresults.reverse()

        blocks = hours * 2

        for period in tariffresults:
            curridx = tariffresults.index(period)
            l = len(tariffresults)
            if curridx >= l-blocks:
                period[str(hours) + '_hour_average'] = 99
                continue
            cost = 0
            for block in range(blocks):
                cost = cost + (tariffresults[curridx+block][u'value_inc_vat'])
            cost = cost / blocks
            period[str(hours) + '_hour_average'] = cost

        self.minprice = min(period[str(hours) + '_hour_average'] for period in tariffresults)
        self.log('Lowest average price for a {} hour block is: {} p/kWh'.format(str(hours), self.minprice))        

        for period in tariffresults:
            if period[str(hours) + '_hour_average'] == self.minprice:
                self.time = period[u'valid_from']
                self.log('Lowest priced {} hour period starts at: {}'.format(str(hours), self.time))

        self.set_state('sensor.octopus_' + str(hours) + 'hour_time', state = self.time)
        self.set_state('sensor.octopus_' + str(hours) + 'hour_price', state = round(self.minprice,4))

This updates every half hour and creates/populates sensor entities for the time and price for each block specified in the apps.yaml file. For example, using the apps.yaml file above, I end up with the following entities:
sensor.octopus_2hour_time
sensor.octopus_2hour_cost
sensor.octopus_3hour_time
sensor.octopus_3hour_cost

The sensors can then be used in the front end, for example, I have entity cards set up…

type: entities
entities:
  - entity: sensor.octopus_2hour_price
    name: Price (p/kWh)
    icon: 'mdi:flash'
  - entity: sensor.octopus_2hour_time
    name: Time
    icon: 'mdi:flash'
title: Best 2hr Price
show_header_toggle: false

Capture2hr

Hope this is useful.

3 Likes

I have just done something similar (just gets the first lowest half-hour slot) but using a template :grinning: I like your method though as it is more flexible. BTW, you don’t need the auth key for tariff data. I’d also be inclined to do it as a single sensor with time as an attribute.

I’m sure there is probably a better way of extracting the valid_from and valid_to attributes in this template, but it works.

sensor:
  - platform: rest
    name: Octopus
    resource_template: >-
      https://api.octopus.energy/v1/products/AGILE-18-02-21/electricity-tariffs/E-1R-AGILE-18-02-21-N/standard-unit-rates/?period_from=
      {% set ts_now = ((as_timestamp(now())/1800)|round(0,'floor')|int * 1800) %}
      {{ ts_now |timestamp_custom ('%Y-%m-%dT%H:%M:%SZ') }}&period_to=
      {{ (ts_now + 24*60*60) |timestamp_custom ('%Y-%m-%dT%H:%M:%SZ') }}
    value_template: 'OK'
    json_attributes:
      - "results"
  - platform: template
    sensors:
      octopus_current_price: 
        unit_of_measurement: 'p/kWh'
        value_template: "{{ states.sensor.octopus.attributes.results[-1].value_inc_vat|round(2) }}"
      octopus_next_price: 
        unit_of_measurement: 'p/kWh'
        value_template: "{{ states.sensor.octopus.attributes.results[-2].value_inc_vat|round(2) }}"
      octopus_min_price: 
        unit_of_measurement: 'p/kWh'
        value_template: "{{ states.sensor.octopus.attributes.results|map(attribute='value_inc_vat')|list|min|round(2) }}"
        attribute_templates:
          valid_from: "{% set omp = states.sensor.octopus.attributes.results|map(attribute='value_inc_vat')|list|min %} {{ (states.sensor.octopus.attributes.results | selectattr('value_inc_vat', '==', omp) | list | last).valid_from }}"
          valid_to: "{% set omp = states.sensor.octopus.attributes.results|map(attribute='value_inc_vat')|list|min %} {{ (states.sensor.octopus.attributes.results | selectattr('value_inc_vat', '==', omp) | list | last).valid_to }}"
2 Likes

I’ve done a couple of updates to handle half hour blocks and remove the auth key from the request, so the code in the app.yaml can now look as follows:

octo_block_1hour:
  module: octoblock
  class: OctoBlock
  hour: 1

octo_block_90minutes:
  module: octoblock
  class: OctoBlock
  hour: 1.5

and the octoblock.py code is:

import appdaemon.plugins.hass.hassapi as hass
import requests
import json
import datetime

class OctoBlock(hass.Hass):
    def initialize(self):
        time = datetime.datetime.now()
        time = time + datetime.timedelta(seconds=5)
        self.run_every(self.get_best_period_and_cost, time, 30 * 60)

    def get_best_period_and_cost(self, kwargs):
        hours = self.args['hour']

        d = datetime.datetime.now().isoformat()
        r = requests.get('https://api.octopus.energy/v1/products/AGILE-18-02-21/electricity-tariffs/E-1R-AGILE-18-02-21-H/standard-unit-rates/?period_from=' + d)

        tariff = json.loads(r.text)
        tariffresults = tariff[u'results']
        tariffresults.reverse()

        blocks = float(hours) * 2
        blocks = int(blocks)

        for period in tariffresults:
            curridx = tariffresults.index(period)
            l = len(tariffresults)
            if curridx > l-blocks:
                period[str(hours) + '_hour_average'] = 99
                continue
            cost = 0
            for block in range(blocks):
                cost = cost + (tariffresults[curridx+block][u'value_inc_vat'])
            cost = cost / blocks
            period[str(hours) + '_hour_average'] = cost

        self.minprice = min(period[str(hours) + '_hour_average'] for period in tariffresults)
        self.log('Lowest average price for a {} hour block is: {} p/kWh'.format(str(hours), self.minprice))        

        for period in tariffresults:
            if period[str(hours) + '_hour_average'] == self.minprice:
                self.time = period[u'valid_from']
                self.log('Lowest priced {} hour period starts at: {}'.format(str(hours), self.time))

        hours = str(hours).replace(".", "_")
        self.set_state('sensor.octopus_' + hours + 'hour_time', state = self.time)
        self.set_state('sensor.octopus_' + hours + 'hour_price', state = round(self.minprice,4))

The rest is the same as above in post 22. And if the sensor entity is for a half hour period it will be named with a _ replacing the . so it doesn’t cause issues (server errors in HASS). e.g. the 1.5 hour block above ends up with sensor entities: sensor.octopus_1_5hour_price and sensor.octopus_1_5hour_time

Edited at 18:13 06/03/2020 to change if curridx >= l-blocks: to the following (just removing the =) if curridx > l-blocks: in the python code so it goes to the last block it can use to calculate in the results, previously it was stopping one block early.

1 Like

@badguy, how does your code handle no response from the server.

I’m having trouble with my approach, if no data is returned, my templates throw a fit!

I think I am going to move the API call into Node-Red and push the data to HA.

@baz123 at the moment it doesn’t, as I haven’t run in to that. I could add a timeout to the requests.get command and put it in a try: except: block to handle any exception, then maybe either retry after 60 seconds or skip the update until the next time callback, in ~30 mins I guess.

@badguy any way to make it only show todays cheapest price (even if its in the past). Want to use the start time to set my automation to charge my solar battery out of hours at the cheapest time possible?

@james_hiscott
Changing the d= and r= lines to be as follows should show it even if it is in the past:

d = datetime.date.today().isoformat()
r = requests.get('https://api.octopus.energy/v1/products/AGILE-18-02-21/electricity-tariffs/E-1R-AGILE-18-02-21-H/standard-unit-rates/?period_from=' + d + 'T00:00:00')

However if you are running this after 4pm it will show tomorrow if it is cheaper than today, if you don’t want that, set r= to the following instead:

r = requests.get('https://api.octopus.energy/v1/products/AGILE-18-02-21/electricity-tariffs/E-1R-AGILE-18-02-21-H/standard-unit-rates/?period_from=' + d + 'T00:00:00&period_to=' + d + 'T23:59:59')
1 Like

@badguy

Am i reading it right, the sensor created will be the best “x” minutes of the day (whatever i set in the apps.yaml) up to 4pm on that day, then after 4pm with be the best up to 4pm the next day?

I think it was mentioned before and am looking for the same. I need to find the best time slot(s) to charge my batteries connected to solar panels/inverter, which I guess will be at night but could be good to top up during the day if the price drops

Also looking at automating the time on my immersion heater when the price is low during the day

@matthewjporter yes basically, it is in hours rather than minutes, just because they use half hour blocks. Octopus publish the next days tariff between 4 and 7pm, though it is usually around 4pm. So just before 4pm their website was showing the following for me:


so I’m currently looking at the right hand tab, and the sensors I’ve written were giving me the following:

The data goes up to 23:00 so for example, the latest the 3 hour time period could start with data ending at 11pm is 8pm, which is what it was showing for the best price.

Now it has gone past 4pm, and the data has been updated, the website is showing the following:


i.e. now looking at the left tab for today and the right tab is tomorrow. The data their API provides, is also updated, so now my sensors are showing:

So based on the new data, the best 3 hour price is now 12:30 tomorrow. Though it could be any time up to 23:00 tomorrow.
Hope that helps.

I’ve been fiddling with a Node-Red node to do something similar.

It is still rather rough round the edges, but it seems to work though (and perhaps as it may be a different region), I’m getting different answers. It could be my maths is wrong of course :grinning:. I have though pulled it into Excel to check.

Personally I also wanted the current price and the next price to display. I’ll probably use a template to pull it in from MQTT for now as I will use it elsewhere as well.

{
	"current_price": 6.174,
	"next_price": 7.7175,
	"min_price_inc_vat": 4.1895,
	"max_price_inc_vat": 24.8745,
	"min_blocks": [{
		"min Block Price": 4.19,
		"min Block valid From": "2020-03-17T04:00:00Z",
		"min_block_size_mins": 60
	}, {
		"min Block Price": 4.47,
		"min Block valid From": "2020-03-17T03:30:00Z",
		"min_block_size_mins": 120
	}]
}
2 Likes

@badguy
Got it, Cheers

I am test driving it at the mo to see as I have not switched yet

One question, how would you do an automation based on the best price per time

I haven’t really played around with this yet, but maybe something along the lines of the following trigger for the automation:

trigger:
  platform: template
  value_template: >
  {% if (states("sensor.date_time_iso") + "Z") == (states("sensor.octopus_1hour_time")) %}
    true
  {% endif %}

This would need Time & Date - Home Assistant configured too.

1 Like

I’ve updated the code again and put it here:

I’ve also created this:

The README.md files in the repos are up-to-date, the region letter can be set in the apps.yaml now as well, and they are also integrated with https://hacs.xyz/ so can be installed into Appdaemon through HACS. The repositories are now part of the HACS default repositories.

2 Likes

Just got them from HACS thanks @badguy do you just use the REST API to get the current price or not bother with that? was wondering if it was a logical extension to your octoblock sensors?

The octocost one is independent of the octoblock one - it calls:
https://api.octopus.energy/v1/electricity-meter-points/(MPAN)/meters/(SERIAL)/consumption/
and
https://api.octopus.energy/v1/products/AGILE-18-02-21/electricity-tariffs/E-1R-AGILE-18-02-21-(region)/standard-unit-rates/

The octoblock one just calls the tariff api.

1 Like

Sorry, i was meaning, maybe the octoblock could pull the current tariff price for this current 30mins (as an extension to what it already does.

Oh I see :slight_smile: I didn’t include that as I’d used the bit @dmgreen had added at the top of this thread. That works nicely for me.

@badguy

Awesome. Now using HACS

Thanks for the great work. Any chance you might be working to make this a component?

I’ll take a look into it. I wasn’t planning it when I wrote the code initially, I just wanted to get something working. But I guess I’ll have plenty of spare time with everything going on now :mask: :woozy_face: