Octopus Agile pricing binding

OK, so I have now tidied this up with some rules.

.items file

// Agile Tariff Items
Number agileRateNow "Current price of electricity [%.3fp]"
Contact agilePlunge "Contact showing if current rate is plunge"

Number agile3Price "Price of electricity in cheapest 3 hours [%.3fp]"
Number agile3Index "Index number of the start of the cheapest 3 hour period [%d]"
Contact agile3Lowest "Contact showing if current time is in cheapest 3 hours"

Number agile6Price "Price of electricity in cheapest 6 hours [%.3fp]"
Number agile6Index "Index number of the start of the cheapest 6 hour period [%d]"
Contact agile6Lowest "Contact showing if current time is in cheapest 6 hours"

.rules file

// Rules to process Octopus Agile Time of Use rates
import java.util.List

// Rules use an array of values 0 - 96 to represent the 96 half hour periods from midnight today to 2359 tomorrow

// initialise array with length 96 and setting 100 to represent a null rate (as rates can be positive, 0 or negative)
var List<Number> rates = newArrayList(100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100)

rule AgileMidnight
when
	Time cron "0 0 0 1/1 * ? *"	// at midnight to move the rates to the right day
then
	for (var i=0 ; i < 48 ; i++) rates.set(i,rates.get(i+48))	//move rates from tomorrow to today
	for (var i=48 ; i < 96 ; i++) rates.set(i,100)			//reset tomorrows rates
	if (agile3Index.state as Number >=48) agile3Index.sendCommand(agile3Index.state as Number - 48) // move the cheapest period index to today if it was tomorrow
	if (agile6Index.state as Number >=48) agile3Index.sendCommand(agile6Index.state as Number - 48) // move the cheapest period index to today if it was tomorrow
end

rule AgileCheckForNewRates
when
        System started or               //run the rule at startup
        Time cron "0 15 16-20/1 1/1 * ? *"      //and hourly from 4:15pm to 8:15pm when the rates for the next day should be available
then
	if (rates.get(48) == 100) {	//only call the api if there are no rates for tomorrow
		// reset all rates ready for refresh from api
		for (var i=0 ; i < 96 ; i++) rates.set(i,100)		//reset the arrary ready for new data

		val String dateString = now.toString("yyyy-MM-dd")	//get today's date as a string to insert into the url
		val Number today$ = dateString.substring(8,10)
		val String agileJSON = sendHttpGetRequest("https://api.octopus.energy/v1/products/AGILE-18-02-21/electricity-tariffs/E-1R-AGILE-18-02-21-B/standard-unit-rates/?period_from="+dateString+"T00:00Z")
		val Number rateCount = (Integer::parseInt(transform("JSONPATH", "$.count", agileJSON)))	//extract the number of rates and convert to an integer
		logWarn ("rules.AgileCheckForNewRates" , "Read "+rateCount.toString+" rates from Octopus API")

		for (var i = 0 ; i < (rateCount) ; i++) {	//for each rate
			var Number index = 0
			var Number rate = (Double::parseDouble(transform("JSONPATH", "$.results["+i+"].value_inc_vat", agileJSON)))	//extract the rate and convert to a number
			var String dtfrom = transform("JSONPATH", "$.results["+i+"].valid_from", agileJSON)	//extract the start date/time
			var Number hour = (Integer::parseInt(dtfrom.substring(11,13))) //extract the hour and convert to a number
			index = (hour * 2)
			if (dtfrom.substring(14,15) == "3") {
				index += 1	//add one for the rates starting at the half hour
				}
			if (dtfrom.substring(8,10) != today$) {
				index += 48	//tomorrow's rates are indexed after today's
				}
//			logWarn ("rules.AgileMidnight" , "Index: "+index.toString+" rate: "+rate.toString)
			var index1 = index.intValue
			rates.set(index1,rate)
			}
		// Check for cheapest 3 and 6 hour rates
                var Number rate3 = 1000 // initialise cheapest 3 hour rate
       	        var Number index3 = 99  // initialise index of start time of cheapest 3 hours
               	var Number rate6 = 2000 // initialise cheapest 6 hour rate
                var Number index6 = 99  // initialise index of start time of cheapest 6 hours


		// Calculate the index starting now
		val String timeString = now.toString("HH:mm")      //get current hour as a string
		val Number hour = (Integer::parseInt(timeString.substring(0,2))) //extract the hour and convert to a number
		var Number index = (hour * 2)
		if (timeString.substring(3,4) == "3" || timeString.substring(3,4) == "4" || timeString.substring(3,4) == "5") {
 		index = (index + 1)     //add one for the rates starting at the half hour
 			}
 		val index1 = index.intValue
 
                for (var i = index1 ; i < (rateCount-5) ; i++) {       //for each rate starting now
			var Number rate3sum = 0	//Set rates back to zero each time
			var Number rate6sum = 0
			for (var offset = 0 ; offset < 12 ; offset++) { // Look at next 12 rates

				if (offset < 6) { // Only add next 3 rates if  we have't already added 3 hours worth
					rate3sum = rate3sum+rates.get(i+offset)
					}

				if (i <= (rateCount-12)) {	// Only read rates if all 12 are available
					rate6sum = rate6sum+rates.get(i+offset)
					}

				}
			if (rate3sum < rate3) {	//If this is currently the cheapest rate, record the index
				index3 = i
				rate3 = rate3sum
				}

                        if (i <= (rateCount-12) && rate6sum < rate6) { //If we are still collecting 6 hour rates and this is currently the cheapest rate, record the index
                                index6 = i
                                rate6 = rate6sum
                                }
			}
		rate3 = (rate3 / 6)	//Calculate the average rate over the cheapest 3 hours (6 half hours)
		agile3Price.postUpdate(rate3)
		agile3Index.postUpdate(index3)
		logWarn ("rules.AgileCheckForNewRates" , "Cheapest 3 hours: "+rate3.toString+" at index: "+index3.toString)

		rate6 = rate6 / 12	//Calculate the average rate over the cheapest 6 hours (12 half hours)
		agile6Price.postUpdate(rate6)
		agile6Index.postUpdate(index6)
		logWarn ("rules.AgileCheckForNewRates" , "Cheapest 6 hours: "+rate6.toString+" at index: "+index6.toString)

		}
end

rule AgileRateChange
when
        System started or               //run the rule at startup Note there is a race condition at system startup that means that this rule may run before the array is populated
 	Time cron "0 0/30 * 1/1 * ? *" //Run rule at each new hour/half hour when rate changes
then
        val String dateString = now.toString("HH:mm")      //get current hour as a string
        val Number hour = (Integer::parseInt(dateString.substring(0,2))) //extract the hour and convert to a number
	var Number index = (hour * 2)

        if (dateString.substring(3,4) == "3" || dateString.substring(3,4) == "4" || dateString.substring(3,4) == "5") {
		index = (index + 1)     //add one for the rates starting at the half hour
                }

	val index1 = index.intValue
	val rate = rates.get(index1)
	agileRateNow.postUpdate(rate)

	if (index >= agile3Index.state as Number && index < (agile3Index.state as Number +6)) agile3Lowest.sendCommand(CLOSED) else agile3Lowest.sendCommand(OPEN) // Close the contact if during the cheapest 3 hours
	if (index >= agile6Index.state as Number && index < (agile6Index.state as Number +12)) agile6Lowest.sendCommand(CLOSED) else agile6Lowest.sendCommand(OPEN) // Close the contact if during the cheapest 6 hours
	if (rate <= 0) agilePlunge.sendCommand(CLOSED) else agilePlunge.sendCommand(OPEN)
end

The principle of operation is to create an array of prices from midnight today until 2300 tomorrow, indexed from 0 to 95. On the first run, the array is blank so it will retrieve what it can from the API and populate the array. If it is before tomorrow’s rates are available, midnight tomorrow (array entry 48) is left at 100.

Starting at 1615, when the new rates should be available, it rule checks to see if it has tomorrows rates. If not it will fill the array from the API. If tomorrows rates are available, tomorrow midnight rate will be populated, if not, it will still be at 100 and the next time the rule is run it will check the API again.

After filling the array, the rule will identify the index and rate for the cheapest 3 hours (eg for washing machine, dishwasher or tumble dryer) and 6 hours (e.g. charging EV or heating hot water). The rule starts from now because there is no point in identifying cheapest periods in the past.

At midnight, the rule moves all of the values that were for tomorrow and moves them into today and initialises tomorrows rates. It also brings any cheapest period index values to today.

Every half hour, the rule updates the current rate and closes a contact if the current period is within the cheapest 3 or 6 hours. There is also a contact for plunge rates so you can switch on power consuming items whenever rates are free or paying back.

To use the contact, you can create a rule triggered on:

when Item agile3Lowest changed from OPEN to CLOSED
1 Like