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