Rules and sitemap elements to support Agile electricity tariff from Octopus in UK

Context

I’ve recently changed my electricity supplier here in the UK to Octopus. They provide a dynamic tariff that varies the unit price every half hour. Occasionally, when it’s sunny and windy the price goes negative and you can get paid to use electricity. Generally the price follows a pattern of higher prices between 1600 and 2000 with lower prices in the very early morning, mid morning and early afternoon. The highest prices are usually around the same price or slightly higher than the variable tariff and the rest of the time it’s much cheaper. The Agile tariff is only available to households with a smart meter.

Incidentally, In order to determine whether Agile would save you money I used an Android app called Octopus Compare.

I wanted to be able to see the unit prices in my OH sitemap and also calculate the cheapest times to run my appliances.

Solution

Octopus provide an API that allows you to download the future prices as JSON data. The new prices are released at 16:00GMT every day and contain prices for the following day until 23:00GMT. The APi documentation is here. The JSON data for the Northwest can be found here.

To make the JSON data available to rules we have to use the HTTP binding to get the JSON data into a string item via a channel:

Now that the thing, channel and item are created and linked we will have the JSON data from the API in the OctopusAgilePrices_AgilePricesJSON item.

I wanted to be able to see all the unit prices in the sitemap which meant create a lot of items. With some Excel jiggery pokery I quickly created 100 Number:EnergyPrice items in an items file:

Number:EnergyPrice Agile_Price_0 "Agile Period 0 Price [%.4f GBP/kWh]"
Number:EnergyPrice Agile_Price_1 "Agile Period 1 Price [%.4f GBP/kWh]"
Number:EnergyPrice Agile_Price_2 "Agile Period 2 Price [%.4f GBP/kWh]"
Number:EnergyPrice Agile_Price_3 "Agile Period 3 Price [%.4f GBP/kWh]"
Number:EnergyPrice Agile_Price_4 "Agile Period 4 Price [%.4f GBP/kWh]"
Number:EnergyPrice Agile_Price_5 "Agile Period 5 Price [%.4f GBP/kWh]"
Number:EnergyPrice Agile_Price_6 "Agile Period 6 Price [%.4f GBP/kWh]"
Number:EnergyPrice Agile_Price_7 "Agile Period 7 Price [%.4f GBP/kWh]"
Number:EnergyPrice Agile_Price_8 "Agile Period 8 Price [%.4f GBP/kWh]"
Number:EnergyPrice Agile_Price_9 "Agile Period 9 Price [%.4f GBP/kWh]"
Number:EnergyPrice Agile_Price_10 "Agile Period 10 Price [%.4f GBP/kWh]"
Number:EnergyPrice Agile_Price_11 "Agile Period 11 Price [%.4f GBP/kWh]"
Number:EnergyPrice Agile_Price_12 "Agile Period 12 Price [%.4f GBP/kWh]"
Number:EnergyPrice Agile_Price_13 "Agile Period 13 Price [%.4f GBP/kWh]"
Number:EnergyPrice Agile_Price_14 "Agile Period 14 Price [%.4f GBP/kWh]"
Number:EnergyPrice Agile_Price_15 "Agile Period 15 Price [%.4f GBP/kWh]"
Number:EnergyPrice Agile_Price_16 "Agile Period 16 Price [%.4f GBP/kWh]"
Number:EnergyPrice Agile_Price_17 "Agile Period 17 Price [%.4f GBP/kWh]"
Number:EnergyPrice Agile_Price_18 "Agile Period 18 Price [%.4f GBP/kWh]"
Number:EnergyPrice Agile_Price_19 "Agile Period 19 Price [%.4f GBP/kWh]"
Number:EnergyPrice Agile_Price_20 "Agile Period 20 Price [%.4f GBP/kWh]"
Number:EnergyPrice Agile_Price_21 "Agile Period 21 Price [%.4f GBP/kWh]"
Number:EnergyPrice Agile_Price_22 "Agile Period 22 Price [%.4f GBP/kWh]"
Number:EnergyPrice Agile_Price_23 "Agile Period 23 Price [%.4f GBP/kWh]"
Number:EnergyPrice Agile_Price_24 "Agile Period 24 Price [%.4f GBP/kWh]"
Number:EnergyPrice Agile_Price_25 "Agile Period 25 Price [%.4f GBP/kWh]"
Number:EnergyPrice Agile_Price_26 "Agile Period 26 Price [%.4f GBP/kWh]"
Number:EnergyPrice Agile_Price_27 "Agile Period 27 Price [%.4f GBP/kWh]"
Number:EnergyPrice Agile_Price_28 "Agile Period 28 Price [%.4f GBP/kWh]"
Number:EnergyPrice Agile_Price_29 "Agile Period 29 Price [%.4f GBP/kWh]"
Number:EnergyPrice Agile_Price_30 "Agile Period 30 Price [%.4f GBP/kWh]"
Number:EnergyPrice Agile_Price_31 "Agile Period 31 Price [%.4f GBP/kWh]"
Number:EnergyPrice Agile_Price_32 "Agile Period 32 Price [%.4f GBP/kWh]"
Number:EnergyPrice Agile_Price_33 "Agile Period 33 Price [%.4f GBP/kWh]"
Number:EnergyPrice Agile_Price_34 "Agile Period 34 Price [%.4f GBP/kWh]"
Number:EnergyPrice Agile_Price_35 "Agile Period 35 Price [%.4f GBP/kWh]"
Number:EnergyPrice Agile_Price_36 "Agile Period 36 Price [%.4f GBP/kWh]"
Number:EnergyPrice Agile_Price_37 "Agile Period 37 Price [%.4f GBP/kWh]"
Number:EnergyPrice Agile_Price_38 "Agile Period 38 Price [%.4f GBP/kWh]"
Number:EnergyPrice Agile_Price_39 "Agile Period 39 Price [%.4f GBP/kWh]"
Number:EnergyPrice Agile_Price_40 "Agile Period 40 Price [%.4f GBP/kWh]"
Number:EnergyPrice Agile_Price_41 "Agile Period 41 Price [%.4f GBP/kWh]"
Number:EnergyPrice Agile_Price_42 "Agile Period 42 Price [%.4f GBP/kWh]"
Number:EnergyPrice Agile_Price_43 "Agile Period 43 Price [%.4f GBP/kWh]"
Number:EnergyPrice Agile_Price_44 "Agile Period 44 Price [%.4f GBP/kWh]"
Number:EnergyPrice Agile_Price_45 "Agile Period 45 Price [%.4f GBP/kWh]"
Number:EnergyPrice Agile_Price_46 "Agile Period 46 Price [%.4f GBP/kWh]"
Number:EnergyPrice Agile_Price_47 "Agile Period 47 Price [%.4f GBP/kWh]"
Number:EnergyPrice Agile_Price_48 "Agile Period 48 Price [%.1f GBP/kWh]"
Number:EnergyPrice Agile_Price_49 "Agile Period 49 Price [%.1f GBP/kWh]"
Number:EnergyPrice Agile_Price_50 "Agile Period 50 Price [%.1f GBP/kWh]"
Number:EnergyPrice Agile_Price_51 "Agile Period 51 Price [%.1f GBP/kWh]"
Number:EnergyPrice Agile_Price_52 "Agile Period 52 Price [%.1f GBP/kWh]"
Number:EnergyPrice Agile_Price_53 "Agile Period 53 Price [%.1f GBP/kWh]"
Number:EnergyPrice Agile_Price_54 "Agile Period 54 Price [%.1f GBP/kWh]"
Number:EnergyPrice Agile_Price_55 "Agile Period 55 Price [%.1f GBP/kWh]"
Number:EnergyPrice Agile_Price_56 "Agile Period 56 Price [%.1f GBP/kWh]"
Number:EnergyPrice Agile_Price_57 "Agile Period 57 Price [%.1f GBP/kWh]"
Number:EnergyPrice Agile_Price_58 "Agile Period 58 Price [%.1f GBP/kWh]"
Number:EnergyPrice Agile_Price_59 "Agile Period 59 Price [%.1f GBP/kWh]"
Number:EnergyPrice Agile_Price_60 "Agile Period 60 Price [%.1f GBP/kWh]"
Number:EnergyPrice Agile_Price_61 "Agile Period 61 Price [%.1f GBP/kWh]"
Number:EnergyPrice Agile_Price_62 "Agile Period 62 Price [%.1f GBP/kWh]"
Number:EnergyPrice Agile_Price_63 "Agile Period 63 Price [%.1f GBP/kWh]"
Number:EnergyPrice Agile_Price_64 "Agile Period 64 Price [%.1f GBP/kWh]"
Number:EnergyPrice Agile_Price_65 "Agile Period 65 Price [%.1f GBP/kWh]"
Number:EnergyPrice Agile_Price_66 "Agile Period 66 Price [%.1f GBP/kWh]"
Number:EnergyPrice Agile_Price_67 "Agile Period 67 Price [%.1f GBP/kWh]"
Number:EnergyPrice Agile_Price_68 "Agile Period 68 Price [%.1f GBP/kWh]"
Number:EnergyPrice Agile_Price_69 "Agile Period 69 Price [%.1f GBP/kWh]"
Number:EnergyPrice Agile_Price_70 "Agile Period 70 Price [%.1f GBP/kWh]"
Number:EnergyPrice Agile_Price_71 "Agile Period 71 Price [%.1f GBP/kWh]"
Number:EnergyPrice Agile_Price_72 "Agile Period 72 Price [%.1f GBP/kWh]"
Number:EnergyPrice Agile_Price_73 "Agile Period 73 Price [%.1f GBP/kWh]"
Number:EnergyPrice Agile_Price_74 "Agile Period 74 Price [%.1f GBP/kWh]"
Number:EnergyPrice Agile_Price_75 "Agile Period 75 Price [%.1f GBP/kWh]"
Number:EnergyPrice Agile_Price_76 "Agile Period 76 Price [%.1f GBP/kWh]"
Number:EnergyPrice Agile_Price_77 "Agile Period 77 Price [%.1f GBP/kWh]"
Number:EnergyPrice Agile_Price_78 "Agile Period 78 Price [%.1f GBP/kWh]"
Number:EnergyPrice Agile_Price_79 "Agile Period 79 Price [%.1f GBP/kWh]"
Number:EnergyPrice Agile_Price_80 "Agile Period 80 Price [%.1f GBP/kWh]"
Number:EnergyPrice Agile_Price_81 "Agile Period 81 Price [%.1f GBP/kWh]"
Number:EnergyPrice Agile_Price_82 "Agile Period 82 Price [%.1f GBP/kWh]"
Number:EnergyPrice Agile_Price_83 "Agile Period 83 Price [%.1f GBP/kWh]"
Number:EnergyPrice Agile_Price_84 "Agile Period 84 Price [%.1f GBP/kWh]"
Number:EnergyPrice Agile_Price_85 "Agile Period 85 Price [%.1f GBP/kWh]"
Number:EnergyPrice Agile_Price_86 "Agile Period 86 Price [%.1f GBP/kWh]"
Number:EnergyPrice Agile_Price_87 "Agile Period 87 Price [%.1f GBP/kWh]"
Number:EnergyPrice Agile_Price_88 "Agile Period 88 Price [%.1f GBP/kWh]"
Number:EnergyPrice Agile_Price_89 "Agile Period 89 Price [%.1f GBP/kWh]"
Number:EnergyPrice Agile_Price_90 "Agile Period 90 Price [%.1f GBP/kWh]"
Number:EnergyPrice Agile_Price_91 "Agile Period 91 Price [%.1f GBP/kWh]"
Number:EnergyPrice Agile_Price_92 "Agile Period 92 Price [%.1f GBP/kWh]"
Number:EnergyPrice Agile_Price_93 "Agile Period 93 Price [%.1f GBP/kWh]"
Number:EnergyPrice Agile_Price_94 "Agile Period 94 Price [%.1f GBP/kWh]"
Number:EnergyPrice Agile_Price_95 "Agile Period 95 Price [%.1f GBP/kWh]"
Number:EnergyPrice Agile_Price_96 "Agile Period 96 Price [%.1f GBP/kWh]"
Number:EnergyPrice Agile_Price_97 "Agile Period 97 Price [%.1f GBP/kWh]"
Number:EnergyPrice Agile_Price_98 "Agile Period 98 Price [%.1f GBP/kWh]"
Number:EnergyPrice Agile_Price_99 "Agile Period 99 Price [%.1f GBP/kWh]"

I also added some items to hold data about the cheapest 30 minute period, and the cheapest 1h, 1.5h, 3h and 4h periods. I chose these periods because they most closely match the most commonly used programs on my dishwasher, washing machine and tumble dryer:

Number              Agile_Price_Highest_Index           "Agile Price Highest Index [%d]"                  //Contains the number of the highest index populated with JSON data

Number              Agile_Price_Lowest_0_5h_Index       "Agile Cheapest Unit Index [%d]"
DateTime            Agile_Price_Lowest_0_5h_Time        "Agile Cheapest Unit Start Time [%s]"
Number              Agile_Price_Lowest_0_5h_Price       "Agile Cheapest Unit Price [%.1f GBP/kWh]"

Number              Agile_Price_Lowest_1h_Index         "Agile Cheapest 1h Index [%d]"
DateTime            Agile_Price_Lowest_1h_Time          "Agile Cheapest 1h Start Time [%s]"
Number              Agile_Price_Lowest_1h_Avg_Price     "Agile Cheapest 1h Avg Unit Price [%.1f GBP/kWh]"

Number              Agile_Price_Lowest_1_5h_Index       "Agile Cheapest 1.5h Index [%d]"
DateTime            Agile_Price_Lowest_1_5h_Time        "Agile Cheapest 1.5h Start Time [%s]"
Number              Agile_Price_Lowest_1_5h_Avg_Price   "Agile Cheapest 1.5h Avg Unit Price [%.1f GBP/kWh]"

Number              Agile_Price_Lowest_3h_Index         "Agile Cheapest 3h Index [%d]"
DateTime            Agile_Price_Lowest_3h_Time          "Agile Cheapest 3h Start Time [%s]"
Number              Agile_Price_Lowest_3h_Avg_Price     "Agile Cheapest 3h Avg Unit Price [%.1f GBP/kWh]"

Number              Agile_Price_Lowest_4h_Index         "Agile Cheapest 4h Index [%d]"
DateTime            Agile_Price_Lowest_4h_Time          "Agile Cheapest 4h Start Time [%s]"
Number              Agile_Price_Lowest_4h_Avg_Price     "Agile Cheapest 4h Avg Unit Price [%.1f GBP/kWh]"

Next I needed to add these to my sitemap. First I added all the unit prices, however, I didn’t want to have to scroll past the items that were no longer relevant because they refer to prices in the past. To get around this I used the sitemap visibility feature with a condition that compares an item that holds the number of minutes from midnight to the current time:


            Text icon=energy label="Electricity Unit Prices" {
                Text icon=energy item=Agile_Price_0 label="Price at 0:00-0:30 [%.4fp/kWh]" visibility=[Minutes_through_day<30]
                Text icon=energy item=Agile_Price_1 label="Price at 0:30-1:00 [%.4fp/kWh]" visibility=[Minutes_through_day<60]
                Text icon=energy item=Agile_Price_2 label="Price at 1:00-1:30 [%.4fp/kWh]" visibility=[Minutes_through_day<90]
                Text icon=energy item=Agile_Price_3 label="Price at 1:30-2:00 [%.4fp/kWh]" visibility=[Minutes_through_day<120]
                Text icon=energy item=Agile_Price_4 label="Price at 2:00-2:30 [%.4fp/kWh]" visibility=[Minutes_through_day<150]
                Text icon=energy item=Agile_Price_5 label="Price at 2:30-3:00 [%.4fp/kWh]" visibility=[Minutes_through_day<180]
                Text icon=energy item=Agile_Price_6 label="Price at 3:00-3:30 [%.4fp/kWh]" visibility=[Minutes_through_day<210]
                Text icon=energy item=Agile_Price_7 label="Price at 3:30-4:00 [%.4fp/kWh]" visibility=[Minutes_through_day<240]
                Text icon=energy item=Agile_Price_8 label="Price at 4:00-4:30 [%.4fp/kWh]" visibility=[Minutes_through_day<270]
                Text icon=energy item=Agile_Price_9 label="Price at 4:30-5:00 [%.4fp/kWh]" visibility=[Minutes_through_day<300]
                Text icon=energy item=Agile_Price_10 label="Price at 5:00-5:30 [%.4fp/kWh]" visibility=[Minutes_through_day<330]
                Text icon=energy item=Agile_Price_11 label="Price at 5:30-6:00 [%.4fp/kWh]" visibility=[Minutes_through_day<360]
                Text icon=energy item=Agile_Price_12 label="Price at 6:00-6:30 [%.4fp/kWh]" visibility=[Minutes_through_day<390]
                Text icon=energy item=Agile_Price_13 label="Price at 6:30-7:00 [%.4fp/kWh]" visibility=[Minutes_through_day<420]
                Text icon=energy item=Agile_Price_14 label="Price at 7:00-7:30 [%.4fp/kWh]" visibility=[Minutes_through_day<450]
                Text icon=energy item=Agile_Price_15 label="Price at 7:30-8:00 [%.4fp/kWh]" visibility=[Minutes_through_day<480]
                Text icon=energy item=Agile_Price_16 label="Price at 8:00-8:30 [%.4fp/kWh]" visibility=[Minutes_through_day<510]
                Text icon=energy item=Agile_Price_17 label="Price at 8:30-9:00 [%.4fp/kWh]" visibility=[Minutes_through_day<540]
                Text icon=energy item=Agile_Price_18 label="Price at 9:00-9:30 [%.4fp/kWh]" visibility=[Minutes_through_day<570]
                Text icon=energy item=Agile_Price_19 label="Price at 9:30-10:00 [%.4fp/kWh]" visibility=[Minutes_through_day<600]
                Text icon=energy item=Agile_Price_20 label="Price at 10:00-10:30 [%.4fp/kWh]" visibility=[Minutes_through_day<630]
                Text icon=energy item=Agile_Price_21 label="Price at 10:30-11:00 [%.4fp/kWh]" visibility=[Minutes_through_day<660]
                Text icon=energy item=Agile_Price_22 label="Price at 11:00-11:30 [%.4fp/kWh]" visibility=[Minutes_through_day<690]
                Text icon=energy item=Agile_Price_23 label="Price at 11:30-12:00 [%.4fp/kWh]" visibility=[Minutes_through_day<720]
                Text icon=energy item=Agile_Price_24 label="Price at 12:00-12:30 [%.4fp/kWh]" visibility=[Minutes_through_day<750]
                Text icon=energy item=Agile_Price_25 label="Price at 12:30-13:00 [%.4fp/kWh]" visibility=[Minutes_through_day<780]
                Text icon=energy item=Agile_Price_26 label="Price at 13:00-13:30 [%.4fp/kWh]" visibility=[Minutes_through_day<810]
                Text icon=energy item=Agile_Price_27 label="Price at 13:30-14:00 [%.4fp/kWh]" visibility=[Minutes_through_day<840]
                Text icon=energy item=Agile_Price_28 label="Price at 14:00-14:30 [%.4fp/kWh]" visibility=[Minutes_through_day<870]
                Text icon=energy item=Agile_Price_29 label="Price at 14:30-15:00 [%.4fp/kWh]" visibility=[Minutes_through_day<900]
                Text icon=energy item=Agile_Price_30 label="Price at 15:00-15:30 [%.4fp/kWh]" visibility=[Minutes_through_day<930]
                Text icon=energy item=Agile_Price_31 label="Price at 15:30-16:00 [%.4fp/kWh]" visibility=[Minutes_through_day<960]
                Text icon=energy item=Agile_Price_32 label="Price at 16:00-16:30 [%.4fp/kWh]" visibility=[Minutes_through_day<990]
                Text icon=energy item=Agile_Price_33 label="Price at 16:30-17:00 [%.4fp/kWh]" visibility=[Minutes_through_day<1020]
                Text icon=energy item=Agile_Price_34 label="Price at 17:00-17:30 [%.4fp/kWh]" visibility=[Minutes_through_day<1050]
                Text icon=energy item=Agile_Price_35 label="Price at 17:30-18:00 [%.4fp/kWh]" visibility=[Minutes_through_day<1080]
                Text icon=energy item=Agile_Price_36 label="Price at 18:00-18:30 [%.4fp/kWh]" visibility=[Minutes_through_day<1110]
                Text icon=energy item=Agile_Price_37 label="Price at 18:30-19:00 [%.4fp/kWh]" visibility=[Minutes_through_day<1140]
                Text icon=energy item=Agile_Price_38 label="Price at 19:00-19:30 [%.4fp/kWh]" visibility=[Minutes_through_day<1170]
                Text icon=energy item=Agile_Price_39 label="Price at 19:30-20:00 [%.4fp/kWh]" visibility=[Minutes_through_day<1200]
                Text icon=energy item=Agile_Price_40 label="Price at 20:00-20:30 [%.4fp/kWh]" visibility=[Minutes_through_day<1230]
                Text icon=energy item=Agile_Price_41 label="Price at 20:30-21:00 [%.4fp/kWh]" visibility=[Minutes_through_day<1260]
                Text icon=energy item=Agile_Price_42 label="Price at 21:00-21:30 [%.4fp/kWh]" visibility=[Minutes_through_day<1290]
                Text icon=energy item=Agile_Price_43 label="Price at 21:30-22:00 [%.4fp/kWh]" visibility=[Minutes_through_day<1320]
                Text icon=energy item=Agile_Price_44 label="Price at 22:00-22:30 [%.4fp/kWh]" visibility=[Minutes_through_day<1350]
                Text icon=energy item=Agile_Price_45 label="Price at 22:30-23:00 [%.4fp/kWh]" visibility=[Minutes_through_day<1380]
                Text icon=energy item=Agile_Price_46 label="Price at 23:00-23:30 [%.4fp/kWh]" visibility=[Minutes_through_day<1410]
                Text icon=energy item=Agile_Price_47 label="Price at 23:30-00:00 [%.4fp/kWh]" visibility=[Minutes_through_day<1440]
                Text icon=energy item=Agile_Price_48 label="Tomorrow at 0:00-0:30 [%.4fp/kWh]"
                Text icon=energy item=Agile_Price_49 label="Tomorrow at 0:30-1:00 [%.4fp/kWh]"
                Text icon=energy item=Agile_Price_50 label="Tomorrow at 1:00-1:30 [%.4fp/kWh]"
                Text icon=energy item=Agile_Price_51 label="Tomorrow at 1:30-2:00 [%.4fp/kWh]"
                Text icon=energy item=Agile_Price_52 label="Tomorrow at 2:00-2:30 [%.4fp/kWh]"
                Text icon=energy item=Agile_Price_53 label="Tomorrow at 2:30-3:00 [%.4fp/kWh]"
                Text icon=energy item=Agile_Price_54 label="Tomorrow at 3:00-3:30 [%.4fp/kWh]"
                Text icon=energy item=Agile_Price_55 label="Tomorrow at 3:30-4:00 [%.4fp/kWh]"
                Text icon=energy item=Agile_Price_56 label="Tomorrow at 4:00-4:30 [%.4fp/kWh]"
                Text icon=energy item=Agile_Price_57 label="Tomorrow at 4:30-5:00 [%.4fp/kWh]"
                Text icon=energy item=Agile_Price_58 label="Tomorrow at 5:00-5:30 [%.4fp/kWh]"
                Text icon=energy item=Agile_Price_59 label="Tomorrow at 5:30-6:00 [%.4fp/kWh]"
                Text icon=energy item=Agile_Price_60 label="Tomorrow at 6:00-6:30 [%.4fp/kWh]"
                Text icon=energy item=Agile_Price_61 label="Tomorrow at 6:30-7:00 [%.4fp/kWh]"
                Text icon=energy item=Agile_Price_62 label="Tomorrow at 7:00-7:30 [%.4fp/kWh]"
                Text icon=energy item=Agile_Price_63 label="Tomorrow at 7:30-8:00 [%.4fp/kWh]"
                Text icon=energy item=Agile_Price_64 label="Tomorrow at 8:00-8:30 [%.4fp/kWh]"
                Text icon=energy item=Agile_Price_65 label="Tomorrow at 8:30-9:00 [%.4fp/kWh]"
                Text icon=energy item=Agile_Price_66 label="Tomorrow at 9:00-9:30 [%.4fp/kWh]"
                Text icon=energy item=Agile_Price_67 label="Tomorrow at 9:30-10:00 [%.4fp/kWh]"
                Text icon=energy item=Agile_Price_68 label="Tomorrow at 10:00-10:30 [%.4fp/kWh]"
                Text icon=energy item=Agile_Price_69 label="Tomorrow at 10:30-11:00 [%.4fp/kWh]"
                Text icon=energy item=Agile_Price_70 label="Tomorrow at 11:00-11:30 [%.4fp/kWh]"
                Text icon=energy item=Agile_Price_71 label="Tomorrow at 11:30-12:00 [%.4fp/kWh]"
                Text icon=energy item=Agile_Price_72 label="Tomorrow at 12:00-12:30 [%.4fp/kWh]"
                Text icon=energy item=Agile_Price_73 label="Tomorrow at 12:30-13:00 [%.4fp/kWh]"
                Text icon=energy item=Agile_Price_74 label="Tomorrow at 13:00-13:30 [%.4fp/kWh]"
                Text icon=energy item=Agile_Price_75 label="Tomorrow at 13:30-14:00 [%.4fp/kWh]"
                Text icon=energy item=Agile_Price_76 label="Tomorrow at 14:00-14:30 [%.4fp/kWh]"
                Text icon=energy item=Agile_Price_77 label="Tomorrow at 14:30-15:00 [%.4fp/kWh]"
                Text icon=energy item=Agile_Price_78 label="Tomorrow at 15:00-15:30 [%.4fp/kWh]"
                Text icon=energy item=Agile_Price_79 label="Tomorrow at 15:30-16:00 [%.4fp/kWh]"
                Text icon=energy item=Agile_Price_80 label="Tomorrow at 16:00-16:30 [%.4fp/kWh]"
                Text icon=energy item=Agile_Price_81 label="Tomorrow at 16:30-17:00 [%.4fp/kWh]"
                Text icon=energy item=Agile_Price_82 label="Tomorrow at 17:00-17:30 [%.4fp/kWh]"
                Text icon=energy item=Agile_Price_83 label="Tomorrow at 17:30-18:00 [%.4fp/kWh]"
                Text icon=energy item=Agile_Price_84 label="Tomorrow at 18:00-18:30 [%.4fp/kWh]"
                Text icon=energy item=Agile_Price_85 label="Tomorrow at 18:30-19:00 [%.4fp/kWh]"
                Text icon=energy item=Agile_Price_86 label="Tomorrow at 19:00-19:30 [%.4fp/kWh]"
                Text icon=energy item=Agile_Price_87 label="Tomorrow at 19:30-20:00 [%.4fp/kWh]"
                Text icon=energy item=Agile_Price_88 label="Tomorrow at 20:00-20:30 [%.4fp/kWh]"
                Text icon=energy item=Agile_Price_89 label="Tomorrow at 20:30-21:00 [%.4fp/kWh]"
                Text icon=energy item=Agile_Price_90 label="Tomorrow at 21:00-21:30 [%.4fp/kWh]"
                Text icon=energy item=Agile_Price_91 label="Tomorrow at 21:30-22:00 [%.4fp/kWh]"
                Text icon=energy item=Agile_Price_92 label="Tomorrow at 22:00-22:30 [%.4fp/kWh]"
                Text icon=energy item=Agile_Price_93 label="Tomorrow at 22:30-23:00 [%.4fp/kWh]"
                Text icon=energy item=Agile_Price_94 label="Tomorrow at 23:00-23:30 [%.4fp/kWh]"
                Text icon=energy item=Agile_Price_95 label="Tomorrow at 23:30-00:00: [%.4fp/kWh]"
            }

This requires a rule that runs every minute or so and updates the Minutes_through_day item:

rules.JSRule({
    name: "Set an item to current time to use in sitemap visbility conditions",
    description: "Change house Set an item to current time to use in sitemap visbility conditions",
    triggers: [triggers.GenericCronTrigger('0 * * * * ? *')],
    execute: data => {
        var logger = log(this.ruleUID);

        var zdtNow = time.toZDT();
        var minutes_through_day = (Number(zdtNow.hour()) * 60) + (Number(zdtNow.minute()));
        items.getItem("CurrentTime").sendCommand(time.toZDT());
        items.getItem("Minutes_through_day").postUpdate(minutes_through_day);
    }
  });

The above rule also updates the CurrentTime item which isn’t used anywhere in the following rules and can be deleted.

Now we have a sitemap section that can show the unit prices and will only show prices from now onward.

Next, we have to populate all those items. The following runs at when the JSON is updated (maybe this should be when it’s changed instead) and at 16:02 every day and also at 00:01 to shift the data from tomorrow’s (now today’s) items into today’s items:

rules.JSRule({
    name: "Get agile unit prices",
    description: "Get agile unit prices",
    triggers: [triggers.ItemStateUpdateTrigger('OctopusAgilePrices_AgilePricesJSON')],
    triggers: [triggers.GenericCronTrigger('0 2 16 * * ? *')],
    triggers: [triggers.GenericCronTrigger('0 1 0 * * ? *')],
    // triggers: [triggers.GenericCronTrigger('0 0/22 * * * ? *')],
    // triggers: [triggers.ItemStateChangeTrigger('OctopusAgileRefreshPrices')],
    execute: data => {
        var logger = log('OCTOPUS');
  
        logger.info("Octopus agile unit price forecast refresh rule...");
        var agileprices = String(items.getItem("OctopusAgilePrices_AgilePricesJSON").state);

        var obj = JSON.parse(agileprices);

        for (var i = 0, l = 99; i < l; i++) {
            var agilePriceItemName = String("Agile_Price_").concat(String(i));
            items.getItem(agilePriceItemName).sendCommand(0);
        }

        // var today = time.ZonedDateTime.now();             //now() returns time in local timezone
        var now_dayOfYear = time.ZonedDateTime.now().dayOfYear();
        var lowest_rate = 1000;
        var lowest_rate_index = 1000;
        var lowest_rate_valid_from;

        for (var i = obj.results.length - 1; i >= 0; i--) {
            var valid_from_ldt = time.toZDT(obj.results[i].valid_from).withZoneSameInstant(time.ZoneId.systemDefault()).toLocalDateTime();
            // logger.info("Valid_From time ({}) converted to local date time ({}).", String(obj.results[i].valid_from), String(valid_from_ldt));
            var valid_from_ldt_dayOfYear = valid_from_ldt.dayOfYear();
            if (valid_from_ldt_dayOfYear >= now_dayOfYear)
            {
                var item_day_index = Number(valid_from_ldt_dayOfYear - now_dayOfYear);
                var item_index = (item_day_index * 48) + (Number(valid_from_ldt.hour()) * 2) + (Number(valid_from_ldt.minute()) >= 30 ? 1 : 0);
                var agilePriceItemName = String("Agile_Price_").concat(String(item_index));
                items.getItem(agilePriceItemName).sendCommand(obj.results[i].value_inc_vat);
                // logger.info("Item name = ({}), valid_from = ({}), unit price = ({}).", agilePriceItemName, String(valid_from_ldt), obj.results[i].value_inc_vat);
            }
        }

        items.getItem("Agile_Price_Highest_Index").sendCommand(Number(item_index));
        
        logger.info("count = ({}).", String(obj.count));
        logger.info("number of results = ({}).", String(obj.results.length));
        // logger.info("Lowest rate = ({}) with index ({}) starting at ({}).", String(lowest_rate), String(lowest_rate_index), String(lowest_rate_valid_from));

    }
  });

I like to create log files for each “subject”/“area”/“topic”/howeveryou’dliketodescribeit so I created a log file called octopus.log. Logs files are configured in etc/log4j2.xml, in my Debian install this is in:

/var/lib/openhab/etc/log4j2.xml

I added two sections, an appender:

		<!-- Custom octopus rules -->
		<RollingRandomAccessFile append="true" fileName="${sys:openhab.logdir}/octopus.log" filePattern="${sys:openhab.logdir}/octopus.log.%i" immediateFlush="true" name="OCTOPUS">
                        <PatternLayout pattern="%d{HH:mm:ss} %m%n"/>
                        <Policies>
                                <SizeBasedTriggeringPolicy size="10 MB"/>
                        </Policies>
                        <DefaultRolloverStrategy max="10"/>
        </RollingRandomAccessFile>

and a logger:

		<!-- OCTOPUS -->
        <Logger additivity="false" level="DEBUG" name="org.openhab.automation.openhab-js.octopus">
                <AppenderRef ref="OCTOPUS"/>
        </Logger>

Now we have the unit prices copied to individual items and displayed in the sitemap. To help optimise usage of cheap periods of electricity we need to find the cheapest periods of electricity for various lengths of time. We can do this with the following rule:

rules.JSRule({
    name: 'Get cheapest agile unit price periods',
    description: 'Get cheapest agile unit price periods',
    triggers: [triggers.ItemStateChangeTrigger('OctopusAgilePrices_AgilePricesJSON')],
    triggers: [triggers.GenericCronTrigger('0 2 16 * * ? *')],
    triggers: [triggers.GenericCronTrigger('0 0/30 * * * ? *')],
    // triggers: [triggers.ItemStateChangeTrigger('OctopusAgileRefreshPrices')],
    execute: data => {
      var logger = log('OCTOPUS')
  
      logger.info('Get cheapest agile unit price periods rule...')
  
      var agileprices = String(items.getItem('OctopusAgilePrices_AgilePricesJSON').state)
      var obj = JSON.parse(agileprices)
  
      var highest_index = Number(items.getItem('Agile_Price_Highest_Index').state)
  
      var lowest_rate = 1000;
      var lowest_rate_index = 1000;
      var lowest_rate_valid_from;
      var h1_rate_lowest = 1000
      var h1_rate = 0
      var h1_rate_items = 0
      var h1_rate_lowest_index = 0
      var h1_5_rate_lowest = 1000
      var h1_5_rate = 0
      var h1_5_rate_items = 0
      var h3_rate_lowest = 1000
      var h3_rate = 0
      var h3_rate_items = 0
      var h4_rate_lowest = 1000
      var h4_rate = 0
      var h4_rate_items = 0
  
      for (var i = 0; i < obj.results.length - 1; i++) {
        if (time.toZDT(obj.results[i].valid_to).isAfter(time.ZonedDateTime.now()) != 0) {
          // logger.info("Loop index = ({}) value_inc_vat = ({}), valid_to = ({}), ZoneDateTime.now() = ({}).", String(i), String(obj.results[i].value_inc_vat), String(obj.results[i].valid_to), String(time.ZonedDateTime.now()));
  
          if ((obj.results[i].value_inc_vat < lowest_rate)) {
            lowest_rate_index = i;
            lowest_rate = obj.results[lowest_rate_index].value_inc_vat;
          }
          if (time.toZDT(obj.results[i].valid_to).isAfter(time.ZonedDateTime.now()) && time.toZDT(obj.results[i].valid_from).isBefore(time.ZonedDateTime.now()) ){
            // logger.info("Agile rate now is  ({}).", String(Number(obj.results[i].value_inc_vat)) / 100);
            items.getItem("ElectricityUnitRateNow").sendCommand(Number(obj.results[i].value_inc_vat) / 100);
          }
  
          h1_rate = h1_rate + obj.results[i].value_inc_vat
          h1_rate_items = h1_rate_items + 1
          h1_5_rate = h1_5_rate + obj.results[i].value_inc_vat
          h1_5_rate_items = h1_5_rate_items + 1
          h3_rate = h3_rate + obj.results[i].value_inc_vat
          h3_rate_items = h3_rate_items + 1
          h4_rate = h4_rate + obj.results[i].value_inc_vat
          h4_rate_items = h4_rate_items + 1
  
          if (h1_rate_items > 2) {
            h1_rate = h1_rate - obj.results[i - 2].value_inc_vat
            h1_rate_items = h1_rate_items - 1
          }
          if (h1_rate_items == 2) {
            if (h1_rate < h1_rate_lowest) {
              h1_rate_lowest = h1_rate
              h1_rate_lowest_index = i
            //   logger.info("1h lowest rate sum = ({}), index = ({}), valid_from = ({}).", String(h1_rate_lowest), String(i), String(time.toZDT(obj.results[i].valid_from).withZoneSameInstant(time.ZoneId.systemDefault()).toLocalDateTime()));
            }
          }
  
          if (h1_5_rate_items > 3) {
            h1_5_rate = h1_5_rate - obj.results[i - 3].value_inc_vat
            h1_5_rate_items = h1_5_rate_items - 1
          }
          if (h1_5_rate_items == 3) {
            if (h1_5_rate < h1_5_rate_lowest) {
              h1_5_rate_lowest = h1_5_rate
              h1_5_rate_lowest_index = i
            }
          }
  
          if (h3_rate_items > 6) {
            h3_rate = h3_rate - obj.results[i - 6].value_inc_vat
            h3_rate_items = h3_rate_items - 1
          }
          if (h3_rate_items == 6) {
            if (h3_rate < h3_rate_lowest) {
              h3_rate_lowest = h3_rate
              h3_rate_lowest_index = i
            }
          }
  
          if (h4_rate_items > 8) {
            h4_rate = h4_rate - obj.results[i - 8].value_inc_vat
            h4_rate_items = h4_rate_items - 1
          }
          if (h4_rate_items == 8) {
            if (h4_rate < h4_rate_lowest) {
              h4_rate_lowest = h4_rate
              h4_rate_lowest_index = i
            }
          }
        }
      }
  
      var h4_rate_start_time_local = time.toZDT(obj.results[h4_rate_lowest_index].valid_from).withZoneSameInstant(time.ZoneId.systemDefault()).toLocalDateTime();
      var h3_rate_start_time_local = time.toZDT(obj.results[h3_rate_lowest_index].valid_from).withZoneSameInstant(time.ZoneId.systemDefault()).toLocalDateTime();
      var h1_5_rate_start_time_local = time.toZDT(obj.results[h1_5_rate_lowest_index].valid_from).withZoneSameInstant(time.ZoneId.systemDefault()).toLocalDateTime();
      var h1_rate_start_time_local = time.toZDT(obj.results[h1_rate_lowest_index].valid_from).withZoneSameInstant(time.ZoneId.systemDefault()).toLocalDateTime();
      var lowest_rate_start_time_local = time.toZDT(obj.results[lowest_rate_index].valid_from).withZoneSameInstant(time.ZoneId.systemDefault()).toLocalDateTime();
  
      items.getItem('Agile_Price_Lowest_0_5h_Index').sendCommand(lowest_rate_index)
      items.getItem("Agile_Price_Lowest_0_5h_Time").sendCommand(String(lowest_rate_start_time_local));
      items.getItem("Agile_Price_Lowest_0_5h_Price").sendCommand(Number(lowest_rate) / 100);
  
      items.getItem('Agile_Price_Lowest_1h_Index').sendCommand(h1_rate_lowest_index)
      items.getItem('Agile_Price_Lowest_1h_Time').sendCommand(String(h1_rate_start_time_local))
      items.getItem('Agile_Price_Lowest_1h_Avg_Price').sendCommand(Number(h1_rate_lowest / 2 / 100))
  
      items.getItem('Agile_Price_Lowest_1_5h_Index').sendCommand(h1_5_rate_lowest_index)
      items.getItem('Agile_Price_Lowest_1_5h_Time').sendCommand(String(h1_5_rate_start_time_local))
      items.getItem('Agile_Price_Lowest_1_5h_Avg_Price').sendCommand(Number(h1_5_rate_lowest / 3 / 100))
  
      items.getItem('Agile_Price_Lowest_3h_Index').sendCommand(h3_rate_lowest_index)
      items.getItem('Agile_Price_Lowest_3h_Time').sendCommand(String(h3_rate_start_time_local))
      items.getItem('Agile_Price_Lowest_3h_Avg_Price').sendCommand(Number(h3_rate_lowest / 6 / 100))
  
      items.getItem('Agile_Price_Lowest_4h_Index').sendCommand(h4_rate_lowest_index)
      items.getItem('Agile_Price_Lowest_4h_Time').sendCommand(String(h4_rate_start_time_local))
      items.getItem('Agile_Price_Lowest_4h_Avg_Price').sendCommand(Number(h4_rate_lowest / 8 / 100))
  
      logger.info('Lowest 4 hour rate index = ({}), valid from UTC ({}), valid from local ({}), unit price = ({}).', String(h4_rate_lowest_index), String(obj.results[h4_rate_lowest_index].valid_from), String(h4_rate_start_time_local), String(h4_rate_lowest / 8))
      logger.info('Lowest 3 hour rate index = ({}), valid from ({}), valid from local ({}), unit price = ({}).', String(h3_rate_lowest_index), String(obj.results[h3_rate_lowest_index].valid_from), String(h3_rate_start_time_local), String(h3_rate_lowest / 6))
      logger.info('Lowest 1.5 hour rate index = ({}), valid from ({}), valid from local ({}), unit price = ({}).', String(h1_5_rate_lowest_index), String(obj.results[h1_5_rate_lowest_index].valid_from), String(h1_5_rate_start_time_local), String(h1_5_rate_lowest / 3))
      logger.info('Lowest 1 hour rate index = ({}), valid from ({}), valid from local ({}), unit price = ({}).', String(h1_rate_lowest_index), String(obj.results[h1_rate_lowest_index].valid_from), String(h1_rate_start_time_local), String(h1_rate_lowest / 2))
      logger.info('Lowest unit rate index = ({}), valid from ({}), valid from local ({}), unit price = ({}).', String(lowest_rate_index), String(obj.results[lowest_rate_index].valid_from), String(lowest_rate_start_time_local), String(lowest_rate))
    }
  })

There’s some scope for that rule to be optimised, there’s a lot of repeated code but at this point I’m not hugely bothered about tidying it up. This rule works by looping through the unit prices in the JSON data and summing the unit prices for various periods of time. h1_rate contains the sum of two consecutive unit prices starting from the unit price valid now through to the end of the data. After a third unit price is added the price two units prior is subtracted and the new sum is compared with the cheapest two unit sum so far. Imagine a spreadsheet with the unit prices stored in a horizontal line and a window two columns wide slides across the line. As the window progresses to the next column the value in that column is added and the value in the column that just disappeared from sight is subtracted. The JSON element index of the cheapest two units is stored and used to get the valid_from element after the loop so we can store the time of the cheapest period in the Agile_Price_Lowest_1h_Time item. The same pieces of code are repeated to get 1.5h, 3h and 4h period data.

A note about the local timestamp processing: the timestamps in the JSON data is provided in UTC format as indicated by the Z at the end of the timestamps. This needs to be converted to the local time to cope with the UK being in British Summer Time. This is done as follows:

time.toZDT(obj.results[h1_rate_lowest_index].valid_from).withZoneSameInstant(time.ZoneId.systemDefault()).toLocalDateTime()

The method withZoneSameInstant(time.ZoneId.systemDefault()) converts the UTC timestamp to the local timezone such as 2024-04-01T22:30+01:00[SYSTEM]. This format cannot be used to update a DateTime item, the [SYSTEM] part causes an error. To remove the [SYSTEM] we call the method toLocalDateTime(). The output can then be used to update an item.

We divide the unit rates by 100 because the unit of measure for the EnergyPrice items is GBP/kWh but the price in the JSON file is in pence per hour.

At this point we have the prices in items, the items are on the sitemap and now we have the cheapest periods and average prices in items too. We need to display them in the sitemap:


            Text icon=energy label="Electricity Cheapest Times" {
                Default icon=price item=ElectricityConsumptionCostToday label="Electricity cost today [%.2f GBP]"
                Default icon=energy item=ElectricityUnitRateNow label="Rate now [%.6f GBP/kWh]"
                Default icon=energy item=Agile_Price_Lowest_0_5h_Price label="Cheapest 30min rate [%.6f GBP/kWh]"
                Default icon=time item=Agile_Price_Lowest_0_5h_Time label="Cheapest 30min start time [%1$tH:%1$tM %1$td.%1$tm]"
                Default icon=energy item=Agile_Price_Lowest_1h_Avg_Price label="Cheapest 1h rate [%.6f GBP/kWh]"
                Default icon=time item=Agile_Price_Lowest_1h_Time label="Cheapest 1h rate start time [%1$tH:%1$tM %1$td.%1$tm]"
                Default icon=energy item=Agile_Price_Lowest_1_5h_Avg_Price label="Cheapest 1.5h rate [%.6f GBP/kWh]"
                Default icon=time item=Agile_Price_Lowest_1_5h_Time label="Cheapest 1.5h start time [%1$tH:%1$tM %1$td.%1$tm]"
                Default icon=energy item=Agile_Price_Lowest_3h_Avg_Price label="Cheapest 3h rate [%.6f GBP/kWh]"
                Default icon=time item=Agile_Price_Lowest_3h_Time label="Cheapest 3h start time [%1$tH:%1$tM %1$td.%1$tm]"
                Default icon=energy item=Agile_Price_Lowest_4h_Avg_Price label="Cheapest 4h rate [%.6f GBP/kWh]"
                Default icon=time item=Agile_Price_Lowest_4h_Time label="Cheapest 4h start time [%1$tH:%1$tM %1$td.%1$tm]"
            }

At the time of writing this section of the sitemap looks like:

What is that in the top left though? Electricity cost today? Neat! How was that calculated?

Well…

The SMETS2 smart meter uses zigbee to communicate with the IHD (In-Home Display) which is a small display that can display data from the smart meter such as the consumption today, the current rate of consumption, etc. Unfortunately, there’s no way to pair the meter with our own zigbee controller and although we can configure the IHD to use our wifi network it only uses that to post data to the cloud and receive firmware updates. However, there is an IHD from Glowmartk that connects to the smart meter and then provides the data from the meter to an MQTT server. The IHD can bought here. At the time of writing it costs £69.99 + delivery. I ordered mine on Friday afternoon and it arrived the following Wednesday.

I already have an MQTT server on my LAN so all I needed to do was configure the wifi and MQTT server details on the IHD and then create a generic MQTT thing:

We need to link an item to a channel to get the electricity consumption in kWh from the meter:

I’ve redacted the MAC address of my meter from the MQTT state topic. You’ll be able to see the topic using something like MQTT Explorer in Windows.

The channel also needs the JSON path to the element that contains the total power consumption so click on the Show advanced checkbox and scroll down to the Incoming Value Transformations field:

Once we’ve linked an item to the channel we’ll now have the consumption stored in an item every 10 seconds. The item needs to be a Number:Energy item:

We can then use this item in a rule that is run at zero minutes and 30 minutes past every hour:

rules.JSRule({
    name: "Calculate electricity cost today",
    description: "Calculate electricity cost today",
    // triggers: [triggers.ItemStateChangeTrigger('OctopusAgileRefreshPrices')],
    // triggers: [triggers.ItemStateChangeTrigger('OctopusAgilePrices_AgilePricesJSON')],
    // triggers: [triggers.ItemStateChangeTrigger('Agile_Price_0')],
    triggers: [triggers.GenericCronTrigger('0 0/30 * * * ? *')],   //0 0/30 0 ? * * *
    execute: data => {
        var logger = log('OCTOPUS');
        var QuantityType = Java.type('org.openhab.core.library.types.QuantityType');

        logger.info("Calculate electricity cost today...");

        var time_now = time.ZonedDateTime.now();

        // logger.info("Hour = ({}), minute ({}).", String(Number(time_now.hour())), String(Number(time_now.minute())));
        if ((Number(time_now.hour()) == 0) && (Number(time_now.minute() < 1))) {
            items.getItem("ElectricityConsumptionCostTodayHistory").sendCommand(items.getItem("ElectricityConsumptionCostToday").state);
            items.getItem("ElectricityConsumptionCostToday").sendCommand("0.4977");
            logger.info("Resetting cost today...");
        }
        var last_run_time = time.toZDT(items.getItem("ElectricityConsumptionCalcLastRunTime").state);
        logger.info("Last run time = ({}).", String(last_run_time));
        if (last_run_time == null){
            last_run_time = time.toZDT(time_now);
        }
        var item_index = (Number(last_run_time.hour()) * 2) + (Number(last_run_time.minute()) >= 30 ? 1 : 0);
        var agilePriceItemName = String("Agile_Price_").concat(String(item_index));
        var divisor = Quantity('100');
        var agileunitprice = new QuantityType(String(items.getItem(agilePriceItemName).rawState)).divide(divisor);
        // logger.info("Unit price = ({}).", String(agileunitprice));
        // logger.info("Unit price index = ({}).", String(item_index));

        var total_to_now = new QuantityType(String(items.getItem("ElectricityMeterGlow_Total_power_imported").rawState));
        if (total_to_now == null){
            total_to_now = 0;
        }
        var total_to_last_period = new QuantityType(String(items.getItem("ElectricityConsumptionTodayToLastPeriod").rawState));
        if (total_to_last_period == null){
            total_to_last_period = 0;
        }

        var consumption_delta = total_to_now.subtract(total_to_last_period);
        var consumption_delta_cost = consumption_delta.multiply(agileunitprice);
        var consumption_cost_to_last_period = new QuantityType(String(items.getItem("ElectricityConsumptionCostToday").rawState));
        var consumption_cost_today = consumption_cost_to_last_period.add(consumption_delta_cost);
        items.getItem("ElectricityConsumptionCostToday").sendCommand(consumption_cost_today);
        items.getItem("ElectricityConsumptionTodayToLastPeriod").sendCommand(total_to_now);
        items.getItem("ElectricityConsumptionCalcLastRunTime").sendCommand(time_now);

        logger.info("Total to now ({}), total to last period = ({}), delta consumption = ({}), unit price = ({}).", String(total_to_now), String(total_to_last_period), String(consumption_delta), String(agileunitprice));
        logger.info("Consumption cost to last period = ({}), consumption delta cost = ({}), total cost today = ({}).", String(consumption_cost_to_last_period), String(consumption_delta_cost), String(consumption_cost_today));
    }
});

This rule uses some items that I created in the UI rather than in items files. If the rule runs with one minute past midnight the ElectricityConsumptionCostTodayHistory item is set to the ElectricityConsumptionCostToday item to persist the cost at the end of every day. I persist this in influxDB. The ElectricityConsumptionCostToday item is then set to 0.4977 which is the daily standing charge.

The ElectricityConsumptionCalcLastRunTime item contains the timestamp of the last time the rule ran. This is the used to build the name of the item that contains the unit price for the past 30 minutes. We use the rawState and divide by 100 stored as a Quantity type (the item contains pence per kWh but we want to store the consumption in GBP) because these items are quantity items, not number items.

Since we are dealing with quantity items we can’t use math operators like + and -, instead we have to use the .subtract, .multiply and .add methods of the quantity items as we can see here:

        var consumption_delta = total_to_now.subtract(total_to_last_period);
        var consumption_delta_cost = consumption_delta.multiply(agileunitprice);
        var consumption_cost_to_last_period = new QuantityType(String(items.getItem("ElectricityConsumptionCostToday").rawState));
        var consumption_cost_today = consumption_cost_to_last_period.add(consumption_delta_cost);

Finally we update the ElectricityConsumptionCostToday, ElectricityConsumptionTodayToLastPeriod and ElectricityConsumptionCalcLastRunTime items.

I’ve also found it useful to see the cost of the current rate of consumption. This makes it very obvious how (relatively) expensive it is to turn the oven on in that peak period between 16:00 and 20:00. To do this we need to get the current power consumption from the IHD via MQTT:

Again, click on the Show advanced checkbox and set the incoming JSON path:

And link the channel to an item:

The item UoM is W but in the channel I set the UoM to kW for consistency.

The rule to calculate the consumption cost is as follows:


  
rules.JSRule({
    name: "Calculate electricity cost instantaneous",
    description: "Calculate electricity cost instantaneous",
    triggers: [triggers.ItemStateChangeTrigger('ElectricityMeterGlow_power')],
    execute: data => {
        var logger = log('OCTOPUS');;
        var QuantityType = Java.type('org.openhab.core.library.types.QuantityType');;

        // logger.info("Calculate electricity cost instaneous...");

        // var instantaneous_consumption = new QuantityType(String(items.getItem("ElectricityMeterGlow_power").rawState));
        var instantaneous_consumption_as_kWh = new QuantityType(String(items.getItem("ElectricityMeterGlow_power").numericState) + ' kWh');
        var unit_rate = new QuantityType(String(items.getItem("ElectricityUnitRateNow").rawState));
        var ElectricityCostInstantaneous = instantaneous_consumption_as_kWh.multiply(unit_rate);

        items.getItem("ElectricityCostInstantaneous").sendCommand(ElectricityCostInstantaneous);

        // logger.info("Instantaneous consumption as number ({}), rate_as_kWh = ({}).", String(instantaneous_consumption_num));
        // logger.info("Instantaneous consumption as kWh = ({}), unit_rate = ({}), instantaneous cost = ({}).", String(instantaneous_consumption_as_kWh), String(unit_rate), String(ElectricityCostInstantaneous));
    }
});

We have to change the unit of measure here from power to energy. The assumption we’re making is that the current rate of consumption is the current power maintained for an hour, kW → kWh. We do this by getting the numericState of the power item, converting to a string, appending " kWh" to it adn then putting the resulting string in a QuantityType. We then mutiply that quantity by the current unit rate using the .mutiply method.

We can display these last few items in the sitemap as follows:

        Text icon=energy item=ElectricityMeterGlow_power label="House consumption now [%.3f kW]"
        Text icon=price item=ElectricityConsumptionCostToday label="Electricity cost today [£%.2f]"
        Text icon=price item=ElectricityConsumptionCostTodayHistory label="Electricity cost yesterday [£%.2f]"
        Text icon=price item=ElectricityCostInstantaneous label="Electricity cost per hour [£%.2f]"

The last piece of data that I’d like to display is the future prices as a chart. At the time of writing charts in sitemaps can show past data, hopefully in the future they’ll be able to also show future data. The recently added timeseries functionality allows us to persist future data. It’s not possible to update timeseries using JS scripting yet but it can be done in ruby:

require "json"

rule "Persist Time Series" do
  changed OctopusAgilePrices_AgilePricesJSON
  run do |event|
    time_series = TimeSeries.new

    logger.info "Triggered item: #{event.item.name}"

    data = JSON.parse(OctopusAgilePrices_AgilePricesJSON.state.to_s)
    data["results"].each do |element|
      timestamp = ZonedDateTime.parse(element["valid_from"])
      time_series.add(timestamp, element["value_inc_vat"])
    end

    Agile_Price_Forecast.time_series = time_series
  end
end

The data is now persisted, I just can’t display it yet! :slight_smile:

I’ve created an enhancement issue on github here, hopefully we’ll be able to show future data in charts in the… future. :slight_smile:

Many thanks to @jimt, @joehil.jh, @florian-h05 and everyone else whose posts helped me immensely while I wrote the above rules.

4 Likes

Thanks for posting!

If you ever decide to take this to the next level, this sort of thing is perfect for rule templates. You could create the Items from the rule and use sendHttpGetRequest to pull the JSON to make it more self contained. Then users would just need to install and configure it instead of copy/paste/edit.

For MainUI, a repeater card would be perfect for displaying the prices. The minutes in the day Item wouldn’t be required because you can do date time comparisons in MainUI expressions.

I’ve never used a rule template before but I’ll have a look at what’s involved. I like the sound of being able to install something more easily than copy and pasting code.

You can see many examples in the Marketplace, and a tutorial Creating Capabilities with Rule Templates: Time of Day (it’s OH 3 but still relevant) and for what you can do with properties see [Do not install] Rule Template Parameters Experiments and Examples.

One thing that I find to be particularly useful when writing rule templates is to make it smart enough to do different things based on how it’s triggered. For example when Debounce [4.0.0.0;4.9.9.9] is manually triggered it checks to make sure the rule and associated Items are configured correctly and generates meaningful and actionable errors if not. When triggered by an Item event it debounces it. This can be a powerful way to combine rules where it makes sense (e.g. MQTT Event Bus [4.0.0.0;4.9.9.9] is able to combine publish and subscribe into one rule instead of two).

Unfortunately any one posting on the marketplace is limited to a single rule template making it challenging to publish a suite of templates that work together (not impossible, you can always have more than one posting).

And of course, ask if you run into trouble. I’m a big advocate of rule templates because OH is better for everyone when users can install/configure instead of needing to touch code for everything.