This document describes the JSON structure provided by the Agile Rates API. The API offers electricity pricing data obtained from Epex Spot and Nord Pool that are relevant for UK users, particularly those on Octopus Energy's Agile tariffs.
The data is refreshed twice daily.
New rates typically become available shortly after these times for the following day (00:00 to 23:59).
The underlying wholesale pricing data is sourced from Epex Spot and Nord Pool.
The copyright for the raw wholesale data belongs to these respective organisations. Please refer to
the copyright
section within the JSON for specific terms.
This API and the resulting JSON data are provided as-is for personal, non-commercial use only, without any guarantees of availability or accuracy. It is intended for integration into personal home automation systems (like Home Assistant) or for informational purposes. Commercial use is strictly forbidden.
All rates presented in this API (Agile Import, Agile Outgoing, averages) are in pence per kilowatt-hour (p/kWh).
The API provides separate JSON files for each UK electricity supply region. You can identify your region using region code or by name.
Love this site? Thinking of making the switch to greener, smarter energy? Planning to make a good use of the Octopus Agile pricing model?
Use my personal referral code when you sign up: rich-zebra-920
You'll get £50 free credit on your first bill, and I'll get £50 too. It's a fantastic way to start your Octopus journey and helps support this site!
The root of the JSON response contains several key-value pairs providing metadata and the core rate information.
{
"result": "ok", // Indicates the overall status of the API request
"regionInfo": { ... }, // Information about the specific region
"lastUpdate": { ... }, // Timestamps related to data freshness
"tradingDate": { ... }, // Information about the source data trading dates
"copyright": { ... }, // Copyright notices for the source data
"rates": [ ... ], // Array of electricity rates per 30-minute slot
"cheapestWindowsPerSlot": { ... }, // Calculated cheapest import windows
"windowsExportPerSlot": { ... } // Calculated most expensive export windows
}
regionInfo
ObjectThis object contains details specific to the electricity supply region covered by the JSON file.
"regionInfo": {
"name": "East England", // Human-readable name of the region
"code": "A", // Single-letter code for the region
"mpan": 10, // First two digits of the MPAN number for this region
// Parameters for Octopus Agile Outgoing tariff calculation:
"B": 1.09, // Balancing Service Use of System (BSUoS) charge (pence/kWh)
"C": 7.04, // Transmission Network Use of System (TNUoS) charge (pence/kWh) - used during peak hours (4pm-7pm)
"M": 0.95, // Wholesale market non-commodity cost multiplier
// Parameters for Octopus Agile tariff calculation:
"D": 2.1, // Distribution Network Use of System (DUoS) charge (pence/kWh)
"P": 13 // Peak time DUoS charge adder - used during peak hours (4pm-7pm)
}
M * W + B + C
to
calculate the Octopus Agile Outgoing export rate based on wholesale prices. 'C' is
applied during the peak hours of 4 PM to 7 PM. More details can be found on the Octopus Energy Outgoing FAQs.min(D x W + P, 95)
to
calculate the Octopus Agile import rate. 'D' represents regional distribution costs, and 'P' is
a value added to 'D' during the peak hours of 4 PM to 7 PM. More details can be found on
the Octopus Energy Agile Pricing Explained blog post.lastUpdate
ObjectProvides timestamps indicating when the various data sources were last fetched or updated by the Agile Rates API system.
"lastUpdate": {
"cache": { // Timestamps when the data was last successfully cached by this API
"epexSpot": {
"GB_30-call-GB": "2025-04-08T16:00:09Z", // Last cache time for Epex Spot 30-min auction data
"GB_GB": "2025-04-08T10:00:06Z" // Last cache time for Epex Spot 60-min auction data
},
"nordPool": {
"N2EX_DayAhead_UK_GBP": "2025-04-08T10:00:06Z" // Last cache time for Nord Pool 60-min auction data
},
"agileRates": "2025-04-09T07:16:58Z" // Last time this specific JSON file was generated/updated
},
"sources": { // Timestamps provided by the original data sources themselves, indicating when data for a specific delivery day was published
"epexSpot": {
"GB_30-call-GB": { // Epex Spot 30-min auction
"2025-04-08": "2025-04-07T14:45:40Z", // Data for delivery on 2025-04-08 was published on 2025-04-07 at 14:45:40Z
"2025-04-09": "2025-04-08T14:45:37Z" // Data for delivery on 2025-04-09 was published on 2025-04-08 at 14:45:37Z
},
"GB_GB": { // Epex Spot 60-min auction
"2025-04-08": "2025-04-07T08:31:17Z",
"2025-04-09": "2025-04-08T08:30:45Z"
}
},
"nordPool": {
"N2EX_DayAhead_UK_GBP": { // Nord Pool 60-min auction
"2025-04-08": "2025-04-07T08:59:11.0813743Z",
"2025-04-09": "2025-04-08T08:58:55.8552498Z"
}
}
}
}
Timestamps are in ISO 8601 format (UTC).
tradingDate
ObjectContains metadata about the source data files used, linking trading dates (when the auction occurred) to delivery dates (when the electricity is used) and providing source URLs.
"tradingDate": {
"epexSpot": {
"GB_30-call-GB": [ // Epex Spot 30-min auction data sources
{
"2025-04-07": { // Trading Date
"deliveryDate": "2025-04-08", // Delivery Date corresponding to the trading date
"lastUpdate": "2025-04-07T14:45:40Z", // Timestamp when this data was last updated by the source
"source": "https://www.epexspot.com/en/market-results?market_area=GB&auction=30-call-GB&trading_date=2025-04-07&delivery_date=2025-04-08&..." // URL to the source data on Epex Spot website
}
},
{
"2025-04-08": { // Trading Date
"deliveryDate": "2025-04-09", // Delivery Date
"lastUpdate": "2025-04-08T14:45:37Z",
"source": "https://www.epexspot.com/en/market-results?market_area=GB&auction=30-call-GB&trading_date=2025-04-08&delivery_date=2025-04-09&..."
}
}
],
"GB_GB": [ // Epex Spot 60-min auction data sources
{
"2025-04-07": {
"deliveryDate": "2025-04-08",
"lastUpdate": "2025-04-07T08:31:17Z",
"source": "https://www.epexspot.com/en/market-results?market_area=GB&auction=GB&trading_date=2025-04-07&delivery_date=2025-04-08&..."
}
},
{
"2025-04-08": {
"deliveryDate": "2025-04-09",
"lastUpdate": "2025-04-08T08:30:45Z",
"source": "https://www.epexspot.com/en/market-results?market_area=GB&auction=GB&trading_date=2025-04-08&delivery_date=2025-04-09&..."
}
}
]
},
"nordPool": {
"N2EX_DayAhead_UK_GBP": [ // Nord Pool 60-min auction data sources
{
"2025-04-07": { // Trading Date
"deliveryDate": "2025-04-08", // Delivery Date
"lastUpdate": "2025-04-07T08:59:11.0813743Z",
"source": "https://dataportal-api.nordpoolgroup.com/api/DayAheadPrices" // URL to the Nord Pool API endpoint
}
},
{
"2025-04-08": { // Trading Date
"deliveryDate": "2025-04-09", // Delivery Date
"lastUpdate": "2025-04-08T08:58:55.8552498Z",
"source": "https://dataportal-api.nordpoolgroup.com/api/DayAheadPrices"
}
}
]
}
}
copyright
ObjectContains important copyright and usage information regarding the underlying data from Epex Spot and Nord Pool.
"copyright": {
"epexSpot": "This is a not-for-profit proxy service intended for exclusive personal, non-commercial use and is provided as-is without any guarantees.\nEPEX SPOT SE is not associated in any way with this proxy service.\nEPEX SPOT SE is the owner of all the data retrieved from the EPEX SPOT SE website.\nContent of this data proxied from EPEX SPOT SE website only to be used for internal reasons, in line with the licence for the use of the EPEX SPOT SE website links above (https://www.epexspot.com/en/UseofWebsite).\nCommercial usage of this service is forbidden. If you require to use this data for commercial purposes, you must use EPEX SPOT SE's commercial offering.",
"nordPool": "This is a not-for-profit proxy service intended for exclusive personal, non-commercial use and is provided as-is without any guarantees.\nNord Pool AS is not associated in any way with this proxy service.\nNord Pool AS is the owner of all the data retrieved from the Nord Pool AS API.\nContent of this data proxied from Nord Pool AS API only to be used for internal reasons, in line with the licence for the use of the Nord Pool AS API links above (https://www.nordpoolgroup.com/4ac608/globalassets/download-center/rules-and-regulations/api-user-agreement---api-general-terms-valid-from-16.12.2023.pdf).\nCommercial usage of this service is forbidden. If you require to use this data for commercial purposes, you must use Nord Pool AS's commercial offering."
}
It emphasises the non-commercial, personal use limitation and directs users needing commercial data access to the official offerings from Epex Spot and Nord Pool.
rates
ArrayThis is the core of the API response, containing an array of objects, where each object represents a 30-minute time slot and includes the underlying wholesale prices and the calculated Octopus Agile import and export rates.
Important - All calculated rates (agileRate.result.rate
,
agileOutgoingRate.result.rate
, and rates within sources
sub-objects) are
in pence per kWh (p/kWh) and include VAT where applicable according to Octopus
Energy's pricing structure.
"rates": [
{ // Object representing the first 30-minute slot of the day
"deliveryStart": "2025-04-08T00:00:00Z", // Start time of the slot (ISO 8601 UTC)
"deliveryEnd": "2025-04-08T00:30:00Z", // End time of the slot (ISO 8601 UTC)
// Raw wholesale prices (as pence/kWh) obtained from the sources for this slot.
// Note: 60-min auction prices (GB_GB, N2EX_DayAhead_UK_GBP) apply to both 30-min slots within the hour.
"epexSpot": {
"GB_30-call-GB": 77.66, // Epex Spot 30-min auction price (p/kWh)
"GB_GB": 72.87 // Epex Spot 60-min auction price (p/kWh)
},
"nordPool": {
"N2EX_DayAhead_UK_GBP": 70.09 // Nord Pool 60-min auction price (p/kWh)
},
// Calculated Octopus Agile import rate for this slot
"agileRate": {
"result": {
// Indicates which wholesale source was used for the final rate calculation.
// Typically the highest price among the available sources for that slot, according to Octopus Agile methodology.
"source": "epexspot_GB_30-call-GB",
// Boolean indicating if this rate is based on predicted/forecasted data. prediction = true IF we don't use 30 min slots for calculation.
"prediction": false,
// The final calculated Octopus Agile import rate (p/kWh, incl. VAT, DUoS charges (D, P)).
"rate": 17.12
},
// Shows what the Agile import rate *would* be if calculated using each source individually. Useful for comparison.
"sources": {
"epexSpot": {
"GB_30-call-GB": 17.12, // Agile rate if calculated from Epex 30-min price
"GB_GB": 16.07 // Agile rate if calculated from Epex 60-min price
},
"nordPool": {
"N2EX_DayAhead_UK_GBP": 15.46 // Agile rate if calculated from Nord Pool 60-min price
}
}
},
// Calculated Octopus Agile Outgoing export rate for this slot
"agileOutgoingRate": {
"result": {
// Indicates which wholesale source was used for the final export rate calculation.
// Typically follows the same source logic as the import rate.
"source": "epexspot_GB_30-call-GB",
// Boolean indicating if this rate is based on predicted/forecasted data. prediction = true IF we don't use 30 min slots for calculation.
"prediction": false,
// The final calculated Octopus Agile Outgoing export rate (p/kWh, incl. adjustments B, C, M).
"rate": 8.89
},
// Shows what the Agile Outgoing rate *would* be if calculated using each source individually.
"sources": {
"epexSpot": {
"GB_30-call-GB": 8.89, // Outgoing rate if calculated from Epex 30-min price
"GB_GB": 8.41 // Outgoing rate if calculated from Epex 60-min price
},
"nordPool": {
"N2EX_DayAhead_UK_GBP": 8.14 // Outgoing rate if calculated from Nord Pool 60-min price
}
}
}
},
{ // Object representing the second 30-minute slot
"deliveryStart": "2025-04-08T00:30:00Z",
"deliveryEnd": "2025-04-08T01:00:00Z",
"epexSpot": {
"GB_30-call-GB": 78.09,
"GB_GB": 72.87 // Note: Same 60-min price as the previous slot
},
"nordPool": {
"N2EX_DayAhead_UK_GBP": 70.09 // Note: Same 60-min price as the previous slot
},
"agileRate": {
"result": { "source": "epexspot_GB_30-call-GB", "prediction": false, "rate": 17.22 },
"sources": { "epexSpot": { "GB_30-call-GB": 17.22, "GB_GB": 16.07 }, "nordPool": { "N2EX_DayAhead_UK_GBP": 15.46 } }
},
"agileOutgoingRate": {
"result": { "source": "epexspot_GB_30-call-GB", "prediction": false, "rate": 8.93 },
"sources": { "epexSpot": { "GB_30-call-GB": 8.93, "GB_GB": 8.41 }, "nordPool": { "N2EX_DayAhead_UK_GBP": 8.14 } }
}
},
// ... more rate objects for subsequent 30-minute slots ...
]
cheapestWindowsPerSlot
ObjectThis object provides pre-calculated information about the cheapest consecutive periods (windows) for
electricity import (using the agileRate
), starting from each 30-minute slot.
The keys of the main object are the start times (ISO 8601 UTC) of each 30-minute slot for which calculations are available. The value for each start time is another object where keys represent the duration of the window in minutes (e.g., "60", "120", "180", etc.).
This is useful for scheduling high-consumption appliances (like EV charging or washing machines) to run during the most cost-effective periods.
"cheapestWindowsPerSlot": {
// Calculations starting from the 07:00:00 slot on 2025-04-09
"2025-04-09T07:00:00Z": {
// Cheapest 60-minute (2 slots) window starting at or after 07:00:00
"60": {
"startTime": "2025-04-09T12:30:00Z", // Start time of the cheapest 60-min window
"endTime": "2025-04-09T13:30:00Z", // End time of the cheapest 60-min window
"averageRate": 16.05, // Average agileRate (p/kWh) during this window
"prediction": false // Whether this calculation involves predicted rates
},
// Cheapest 120-minute (4 slots) window starting at or after 07:00:00
"120": {
// ... similar structure ...
},
"180": { /* ... */ },
"240": { /* ... */ },
"300": { /* ... */ },
"360": { /* ... */ }
},
// Calculations starting from the 07:30:00 slot on 2025-04-09
"2025-04-09T07:30:00Z": {
// Cheapest 60-minute window starting at or after 07:30:00
"60": {
"startTime": "2025-04-09T12:30:00Z", // It might find the same window as the previous start slot
"endTime": "2025-04-09T13:30:00Z",
"averageRate": 16.05,
"prediction": false
},
"120": { /* ... */ },
"180": { /* ... */ },
"240": { /* ... */ },
"300": { /* ... */ },
"360": { /* ... */ }
},
// ... more entries for subsequent start slots ...
}
windowsExportPerSlot
ObjectSimilar in structure to cheapestWindowsPerSlot
, this object provides pre-calculated
information about the most expensive consecutive periods (windows) for electricity
export (using the agileOutgoingRate
), starting from each 30-minute slot.
The keys and structure mirror cheapestWindowsPerSlot
, but the averageRate
represents the average agileOutgoingRate
during the identified window, aiming to find
the most profitable time to export energy (e.g., from solar panels or home batteries).
This helps users maximise earnings when exporting electricity back to the grid on the Agile Outgoing tariff.
"windowsExportPerSlot": {
// Calculations starting from the 07:00:00 slot on 2025-04-09
"2025-04-09T07:00:00Z": {
// Most expensive (highest export rate) 60-minute window starting at or after 07:00:00
"60": {
"startTime": "2025-04-09T17:00:00Z", // Start time of the most expensive 60-min window for export
"endTime": "2025-04-09T18:00:00Z", // End time of the window
"averageRate": 19.29, // Average agileOutgoingRate (p/kWh) during this window
"prediction": false // Whether this calculation involves predicted rates
},
// Most expensive 120-minute window
"120": {
// ... similar structure ...
},
"180": { /* ... */ },
"240": { /* ... */ },
"300": { /* ... */ },
"360": { /* ... */ }
},
// Calculations starting from the 07:30:00 slot on 2025-04-09
"2025-04-09T07:30:00Z": {
// Most expensive 60-minute window starting at or after 07:30:00
"60": {
"startTime": "2025-04-09T17:00:00Z",
"endTime": "2025-04-09T18:00:00Z",
"averageRate": 19.29,
"prediction": false
},
"120": { /* ... */ },
"180": { /* ... */ },
"240": { /* ... */ },
"300": { /* ... */ },
"360": { /* ... */ }
},
// ... more entries for subsequent start slots ...
}
This is a not-for-profit website and API intended for exclusive personal, non-commercial
use and is
provided "as-is" without any guarantees.
Commercial usage of this service and the data it contains
is strictly forbidden.
If you require this data for commercial purposes,
you
must use a commercial offering from EPEX SPOT SE or Nord Pool AS.
EPEX SPOT SE is not associated in any way with this service.
EPEX SPOT SE is the owner of all data retrieved from the EPEX SPOT SE website.
Content of the data retrieved from the EPEX SPOT SE website is only to be used for
internal
reasons, in line with the Epex Spot website terms and conditions.
Nord Pool AS is not associated in any way with this service.
Nord Pool AS is the owner of all data retrieved from the Nord Pool AS API.
Content of the data retrieved from the Nord Pool AS API is only to be used for internal
reasons, in line with the license for the use of the Nord Pool AS API links above (API terms and conditions).
Octopus Energy is not associated in any way with this service.
Octopus Energy is the owner of the formula used for
the
calculation of the Agile rate.
Here are examples for integrating the Agile Rates API into Home Assistant:
First, define a REST sensor to fetch the complete JSON data from the API for your region. This sensor will hold the entire dataset in its attributes.
# In your configuration.yaml or sensors.yaml
sensor:
- platform: rest
# IMPORTANT: Replace with the URL for YOUR region! This example uses London (C).
resource: https://agilerates.uk/api/agile_rates_region_C.json
name: "Agile Rates API Data"
# Use a simple value_template, we'll get data from attributes
value_template: "{{ value_json.result }}"
# Update every 15 minutes (900 seconds). Adjust as needed.
# Data only changes around 10am and 4pm, so frequent updates aren't strictly necessary.
scan_interval: 900
# Pull the main data structures into attributes
json_attributes:
- regionInfo
- lastUpdate
- rates
- cheapestWindowsPerSlot
- windowsExportPerSlot
After restarting Home Assistant, you should have a sensor called
sensor.agile_rates_api_data
. You can inspect its state and attributes in Developer
Tools -> States.
Now, create template sensors that extract specific useful information from the attributes of the
sensor.agile_rates_api_data
sensor.
# In your configuration.yaml or sensors.yaml under the 'template:' key
# Or in a separate template.yaml file if using includes
template:
- sensor:
# --- Current Agile Import Rate ---
- name: "Agile Current Import Rate"
# Unit is pence per kWh
unit_of_measurement: "p/kWh"
# Set device class to monetary for potential currency display (adjust currency if needed)
device_class: monetary
# Set state_class for long-term statistics
state_class: measurement
icon: mdi:cash-multiple
state: >
{% set current_time = now() %}
{# Get the rates array from the main sensor's attributes #}
{% set rates = state_attr('sensor.agile_rates_api_data', 'rates') %}
{% if rates %}
{# Loop through each 30-min slot #}
{% for rate_slot in rates %}
{# Convert start/end times from ISO strings to timestamps #}
{% set start_time = rate_slot.deliveryStart | as_datetime | as_timestamp %}
{% set end_time = rate_slot.deliveryEnd | as_datetime | as_timestamp %}
{# Check if current time falls within this slot #}
{% if start_time <= current_time.timestamp() < end_time %}
{# Output the rate and stop the loop #}
{{ rate_slot.agileRate.result.rate }}
{% break %}
{% endif %}
{% endfor %}
{% else %}
{# Fallback if data is not available #}
unknown
{% endif %}
# --- Current Agile Export Rate ---
- name: "Agile Current Export Rate"
unit_of_measurement: "p/kWh"
device_class: monetary
state_class: measurement
icon: mdi:cash-refund
state: >
{% set current_time = now() %}
{% set rates = state_attr('sensor.agile_rates_api_data', 'rates') %}
{% if rates %}
{% for rate_slot in rates %}
{% set start_time = rate_slot.deliveryStart | as_datetime | as_timestamp %}
{% set end_time = rate_slot.deliveryEnd | as_datetime | as_timestamp %}
{% if start_time <= current_time.timestamp() < end_time %}
{{ rate_slot.agileOutgoingRate.result.rate }}
{% break %}
{% endif %}
{% endfor %}
{% else %}
unknown
{% endif %}
# --- Next Hour's Average Import Rate (Example Calculation) ---
# Note: This calculates the average for the *next full hour* starting from now
- name: "Agile Next Hour Average Import Rate"
unit_of_measurement: "p/kWh"
device_class: monetary
state_class: measurement
icon: mdi:cash-clock
state: >
{% set current_time_ts = now().timestamp() %}
{% set rates = state_attr('sensor.agile_rates_api_data', 'rates') %}
{% set next_rates = namespace(values=[]) %} {# Use namespace to allow modification inside loop #}
{% if rates %}
{% for rate_slot in rates %}
{% set start_time = rate_slot.deliveryStart | as_datetime | as_timestamp %}
{# Find the next two slots (60 minutes) #}
{% if start_time >= current_time_ts and (next_rates.values | length < 2) %}
{% set next_rates.values = next_rates.values + [rate_slot.agileRate.result.rate] %}
{% endif %}
{% endfor %}
{# Calculate average if we found two rates #}
{% if (next_rates.values | length == 2) %}
{{ (next_rates.values[0] + next_rates.values[1]) / 2 | round(2) }}
{% else %}
unknown {# Not enough future data points yet #}
{% endif %}
{% else %}
unknown
{% endif %}
# --- Cheapest 2-Hour Import Window Start Time (Example) ---
# Note: Finds the cheapest 120min window based on the *current* calculation slot
- name: "Agile Cheapest 2h Window Start Time"
device_class: timestamp # Display as a time
icon: mdi:clock-down
state: >
{% set current_time_ts = now().timestamp() %}
{% set windows = state_attr('sensor.agile_rates_api_data', 'cheapestWindowsPerSlot') %}
{% if windows %}
{# Find the latest calculation key (start slot time) that is <= now #}
{% set valid_keys = namespace(latest_key=none, latest_ts=0) %}
{% for key_str in windows.keys() %}
{% set key_ts = key_str | as_datetime | as_timestamp %}
{% if key_ts <= current_time_ts and key_ts > valid_keys.latest_ts %}
{% set valid_keys.latest_key = key_str %}
{% set valid_keys.latest_ts = key_ts %}
{% endif %}
{% endfor %}
{# If a valid key was found, extract the start time for the 120 min window #}
{% if valid_keys.latest_key and '120' in windows[valid_keys.latest_key] %}
{{ windows[valid_keys.latest_key]['120'].startTime | as_datetime }}
{% else %}
unknown
{% endif %}
{% else %}
unknown
{% endif %}
# --- Cheapest 2-Hour Import Window Average Rate (Example) ---
- name: "Agile Cheapest 2h Window Average Rate"
unit_of_measurement: "p/kWh"
device_class: monetary
state_class: measurement
icon: mdi:currency-gbp
state: >
{% set current_time_ts = now().timestamp() %}
{% set windows = state_attr('sensor.agile_rates_api_data', 'cheapestWindowsPerSlot') %}
{% if windows %}
{# Find the latest calculation key (start slot time) that is <= now (same logic as above) #}
{% set valid_keys = namespace(latest_key=none, latest_ts=0) %}
{% for key_str in windows.keys() %}
{% set key_ts = key_str | as_datetime | as_timestamp %}
{% if key_ts <= current_time_ts and key_ts > valid_keys.latest_ts %}
{% set valid_keys.latest_key = key_str %}
{% set valid_keys.latest_ts = key_ts %}
{% endif %}
{% endfor %}
{# If a valid key was found, extract the average rate for the 120 min window #}
{% if valid_keys.latest_key and '120' in windows[valid_keys.latest_key] %}
{{ windows[valid_keys.latest_key]['120'].averageRate }}
{% else %}
unknown
{% endif %}
{% else %}
unknown
{% endif %}
# --- Most Expensive 1-Hour Export Window Start Time (Example) ---
- name: "Agile Most Expensive 1h Export Window Start Time"
device_class: timestamp
icon: mdi:clock-up
state: >
{% set current_time_ts = now().timestamp() %}
{# Use 'windowsExportPerSlot' attribute #}
{% set windows = state_attr('sensor.agile_rates_api_data', 'windowsExportPerSlot') %}
{% if windows %}
{# Find the latest calculation key (start slot time) that is <= now (same logic) #}
{% set valid_keys = namespace(latest_key=none, latest_ts=0) %}
{% for key_str in windows.keys() %}
{% set key_ts = key_str | as_datetime | as_timestamp %}
{% if key_ts <= current_time_ts and key_ts > valid_keys.latest_ts %}
{% set valid_keys.latest_key = key_str %}
{% set valid_keys.latest_ts = key_ts %}
{% endif %}
{% endfor %}
{# Extract start time for the 60 min window #}
{% if valid_keys.latest_key and '60' in windows[valid_keys.latest_key] %}
{{ windows[valid_keys.latest_key]['60'].startTime | as_datetime }}
{% else %}
unknown
{% endif %}
{% else %}
unknown
{% endif %}
# --- Most Expensive 1-Hour Export Window Average Rate (Example) ---
- name: "Agile Most Expensive 1h Export Window Average Rate"
unit_of_measurement: "p/kWh"
device_class: monetary
state_class: measurement
icon: mdi:currency-gbp
state: >
{% set current_time_ts = now().timestamp() %}
{% set windows = state_attr('sensor.agile_rates_api_data', 'windowsExportPerSlot') %}
{% if windows %}
{# Find the latest calculation key (start slot time) that is <= now (same logic) #}
{% set valid_keys = namespace(latest_key=none, latest_ts=0) %}
{% for key_str in windows.keys() %}
{% set key_ts = key_str | as_datetime | as_timestamp %}
{% if key_ts <= current_time_ts and key_ts > valid_keys.latest_ts %}
{% set valid_keys.latest_key = key_str %}
{% set valid_keys.latest_ts = key_ts %}
{% endif %}
{% endfor %}
{# Extract average rate for the 60 min window #}
{% if valid_keys.latest_key and '60' in windows[valid_keys.latest_key] %}
{{ windows[valid_keys.latest_key]['60'].averageRate }}
{% else %}
unknown
{% endif %}
{% else %}
unknown
{% endif %}
# --- API Last Update Time ---
- name: "Agile Rates API Last Update"
device_class: timestamp
icon: mdi:update
state: >
{# Access the nested last update time for the generated file #}
{{ state_attr('sensor.agile_rates_api_data', 'lastUpdate')['cache']['agileRates'] | as_datetime }}
Concept | Explanation |
---|---|
Region URL | Double-check you are using the correct URL for your DNO region. |
Current Rate Logic | The templates for "Current Import Rate" and "Current Export Rate"
iterate through the rates array and compare the current time with the
deliveryStart and deliveryEnd times for each slot to find the
correct rate.
|
Window Logic | The templates for finding the cheapest/most expensive windows first
determine the "current calculation key" (the latest timestamp key in the
cheapestWindowsPerSlot or windowsExportPerSlot object that is
less than or equal to the current time) and then extract the specific window data (e.g.,
for '60' or '120' minutes) associated with that key.
|
Error Handling | Basic checks ({% raw %}{% if rates %}{% endraw %} ,
{% raw %}{% if windows %}{% endraw %} ) are included, but more robust error
handling might be needed depending on your setup.
|
Timezones | The API provides times in UTC (indicated by 'Z'). Home Assistant's
now() is timezone-aware (based on your HA configuration). The
as_datetime and as_timestamp filters handle the conversion
correctly for comparisons.
|
Statistics & History | Using state_class: measurement or
state_class: total_increasing (where appropriate, though less relevant
here) allows Home Assistant to store long-term statistics for these sensors.
device_class: monetary or timestamp helps with formatting in
the UI.
|
Automation | You can use these sensors in automations (e.g., "Turn on charger if Agile Current Import Rate < 10 p/kWh" or "Boost water heater during the Agile Cheapest 2h Window Start Time"). |
Want to automate actions based on Agile Rates without writing code?
You can now connect Agile Rates data to thousands of other apps and services using Zapier via the Agile Rates integration.
This makes it incredibly easy to build powerful home automations, get custom notifications, log data, and much more.
Click the link below to accept the public invite and add the Agile Rates integration to your Zapier account:
Accept Zapier Invite for Agile Rates
Once added, you can use the following triggers and actions in your Zaps:
Fires automatically whenever new day-ahead rates have been successfully fetched and processed by Agile Rates (typically around 10 AM and 4 PM UK time).
Use Case Examples:Finds the optimal start time, end time, and average rate for the cheapest consecutive block of time (you specify the duration, e.g., 60, 120, 180 mins) for electricity import, starting from the current time.
Use Case Examples:Finds the optimal start time, end time, and average rate for the most profitable consecutive block of time (you specify duration) for electricity export on the Agile Outgoing tariff.
Use Case Examples:Retrieves the Agile import and/or export rate applicable for the current 30-minute slot in your specified region.
Use Case Examples:Retrieves the specific Agile import and export rates for a future date and time window that you define (e.g., tomorrow from 14:00 to 16:00).
Use Case Examples:Combine these triggers and actions with Zapier's vast library of app integrations and built-in tools (like Filters, Delays, and Scheduling) to create sophisticated, personalised energy automations without needing to be a programmer. Happy Zapping!
Here's an example flow for integrating the Agile Rates API into Node-RED:
This flow uses an Inject
node to trigger fetching data periodically, an
HTTP Request
node to call the API, a JSON
node (often automatic with HTTP
Request) to parse the response, a Function
node to process the data, and
Debug
nodes to display the results.
You can import the following flow directly into Node-RED:
[
{
"id": "FETCH_TRIGGER",
"type": "inject",
"z": "YOUR_FLOW_ID",
"name": "Fetch every 15 mins",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "900",
"crontab": "",
"once": true,
"onceDelay": 0.1,
"topic": "",
"payload": "",
"payloadType": "date",
"x": 150,
"y": 100,
"wires": [
[
"HTTP_REQUEST_NODE"
]
]
},
{
"id": "HTTP_REQUEST_NODE",
"type": "http request",
"z": "YOUR_FLOW_ID",
"name": "Get Agile Rates API Data (Region C)",
"method": "GET",
"ret": "obj",
"paytoqs": "ignore",
"url": "https://agilerates.uk/api/agile_rates_region_C.json",
"tls": "",
"persist": false,
"proxy": "",
"insecureHTTPParser": false,
"authType": "",
"senderr": false,
"headers": [],
"x": 410,
"y": 100,
"wires": [
[
"PROCESS_RATES_FUNCTION"
]
]
},
{
"id": "PROCESS_RATES_FUNCTION",
"type": "function",
"z": "YOUR_FLOW_ID",
"name": "Extract Agile Rates Info",
"func": "// Get the full payload (parsed JSON from HTTP Request)\nconst apiData = msg.payload;\n\n// Initialise variables to store extracted data\nlet currentImportRate = null;\nlet currentExportRate = null;\nlet nextHourAvgImportRate = null;\nlet cheapest2hWindow = null;\nlet mostExpensive1hExportWindow = null;\nlet apiLastUpdate = null;\n\n// Get current time timestamp (in milliseconds for JS Date)\nconst now = new Date();\nconst now_ts = now.getTime();\n\n// --- 1. Find Current Import/Export Rates ---\nif (apiData && apiData.rates && Array.isArray(apiData.rates)) {\n for (const rateSlot of apiData.rates) {\n const start_ts = new Date(rateSlot.deliveryStart).getTime();\n const end_ts = new Date(rateSlot.deliveryEnd).getTime();\n\n if (start_ts <= now_ts && now_ts < end_ts) {\n currentImportRate = rateSlot.agileRate.result.rate;\n currentExportRate = rateSlot.agileOutgoingRate.result.rate;\n break; // Found the current slot, exit loop\n }\n }\n}\n\n// --- 2. Calculate Next Hour's Average Import Rate ---\nif (apiData && apiData.rates && Array.isArray(apiData.rates)) {\n const nextRates = [];\n for (const rateSlot of apiData.rates) {\n const start_ts = new Date(rateSlot.deliveryStart).getTime();\n if (start_ts >= now_ts && nextRates.length < 2) {\n nextRates.push(rateSlot.agileRate.result.rate);\n }\n if (nextRates.length === 2) break; // Got the next two slots\n }\n if (nextRates.length === 2) {\n nextHourAvgImportRate = parseFloat(((nextRates[0] + nextRates[1]) / 2).toFixed(2));\n }\n}\n\n// --- Function to find the relevant window data ---\nfunction findWindowData(windowObject, durationMinutesKey) {\n if (!windowObject) return null;\n\n let latest_valid_key_ts = 0;\n let latest_valid_key_str = null;\n\n // Find the latest calculation key (slot start time) that is <= now\n for (const key_str in windowObject) {\n const key_ts = new Date(key_str).getTime();\n if (key_ts <= now_ts && key_ts > latest_valid_key_ts) {\n latest_valid_key_ts = key_ts;\n latest_valid_key_str = key_str;\n }\n }\n\n // If a valid key was found, extract the specific window data\n if (latest_valid_key_str && windowObject[latest_valid_key_str] && windowObject[latest_valid_key_str][durationMinutesKey]) {\n return windowObject[latest_valid_key_str][durationMinutesKey];\n }\n return null;\n}\n\n// --- 3. Find Cheapest 2-Hour Import Window ---\ncheapest2hWindow = findWindowData(apiData.cheapestWindowsPerSlot, '120');\n\n// --- 4. Find Most Expensive 1-Hour Export Window ---\nmostExpensive1hExportWindow = findWindowData(apiData.windowsExportPerSlot, '60');\n\n// --- 5. Get API Last Update Time ---\nif (apiData && apiData.lastUpdate && apiData.lastUpdate.cache && apiData.lastUpdate.cache.agileRates) {\n apiLastUpdate = apiData.lastUpdate.cache.agileRates;\n}\n\n// --- Prepare output message ---\n// You can send separate messages or one message with all data\n// Option 1: Send one message with structured payload\nmsg.payload = {\n currentImportRate: currentImportRate,\n currentExportRate: currentExportRate,\n nextHourAvgImportRate: nextHourAvgImportRate,\n cheapest2hWindow: cheapest2hWindow, // Contains startTime, endTime, averageRate\n mostExpensive1hExportWindow: mostExpensive1hExportWindow, // Contains startTime, endTime, averageRate\n apiLastUpdate: apiLastUpdate\n};\n\n// Option 2: Send multiple messages (uncomment if preferred)\n/*\nnode.send({ topic: \"currentImportRate\", payload: currentImportRate });\nnode.send({ topic: \"currentExportRate\", payload: currentExportRate });\nnode.send({ topic: \"nextHourAvgImportRate\", payload: nextHourAvgImportRate });\nnode.send({ topic: \"cheapest2hWindowStartTime\", payload: cheapest2hWindow ? cheapest2hWindow.startTime : null });\nnode.send({ topic: \"cheapest2hWindowAvgRate\", payload: cheapest2hWindow ? cheapest2hWindow.averageRate : null });\nnode.send({ topic: \"mostExpensive1hExportStartTime\", payload: mostExpensive1hExportWindow ? mostExpensive1hExportWindow.startTime : null });\nnode.send({ topic: \"mostExpensive1hExportAvgRate\", payload: mostExpensive1hExportWindow ? mostExpensive1hExportWindow.averageRate : null });\nnode.send({ topic: \"apiLastUpdate\", payload: apiLastUpdate });\nreturn null; // Prevent the original msg from being sent if sending multiple\n*/\n\nreturn msg;\n",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 670,
"y": 100,
"wires": [
[
"DEBUG_OUTPUT"
]
]
},
{
"id": "DEBUG_OUTPUT",
"type": "debug",
"z": "YOUR_FLOW_ID",
"name": "Processed Agile Data",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "payload",
"targetType": "msg",
"statusVal": "",
"statusType": "auto",
"x": 910,
"y": 100,
"wires": []
},
{
"id": "CATCH_HTTP_ERRORS",
"type": "catch",
"z": "YOUR_FLOW_ID",
"name": "Catch HTTP Errors",
"scope": [
"HTTP_REQUEST_NODE"
],
"uncaught": false,
"x": 390,
"y": 160,
"wires": [
[
"DEBUG_HTTP_ERROR"
]
]
},
{
"id": "DEBUG_HTTP_ERROR",
"type": "debug",
"z": "YOUR_FLOW_ID",
"name": "HTTP Request Error",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "true",
"targetType": "full",
"statusVal": "",
"statusType": "auto",
"x": 600,
"y": 160,
"wires": []
}
]
How to Import: Copy the JSON above. In Node-RED, click the menu (☰) -> Import -> Clipboard, paste the JSON, and click Import.
IMPORTANT:
YOUR_FLOW_ID
in the JSON with the actual ID of the flow tab you are
importing into (or let Node-RED create a new flow).HTTP Request
node ("Get Agile Rates API Data"), change the
URL to use the correct region code for your area (e.g., replace `_C.json` with
`_A.json`, `_B.json`, etc., based on the API Endpoint URLs table).
msg.payload
).rates
array in the payload to find the
agileRate.result.rate
and agileOutgoingRate.result.rate
for
the current 30-minute slot.
The Function
node outputs an object in msg.payload
containing the key
information:
{
"currentImportRate": 17.12, // Example value
"currentExportRate": 8.89, // Example value
"nextHourAvgImportRate": 16.64, // Example value
"cheapest2hWindow": {
"startTime": "2025-04-09T12:30:00Z",
"endTime": "2025-04-09T14:30:00Z",
"averageRate": 15.95,
"prediction": false
}, // Example value or null
"mostExpensive1hExportWindow": {
"startTime": "2025-04-09T17:00:00Z",
"endTime": "2025-04-09T18:00:00Z",
"averageRate": 19.29,
"prediction": false
}, // Example value or null
"apiLastUpdate": "2025-04-09T07:16:58Z" // Example value
}
You can connect the output of the Function
node to:
ui_text
, ui_gauge
, or
ui_chart
nodes (from the node-red-dashboard
package) to display rates
and times.
flow.set("currentAgileImport", msg.payload.currentImportRate)
) for use in other
flows or logic.
msg.payload.currentImportRate < 10
).
Here is an example for integrating the Agile Rates API into Domoticz using dzVents (Lua scripting):
First, you need to create virtual sensors in Domoticz to hold the data fetched from the API. If you don't already have one, create a "Dummy" hardware device (Setup -> Hardware).
Then, under the Dummy hardware, create the following virtual sensors (Setup -> Devices -> Create Virtual Sensors):
Replace the names above with your preferred names, but make sure they match the names used in the dzVents script below (or update the script accordingly). Using names in the script is generally easier than using IDX values.
Create a new Lua script in your Domoticz `scripts/dzVents/scripts` directory (e.g., `AgileRates.lua`). Paste the following code into the script.
IMPORTANT:
'https://agilerates.uk/api/agile_rates_region_C.json'
with
the correct API URL for your region from the API
Endpoint URLs table.'Agile Current Import Rate'
with the exact
names you gave your virtual sensors in Domoticz setup. Alternatively, you can use
device IDs (IDX) like domoticz.devices(123)
if you prefer.-- AgileRates.lua
-- Fetches data from Agile Rates API and updates Domoticz virtual devices
return {
active = true, -- Set to true to enable the script
on = {
timer = {
-- Run every 15 minutes. Adjust as needed.
-- Data only changes ~10am and ~4pm, so frequent updates aren't strictly necessary.
'every 15 minutes'
},
httpResponses = {
'agileRatesResponse' -- Trigger name matches the httpGet call below
}
},
logging = {
level = domoticz.LOG_INFO, -- Adjust log level (LOG_DEBUG, LOG_ERROR, etc.)
marker = 'AgileRates'
},
execute = function(domoticz, item)
local API_URL = 'https://agilerates.uk/api/agile_rates_region_C.json' -- <<< --- CHANGE TO YOUR REGION's URL
-- Device names (replace with your actual device names or use IDX)
local deviceImportRate = 'Agile Current Import Rate'
local deviceExportRate = 'Agile Current Export Rate'
local deviceNextHourAvgImport = 'Agile Next Hour Average Import Rate'
local deviceCheapest2hStart = 'Agile Cheapest 2h Window Start Time'
local deviceCheapest2hRate = 'Agile Cheapest 2h Window Average Rate'
local deviceExpensive1hExportStart = 'Agile Most Expensive 1h Export Window Start Time'
local deviceExpensive1hExportRate = 'Agile Most Expensive 1h Export Window Average Rate'
local deviceApiLastUpdate = 'Agile Rates API Last Update'
-- Function to safely update devices
local function updateDevice(deviceName, value, textValue)
if domoticz.devices(deviceName) == nil then
domoticz.log('Device "' .. deviceName .. '" not found. Please check name/IDX.', domoticz.LOG_ERROR)
return
end
if textValue ~= nil then -- For Text devices
if domoticz.devices(deviceName).text ~= textValue then
domoticz.devices(deviceName).updateText(textValue)
domoticz.log('Updated ' .. deviceName .. ' to: ' .. textValue, domoticz.LOG_DEBUG)
end
elseif value ~= nil then -- For Custom Sensor, Percentage etc.
-- Custom sensor expects sValue as string
local sValue = tostring(value)
if domoticz.devices(deviceName).sValue ~= sValue then
domoticz.devices(deviceName).updateCustomSensor(value)
domoticz.log('Updated ' .. deviceName .. ' to: ' .. sValue, domoticz.LOG_DEBUG)
end
else
domoticz.log('No value provided for device "' .. deviceName .. '".', domoticz.LOG_WARNING)
end
end
-- Function to find the relevant window data
local function findWindowData(windowObject, durationMinutesKey, currentTimeTs)
if windowObject == nil then return nil end
local latest_valid_key_ts = 0
local latest_valid_key_str = nil
-- Find the latest calculation key (slot start time) that is <= now
for key_str, _ in pairs(windowObject) do
local key_time = domoticz.time.fromString(key_str) -- Parses ISO8601 UTC string
if key_time ~= nil then
local key_ts = key_time.timestamp
if key_ts <= currentTimeTs and key_ts > latest_valid_key_ts then
latest_valid_key_ts = key_ts
latest_valid_key_str = key_str
end
end
end
-- If a valid key was found, extract the specific window data
if latest_valid_key_str ~= nil and
windowObject[latest_valid_key_str] ~= nil and
windowObject[latest_valid_key_str][durationMinutesKey] ~= nil then
return windowObject[latest_valid_key_str][durationMinutesKey] -- Returns {startTime=..., endTime=..., averageRate=..., prediction=...}
end
return nil
end
-- Trigger the HTTP request if the script was triggered by the timer
if (item.isTimer) then
domoticz.log('Timer triggered. Fetching data from ' .. API_URL, domoticz.LOG_INFO)
domoticz.openURL({
url = API_URL,
method = 'GET',
callback = 'agileRatesResponse' -- Must match httpResponses trigger name
})
end
-- Process the response when it arrives
if (item.isHTTPResponse) then
if (item.ok and item.isJSON) then
domoticz.log('HTTP Response received successfully.', domoticz.LOG_INFO)
local apiData = item.json
if apiData == nil or apiData.result ~= 'ok' then
domoticz.log('API response does not contain valid data or result is not "ok".', domoticz.LOG_ERROR)
return
end
local currentImportRate = nil
local currentExportRate = nil
local nextHourAvgImportRate = nil
local cheapest2hWindow = nil
local mostExpensive1hExportWindow = nil
local apiLastUpdate = nil
local nowTime = domoticz.time -- Current time object
local now_ts = nowTime.timestamp -- Current time as Unix timestamp (UTC)
-- --- 1. Find Current Import/Export Rates ---
if apiData.rates and type(apiData.rates) == 'table' then
for _, rateSlot in ipairs(apiData.rates) do
local startTime = domoticz.time.fromString(rateSlot.deliveryStart)
local endTime = domoticz.time.fromString(rateSlot.deliveryEnd)
if startTime and endTime and startTime.timestamp <= now_ts and now_ts < endTime.timestamp then
if rateSlot.agileRate and rateSlot.agileRate.result then
currentImportRate = rateSlot.agileRate.result.rate
end
if rateSlot.agileOutgoingRate and rateSlot.agileOutgoingRate.result then
currentExportRate = rateSlot.agileOutgoingRate.result.rate
end
break -- Found the current slot
end
end
else
domoticz.log('API response missing "rates" array.', domoticz.LOG_WARNING)
end
-- --- 2. Calculate Next Hour's Average Import Rate ---
if apiData.rates and type(apiData.rates) == 'table' then
local nextRates = {}
for _, rateSlot in ipairs(apiData.rates) do
local startTime = domoticz.time.fromString(rateSlot.deliveryStart)
if startTime and startTime.timestamp >= now_ts and #nextRates < 2 then
if rateSlot.agileRate and rateSlot.agileRate.result and rateSlot.agileRate.result.rate then
table.insert(nextRates, rateSlot.agileRate.result.rate)
end
end
if #nextRates == 2 then break end -- Got the next two slots
end
if #nextRates == 2 then
nextHourAvgImportRate = tonumber(string.format("%.2f", (nextRates[1] + nextRates[2]) / 2))
end
end
-- --- 3. Find Cheapest 2-Hour Import Window ---
cheapest2hWindow = findWindowData(apiData.cheapestWindowsPerSlot, '120', now_ts)
-- --- 4. Find Most Expensive 1-Hour Export Window ---
mostExpensive1hExportWindow = findWindowData(apiData.windowsExportPerSlot, '60', now_ts)
-- --- 5. Get API Last Update Time ---
if apiData.lastUpdate and apiData.lastUpdate.cache and apiData.lastUpdate.cache.agileRates then
apiLastUpdate = apiData.lastUpdate.cache.agileRates
end
-- --- Update Domoticz Devices ---
domoticz.log('Updating devices...', domoticz.LOG_DEBUG)
updateDevice(deviceImportRate, currentImportRate)
updateDevice(deviceExportRate, currentExportRate)
updateDevice(deviceNextHourAvgImport, nextHourAvgImportRate)
if cheapest2hWindow then
updateDevice(deviceCheapest2hStart, nil, cheapest2hWindow.startTime)
updateDevice(deviceCheapest2hRate, cheapest2hWindow.averageRate)
else
updateDevice(deviceCheapest2hStart, nil, 'Unknown')
updateDevice(deviceCheapest2hRate, 0) -- Or handle appropriately
end
if mostExpensive1hExportWindow then
updateDevice(deviceExpensive1hExportStart, nil, mostExpensive1hExportWindow.startTime)
updateDevice(deviceExpensive1hExportRate, mostExpensive1hExportWindow.averageRate)
else
updateDevice(deviceExpensive1hExportStart, nil, 'Unknown')
updateDevice(deviceExpensive1hExportRate, 0) -- Or handle appropriately
end
updateDevice(deviceApiLastUpdate, nil, apiLastUpdate or 'Unknown')
domoticz.log('Device updates complete.', domoticz.LOG_INFO)
elseif item.ok == false then
domoticz.log('HTTP request failed. Status code: ' .. item.statusCode, domoticz.LOG_ERROR)
else
domoticz.log('HTTP response was not valid JSON.', domoticz.LOG_ERROR)
domoticz.log('Raw response: ' .. item.data, domoticz.LOG_DEBUG) -- Log raw data for debugging
end
end
end
}
Concept | Explanation |
---|---|
Region URL & Device Names | Crucially, update the API_URL variable and the device
name variables (deviceImportRate , etc.) in the script to match your
specific setup. |
dzVents Structure | The script uses the standard dzVents structure with
active , on (timer and HTTP response triggers),
logging , and execute sections.
|
Asynchronous HTTP | domoticz.openURL with a callback performs
an asynchronous request. The script first runs on the timer trigger to *initiate* the
request, then runs *again* when the httpResponses trigger
('agileRatesResponse') fires, at which point the response data is processed. |
Current Rate Logic | It iterates through the apiData.rates table, parses the
UTC start/end times using domoticz.time.fromString , and compares their
timestamps with the current time's timestamp (nowTime.timestamp ) to find
the active slot. |
Window Logic | The findWindowData function replicates the logic from
the HA example: it finds the latest timestamp key in the window object (e.g.,
apiData.cheapestWindowsPerSlot ) that is less than or equal to the current
time, then extracts the data for the specified duration ('60' or '120').
|
Device Updates | The updateDevice helper function checks if the device
exists and updates it only if the new value is different from the current value,
preventing unnecessary updates and log spam. It uses updateCustomSensor for
numeric rates and updateText for timestamps/strings. |
Error Handling | The script checks if the HTTP request was successful
(item.ok ), if the response is valid JSON (item.isJSON ), and if
the expected data structures exist within the JSON. Errors are logged using
domoticz.log() . Check the Domoticz log (Setup -> Log) for messages from
'AgileRates'.
|
Timezones | The API provides times in UTC ('Z'). dzVents'
domoticz.time.fromString correctly parses these as UTC, and
domoticz.time.timestamp provides the UTC Unix timestamp, ensuring correct
comparisons. Domoticz typically displays times in your configured local timezone.
|
Automation | You can use the updated virtual sensors in other Domoticz events (Blockly, Lua, dzVents) to trigger automations (e.g., "IF Agile Current Import Rate < 10 THEN Turn ON SmartPlug"). |
Here is an example demonstrating how to fetch and process data from the Agile Rates API using
standard Python. This script uses the popular requests
library for HTTP calls and
standard Python datetime
for handling timestamps.
This allows you to integrate the Agile Rates data into your custom Python applications, scripts, or backend services.
This script fetches the data for a specified region, finds the current rates, calculates the next hour's average import rate, and identifies the cheapest import and most expensive export windows based on pre-defined durations.
import requests
import json
from datetime import datetime, timezone, timedelta
# --- Configuration ---
# IMPORTANT: Replace with the URL for YOUR region! This example uses London (C).
# Find your region's URL in the API documentation: https://agilerates.uk/api.html#api_endpoints
REGION_CODE = 'C'
API_URL = f"https://agilerates.uk/api/agile_rates_region_{REGION_CODE}.json"
# Define the window durations you are interested in (in minutes)
CHEAPEST_IMPORT_WINDOW_MINUTES = 120 # e.g., 2 hours
MOST_EXPENSIVE_EXPORT_WINDOW_MINUTES = 60 # e.g., 1 hour
def get_agile_rates_data(api_url: str) -> dict | None:
"""
Fetches and parses the Agile Rates data from the specified API endpoint.
Args:
api_url: The URL of the regional Agile Rates API JSON file.
Returns:
A dictionary containing the parsed JSON data, or None if an error occurs.
"""
print(f"Fetching data from: {api_url}")
try:
response = requests.get(api_url, timeout=10) # Add a timeout
response.raise_for_status() # Raise an exception for bad status codes (4xx or 5xx)
# Check if the content type is JSON before attempting to parse
if 'application/json' not in response.headers.get('Content-Type', ''):
print(f"Error: Unexpected content type: {response.headers.get('Content-Type')}")
print(f"Raw response text: {response.text[:500]}...") # Log part of the response
return None
data = response.json()
print("Data fetched and parsed successfully.")
return data
except requests.exceptions.RequestException as e:
print(f"Error fetching data: {e}")
return None
except json.JSONDecodeError as e:
print(f"Error decoding JSON response: {e}")
print(f"Raw response text: {response.text[:500]}...") # Log part of the response
return None
def process_agile_data(data: dict) -> dict:
"""
Processes the fetched Agile Rates data to extract key information.
Args:
data: The dictionary containing the parsed JSON data from the API.
Returns:
A dictionary containing extracted information (current rates, windows, etc.).
"""
results = {
"current_import_rate": None,
"current_export_rate": None,
"next_hour_avg_import_rate": None,
"cheapest_import_window": None,
"most_expensive_export_window": None,
"api_last_update": None,
"region_info": None,
"error": None
}
if not data or data.get('result') != 'ok':
results["error"] = "Invalid or missing data in API response."
print(f"Error: {results['error']}")
return results
try:
results["region_info"] = data.get('regionInfo')
results["api_last_update"] = data.get('lastUpdate', {}).get('cache', {}).get('agileRates')
now_utc = datetime.now(timezone.utc)
rates = data.get('rates', [])
# --- 1. Find Current Import/Export Rates ---
for rate_slot in rates:
try:
# Python < 3.11 doesn't handle 'Z' directly in fromisoformat
start_time = datetime.fromisoformat(rate_slot['deliveryStart'].replace('Z', '+00:00'))
end_time = datetime.fromisoformat(rate_slot['deliveryEnd'].replace('Z', '+00:00'))
if start_time <= now_utc < end_time:
results["current_import_rate"] = rate_slot.get('agileRate', {}).get('result', {}).get('rate')
results["current_export_rate"] = rate_slot.get('agileOutgoingRate', {}).get('result', {}).get('rate')
break # Found the current slot
except (KeyError, ValueError, TypeError) as e:
print(f"Warning: Could not process rate slot {rate_slot.get('deliveryStart')}: {e}")
continue # Skip to next slot if current one has issues
# --- 2. Calculate Next Hour's Average Import Rate ---
next_rates_found = []
for rate_slot in rates:
try:
start_time = datetime.fromisoformat(rate_slot['deliveryStart'].replace('Z', '+00:00'))
if start_time >= now_utc and len(next_rates_found) < 2:
rate_value = rate_slot.get('agileRate', {}).get('result', {}).get('rate')
if rate_value is not None:
next_rates_found.append(rate_value)
if len(next_rates_found) == 2:
break # Got the next two slots
except (KeyError, ValueError, TypeError) as e:
print(f"Warning: Could not process rate slot for next hour calc {rate_slot.get('deliveryStart')}: {e}")
continue
if len(next_rates_found) == 2:
results["next_hour_avg_import_rate"] = round((next_rates_found[0] + next_rates_found[1]) / 2, 2)
# --- Function to find the relevant window data ---
def find_window_data(window_object, duration_minutes_key, current_time_utc):
if not window_object or not isinstance(window_object, dict):
return None
latest_valid_key_ts = 0
latest_valid_key_str = None
# Find the latest calculation key (slot start time) that is <= now
for key_str in window_object.keys():
try:
key_time = datetime.fromisoformat(key_str.replace('Z', '+00:00'))
key_ts = key_time.timestamp() # Get Unix timestamp
if key_ts <= current_time_utc.timestamp() and key_ts > latest_valid_key_ts:
latest_valid_key_ts = key_ts
latest_valid_key_str = key_str
except (ValueError, TypeError):
print(f"Warning: Could not parse window key: {key_str}")
continue # Skip invalid keys
# If a valid key was found, extract the specific window data
if latest_valid_key_str:
duration_data = window_object.get(latest_valid_key_str, {}).get(str(duration_minutes_key))
if duration_data and isinstance(duration_data, dict):
# Basic validation of expected keys
if all(k in duration_data for k in ('startTime', 'endTime', 'averageRate')):
return duration_data # Returns {startTime, endTime, averageRate, prediction}
return None
# --- 3. Find Cheapest Import Window ---
results["cheapest_import_window"] = find_window_data(
data.get('cheapestWindowsPerSlot'),
CHEAPEST_IMPORT_WINDOW_MINUTES,
now_utc
)
# --- 4. Find Most Expensive Export Window ---
results["most_expensive_export_window"] = find_window_data(
data.get('windowsExportPerSlot'),
MOST_EXPENSIVE_EXPORT_WINDOW_MINUTES,
now_utc
)
except Exception as e:
results["error"] = f"An unexpected error occurred during processing: {e}"
print(f"Error: {results['error']}")
return results
# --- Main Execution ---
if __name__ == "__main__":
print("--- Agile Rates API Python Example ---")
api_data = get_agile_rates_data(API_URL)
if api_data:
processed_results = process_agile_data(api_data)
print("\n--- Processed Results ---")
if processed_results.get("error"):
print(f"Processing Error: {processed_results['error']}")
else:
print(f"Region: {processed_results.get('region_info', {}).get('name', 'N/A')} ({processed_results.get('region_info', {}).get('code', 'N/A')})")
print(f"API Data Last Updated: {processed_results.get('api_last_update', 'N/A')}")
print("-" * 20)
print(f"Current Import Rate: {processed_results.get('current_import_rate', 'N/A')} p/kWh")
print(f"Current Export Rate: {processed_results.get('current_export_rate', 'N/A')} p/kWh")
print(f"Next Hour Avg Import Rate: {processed_results.get('next_hour_avg_import_rate', 'N/A')} p/kWh")
print("-" * 20)
cheapest_window = processed_results.get('cheapest_import_window')
if cheapest_window:
print(f"Cheapest {CHEAPEST_IMPORT_WINDOW_MINUTES}min Import Window:")
print(f" Start Time: {cheapest_window.get('startTime', 'N/A')}")
print(f" End Time: {cheapest_window.get('endTime', 'N/A')}")
print(f" Avg Rate: {cheapest_window.get('averageRate', 'N/A')} p/kWh")
print(f" Prediction: {cheapest_window.get('prediction', 'N/A')}")
else:
print(f"Cheapest {CHEAPEST_IMPORT_WINDOW_MINUTES}min Import Window: Not available/found")
print("-" * 20)
expensive_window = processed_results.get('most_expensive_export_window')
if expensive_window:
print(f"Most Expensive {MOST_EXPENSIVE_EXPORT_WINDOW_MINUTES}min Export Window:")
print(f" Start Time: {expensive_window.get('startTime', 'N/A')}")
print(f" End Time: {expensive_window.get('endTime', 'N/A')}")
print(f" Avg Rate: {expensive_window.get('averageRate', 'N/A')} p/kWh")
print(f" Prediction: {expensive_window.get('prediction', 'N/A')}")
else:
print(f"Most Expensive {MOST_EXPENSIVE_EXPORT_WINDOW_MINUTES}min Export Window: Not available/found")
else:
print("\nFailed to retrieve or parse API data.")
print("\n--- End of Example ---")
Concept | Explanation |
---|---|
Dependencies | Requires the requests library. Install it using pip:
pip install requests
|
Region URL | You must change the REGION_CODE
variable (and thus the API_URL ) to match your specific UK electricity
supply region. Refer to the API Endpoint URLs table. |
Window Durations | You can change the CHEAPEST_IMPORT_WINDOW_MINUTES and
MOST_EXPENSIVE_EXPORT_WINDOW_MINUTES variables to other supported values
(e.g., 60, 120, 180, 240, 300, 360) as needed for your application.
|
Fetching Data | The get_agile_rates_data function handles the HTTP GET
request, includes a timeout, checks the response status, verifies the content type, and
parses the JSON. It returns the parsed data or None on error. |
Processing Data | The process_agile_data function takes the raw dictionary
and extracts the specific pieces of information needed, performing calculations and
lookups. |
Timezones & Timestamps | The API provides timestamps in ISO 8601 format ending in 'Z' (UTC).
The script uses datetime.now(timezone.utc) to get the current time in UTC
and parses the API timestamps using datetime.fromisoformat() after
replacing 'Z' with '+00:00' for compatibility. All time comparisons are done in UTC.
|
Current Rate Logic | It iterates through the rates list, parses the
deliveryStart and deliveryEnd times for each slot, and checks
if the current UTC time falls within that slot's range.
|
Window Logic | The find_window_data helper function finds the correct
pre-calculated window. It identifies the latest timestamp key in the
cheapestWindowsPerSlot or windowsExportPerSlot dictionary that
is less than or equal to the current time, then retrieves the data for the requested
duration (e.g., '120' or '60').
|
Error Handling | Includes try...except blocks for network errors
(requests.exceptions.RequestException ), JSON parsing errors
(json.JSONDecodeError ), and general processing errors
(KeyError , ValueError , TypeError when accessing
potentially missing dictionary keys or parsing timestamps). Errors are printed to the
console, and functions return None or an error message in the results
dictionary. |
Modularity | The fetching and processing logic are separated into functions
(get_agile_rates_data , process_agile_data ) for better
organization and potential reuse in larger applications. |
Usage | Save the code as a Python file (e.g., agile_checker.py )
and run it from your terminal: python agile_checker.py . You can
integrate the functions into your own Python projects, schedulers (like `cron` or
Windows Task Scheduler), or backend services. |
Here is an example demonstrating how to fetch and process data from the Agile Rates API using modern
Node.js. This script uses the popular node-fetch
library for HTTP calls and standard
JavaScript Date
objects for handling timestamps.
This allows you to integrate the Agile Rates data into your custom Node.js applications, scripts, or backend services.
Dependency: This script requires the node-fetch
library (version 2 for
CommonJS or version 3+ for ES Modules). Install it using npm:
npm install node-fetch
If you are using ES Modules ("type": "module"
in your package.json
or using
.mjs
files), use import fetch from 'node-fetch';
. If using CommonJS
(default .js
files), use const fetch = require('node-fetch');
.
This script fetches the data for a specified region, finds the current rates, calculates the next hour's average import rate, and identifies the cheapest import and most expensive export windows based on pre-defined durations.
// Use 'import fetch from 'node-fetch';' if using ES Modules
const fetch = require('node-fetch'); // Use 'require' for CommonJS
// --- Configuration ---
// IMPORTANT: Replace with the code for YOUR region! This example uses London (C).
// Find your region's code in the API documentation: https://agilerates.uk/api.html#api_endpoints
const REGION_CODE = 'C';
const API_URL = `https://agilerates.uk/api/agile_rates_region_${REGION_CODE}.json`;
// Define the window durations you are interested in (in minutes)
const CHEAPEST_IMPORT_WINDOW_MINUTES = 120; // e.g., 2 hours
const MOST_EXPENSIVE_EXPORT_WINDOW_MINUTES = 60; // e.g., 1 hour
/**
* Fetches and parses the Agile Rates data from the specified API endpoint.
*
* @param {string} apiUrl - The URL of the regional Agile Rates API JSON file.
* @returns {Promise
Concept | Explanation |
---|---|
Dependencies | Requires the node-fetch library. Install via
npm install node-fetch . Note the difference between import
(ESM) and require (CommonJS).
|
Region URL | You must change the REGION_CODE
variable to match your specific UK electricity supply region. Refer to the API Endpoint URLs table. |
Window Durations | You can change the CHEAPEST_IMPORT_WINDOW_MINUTES and
MOST_EXPENSIVE_EXPORT_WINDOW_MINUTES variables to other supported values
(e.g., 60, 180, 360) as needed. Note that the keys in the JSON are strings (e.g., '60',
'120'), so the script converts the number to a string when looking up the window data.
|
Fetching Data (Async/Await) | The getAgileRatesData function uses
async/await with node-fetch to perform the HTTP GET request.
It includes a timeout, checks the response status (response.ok ), verifies
the content type, and parses the JSON (response.json() ). It returns the
parsed data or null on error.
|
Processing Data | The processAgileData function takes the raw data object
and extracts the specific pieces of information, performing calculations and lookups
similar to the Python example. Optional chaining (?. ) is used for safer
access to nested properties. |
Timezones & Timestamps | The API provides timestamps in ISO 8601 format ending in 'Z' (UTC).
JavaScript's built-in new Date(isoString) correctly parses these as UTC.
new Date().getTime() returns the number of milliseconds since the Unix
epoch (UTC), allowing for correct time comparisons.
|
Current Rate Logic | It iterates through the rates array, parses the
deliveryStart and deliveryEnd times for each slot using
new Date() , and checks if the current time's timestamp
(now.getTime() ) falls within that slot's range.
|
Window Logic | The findWindowData helper function finds the correct
pre-calculated window. It identifies the latest timestamp key (which is an ISO string)
in the cheapestWindowsPerSlot or windowsExportPerSlot object
that represents a time less than or equal to the current time, then retrieves the data
for the requested duration key (e.g., '120' or '60'). |
Error Handling | Includes try...catch blocks for network errors (fetch
promise rejection), JSON parsing errors (response.json() rejection), and
general processing errors. Errors are logged to the console, and functions return
null or an error message in the results object. Basic checks for date
validity (!isNaN() ) and object structure are included.
|
Modularity | The fetching and processing logic are separated into functions
(getAgileRatesData , processAgileData ) for better organization.
The main execution logic is within an async function main() . |
Usage | Save the code as a JavaScript file (e.g.,
agileChecker.js ) and run it from your terminal using Node.js:
node agileChecker.js . You can integrate the functions into your own
Node.js projects, scheduled tasks (using `node-cron` or system cron), or backend
services (like Express.js).
|
Here is an example demonstrating how to fetch and process data from the Agile Rates API using
standard PHP. This script uses the cURL extension for HTTP calls and standard PHP
DateTime
objects for handling timestamps.
This allows you to integrate the Agile Rates data into your custom PHP applications, websites, or backend services.
Dependencies: This script requires the cURL and JSON extensions to be enabled in your PHP installation (these are commonly enabled by default).
This script fetches the data for a specified region, finds the current rates, calculates the next hour's average import rate, and identifies the cheapest import and most expensive export windows based on pre-defined durations.
<?php
// --- Configuration ---
// IMPORTANT: Replace with the code for YOUR region! This example uses London (C).
// Find your region's code in the API documentation: https://agilerates.uk/api.html#api_endpoints
define('REGION_CODE', 'C');
define('API_URL', 'https://agilerates.uk/api/agile_rates_region_' . REGION_CODE . '.json');
// Define the window durations you are interested in (in minutes)
define('CHEAPEST_IMPORT_WINDOW_MINUTES', 120); // e.g., 2 hours
define('MOST_EXPENSIVE_EXPORT_WINDOW_MINUTES', 60); // e.g., 1 hour
/**
* Fetches and parses the Agile Rates data from the specified API endpoint using cURL.
*
* @param string $apiUrl The URL of the regional Agile Rates API JSON file.
* @return array|null An associative array containing the parsed JSON data, or null on error.
*/
function getAgileRatesData(string $apiUrl): ?array
{
echo "Fetching data from: " . $apiUrl . "\n";
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $apiUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); // Return the transfer as a string
curl_setopt($ch, CURLOPT_TIMEOUT, 10); // Timeout in seconds
curl_setopt($ch, CURLOPT_FAILONERROR, true); // Fail silently (return false on >400 http codes)
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Accept: application/json']); // Set Accept header
$jsonResponse = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$contentType = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
if (curl_errno($ch)) {
echo "cURL Error: " . curl_error($ch) . "\n";
curl_close($ch);
return null;
}
curl_close($ch);
if ($httpCode >= 400) {
echo "HTTP Error: Received status code " . $httpCode . "\n";
return null;
}
if (strpos($contentType, 'application/json') === false) {
echo "Error: Unexpected content type: " . $contentType . "\n";
echo "Raw response text: " . substr($jsonResponse, 0, 500) . "...\n";
return null;
}
// Decode JSON into an associative array (true as second argument)
$data = json_decode($jsonResponse, true);
if (json_last_error() !== JSON_ERROR_NONE) {
echo "Error decoding JSON: " . json_last_error_msg() . "\n";
echo "Raw response text: " . substr($jsonResponse, 0, 500) . "...\n";
return null;
}
echo "Data fetched and parsed successfully.\n";
return $data;
}
/**
* Processes the fetched Agile Rates data to extract key information.
*
* @param array $data The associative array containing the parsed JSON data from the API.
* @return array An associative array containing extracted information.
*/
function processAgileData(array $data): array
{
$results = [
'current_import_rate' => null,
'current_export_rate' => null,
'next_hour_avg_import_rate' => null,
'cheapest_import_window' => null,
'most_expensive_export_window' => null,
'api_last_update' => null,
'region_info' => null,
'error' => null
];
if (empty($data) || ($data['result'] ?? null) !== 'ok') {
$results['error'] = "Invalid or missing data in API response.";
echo "Error: " . $results['error'] . "\n";
return $results;
}
try {
$results['region_info'] = $data['regionInfo'] ?? null;
$results['api_last_update'] = $data['lastUpdate']['cache']['agileRates'] ?? null;
// Ensure all times are handled in UTC
$utcTimeZone = new DateTimeZone('UTC');
$nowUtc = new DateTime('now', $utcTimeZone);
$nowTimestamp = $nowUtc->getTimestamp();
$rates = $data['rates'] ?? [];
// --- 1. Find Current Import/Export Rates ---
foreach ($rates as $rateSlot) {
try {
$startTime = new DateTime($rateSlot['deliveryStart'], $utcTimeZone);
$endTime = new DateTime($rateSlot['deliveryEnd'], $utcTimeZone);
if ($startTime->getTimestamp() <= $nowTimestamp && $nowTimestamp < $endTime->getTimestamp()) {
$results['current_import_rate'] = $rateSlot['agileRate']['result']['rate'] ?? null;
$results['current_export_rate'] = $rateSlot['agileOutgoingRate']['result']['rate'] ?? null;
break; // Found the current slot
}
} catch (Exception $e) {
echo "Warning: Could not parse date for rate slot " . ($rateSlot['deliveryStart'] ?? 'N/A') . ": " . $e->getMessage() . "\n";
continue; // Skip slot on error
}
}
// --- 2. Calculate Next Hour's Average Import Rate ---
$nextRatesFound = [];
foreach ($rates as $rateSlot) {
try {
$startTime = new DateTime($rateSlot['deliveryStart'], $utcTimeZone);
if ($startTime->getTimestamp() >= $nowTimestamp && count($nextRatesFound) < 2) {
$rateValue = $rateSlot['agileRate']['result']['rate'] ?? null;
if (is_numeric($rateValue)) {
$nextRatesFound[] = (float)$rateValue;
}
}
if (count($nextRatesFound) === 2) {
break; // Got the next two slots
}
} catch (Exception $e) {
echo "Warning: Could not parse date for next hour calc slot " . ($rateSlot['deliveryStart'] ?? 'N/A') . ": " . $e->getMessage() . "\n";
continue;
}
}
if (count($nextRatesFound) === 2) {
$results['next_hour_avg_import_rate'] = round(($nextRatesFound[0] + $nextRatesFound[1]) / 2, 2);
}
/**
* Helper function to find the relevant window data based on the current time.
* @param array|null $windowObject The cheapestWindowsPerSlot or windowsExportPerSlot array.
* @param string $durationMinutesKey The duration key (e.g., '60', '120').
* @param int $currentTimeTs The current time as a Unix timestamp (UTC).
* @param DateTimeZone $tz The UTC timezone object.
* @return array|null The window data {startTime, endTime, averageRate, prediction} or null.
*/
$findWindowData = function(?array $windowObject, string $durationMinutesKey, int $currentTimeTs, DateTimeZone $tz): ?array {
if (empty($windowObject)) {
return null;
}
$latestValidKeyTs = 0;
$latestValidKeyStr = null;
// Find the latest calculation key (slot start time) that is <= now
foreach (array_keys($windowObject) as $keyStr) {
try {
$keyTime = new DateTime($keyStr, $tz);
$keyTs = $keyTime->getTimestamp();
if ($keyTs <= $currentTimeTs && $keyTs > $latestValidKeyTs) {
$latestValidKeyTs = $keyTs;
$latestValidKeyStr = $keyStr;
}
} catch (Exception $e) {
echo "Warning: Could not parse window key: " . $keyStr . ": " . $e->getMessage() . "\n";
continue; // Skip invalid keys
}
}
// If a valid key was found, extract the specific window data
if ($latestValidKeyStr !== null) {
$durationData = $windowObject[$latestValidKeyStr][$durationMinutesKey] ?? null;
// Basic validation
if (is_array($durationData) &&
isset($durationData['startTime']) &&
isset($durationData['endTime']) &&
isset($durationData['averageRate'])) {
return $durationData;
}
}
return null;
};
// --- 3. Find Cheapest Import Window ---
$results['cheapest_import_window'] = $findWindowData(
$data['cheapestWindowsPerSlot'] ?? null,
(string)CHEAPEST_IMPORT_WINDOW_MINUTES, // Key must be a string
$nowTimestamp,
$utcTimeZone
);
// --- 4. Find Most Expensive Export Window ---
$results['most_expensive_export_window'] = $findWindowData(
$data['windowsExportPerSlot'] ?? null,
(string)MOST_EXPENSIVE_EXPORT_WINDOW_MINUTES, // Key must be a string
$nowTimestamp,
$utcTimeZone
);
} catch (Exception $e) {
$results['error'] = "An unexpected error occurred during processing: " . $e->getMessage();
echo "Error: " . $results['error'] . "\n";
// Consider logging $e->getTraceAsString() for debugging
}
return $results;
}
// --- Main Execution ---
// This check prevents execution if the file is included/required elsewhere.
if (basename(__FILE__) === basename($_SERVER['SCRIPT_FILENAME'])) {
echo "--- Agile Rates API PHP Example ---\n";
$apiData = getAgileRatesData(API_URL);
if ($apiData) {
$processedResults = processAgileData($apiData);
echo "\n--- Processed Results ---\n";
if ($processedResults['error']) {
echo "Processing Error: " . $processedResults['error'] . "\n";
} else {
$regionName = $processedResults['region_info']['name'] ?? 'N/A';
$regionCode = $processedResults['region_info']['code'] ?? 'N/A';
echo "Region: " . $regionName . " (" . $regionCode . ")\n";
echo "API Data Last Updated: " . ($processedResults['api_last_update'] ?? 'N/A') . "\n";
echo str_repeat("-", 20) . "\n";
echo "Current Import Rate: " . ($processedResults['current_import_rate'] ?? 'N/A') . " p/kWh\n";
echo "Current Export Rate: " . ($processedResults['current_export_rate'] ?? 'N/A') . " p/kWh\n";
echo "Next Hour Avg Import Rate: " . ($processedResults['next_hour_avg_import_rate'] ?? 'N/A') . " p/kWh\n";
echo str_repeat("-", 20) . "\n";
$cheapestWindow = $processedResults['cheapest_import_window'];
if ($cheapestWindow) {
echo "Cheapest " . CHEAPEST_IMPORT_WINDOW_MINUTES . "min Import Window:\n";
echo " Start Time: " . ($cheapestWindow['startTime'] ?? 'N/A') . "\n";
echo " End Time: " . ($cheapestWindow['endTime'] ?? 'N/A') . "\n";
echo " Avg Rate: " . number_format($cheapestWindow['averageRate'] ?? 0, 2) . " p/kWh\n";
echo " Prediction: " . (($cheapestWindow['prediction'] ?? false) ? 'Yes' : 'No') . "\n";
} else {
echo "Cheapest " . CHEAPEST_IMPORT_WINDOW_MINUTES . "min Import Window: Not available/found\n";
}
echo str_repeat("-", 20) . "\n";
$expensiveWindow = $processedResults['most_expensive_export_window'];
if ($expensiveWindow) {
echo "Most Expensive " . MOST_EXPENSIVE_EXPORT_WINDOW_MINUTES . "min Export Window:\n";
echo " Start Time: " . ($expensiveWindow['startTime'] ?? 'N/A') . "\n";
echo " End Time: " . ($expensiveWindow['endTime'] ?? 'N/A') . "\n";
echo " Avg Rate: " . number_format($expensiveWindow['averageRate'] ?? 0, 2) . " p/kWh\n";
echo " Prediction: " . (($expensiveWindow['prediction'] ?? false) ? 'Yes' : 'No') . "\n";
} else {
echo "Most Expensive " . MOST_EXPENSIVE_EXPORT_WINDOW_MINUTES . "min Export Window: Not available/found\n";
}
}
} else {
echo "\nFailed to retrieve or parse API data.\n";
}
echo "\n--- End of Example ---\n";
}
?>
Concept | Explanation |
---|---|
Dependencies | Requires the PHP cURL and JSON extensions. Check your
php.ini or use phpinfo() to confirm they are enabled.
|
Region URL | You must change the REGION_CODE
constant to match your specific UK electricity supply region. Refer to the API Endpoint URLs table. |
Window Durations | You can change the CHEAPEST_IMPORT_WINDOW_MINUTES and
MOST_EXPENSIVE_EXPORT_WINDOW_MINUTES constants to other supported values
(e.g., 60, 180, 360). Note that the keys in the JSON are strings (e.g., '60', '120'), so
the script converts the number to a string when looking up the window data.
|
Fetching Data (cURL) | The getAgileRatesData function uses the cURL library to
perform the HTTP GET request. It sets options for returning the response, timeout,
failing on HTTP errors (>=400), and setting an `Accept` header. It checks for cURL
errors, HTTP status codes, content type, and JSON decoding errors before returning the
parsed associative array or null . |
Processing Data | The processAgileData function takes the raw data array
and extracts the specific pieces of information. It uses null-coalescing operators
(?? ) for safer access to potentially missing array keys. |
Timezones & Timestamps | The API provides timestamps in ISO 8601 format ending in 'Z' (UTC).
The script explicitly uses new DateTimeZone('UTC') when creating
DateTime objects from the API strings and for getting the current time
(new DateTime('now', $utcTimeZone) ). All time comparisons are done using
Unix timestamps (getTimestamp() ), which are inherently UTC-based.
|
Current Rate Logic | It iterates through the rates array, parses the
deliveryStart and deliveryEnd times into UTC
DateTime objects, and checks if the current UTC timestamp falls within that
slot's range using getTimestamp() .
|
Window Logic | The findWindowData closure (anonymous function) finds
the correct pre-calculated window. It identifies the latest timestamp key (which is an
ISO string) in the cheapestWindowsPerSlot or
windowsExportPerSlot array that represents a time less than or equal to the
current time, then retrieves the data for the requested duration key (e.g., '120' or
'60').
|
Error Handling | Includes checks for cURL errors (curl_errno ), HTTP
status codes, content type, JSON decoding errors (json_last_error ), and
uses try...catch blocks for potential exceptions during date parsing or
array access within the processing logic. Errors are printed to standard output (for CLI
usage) or could be logged using PHP's logging mechanisms. |
Modularity | The fetching and processing logic are separated into functions
(getAgileRatesData , processAgileData ) for better organization
and potential reuse. |
Usage | Save the code as a PHP file (e.g., agile_checker.php ).
You can run it from your command line: php agile_checker.php .Alternatively, you can integrate the functions into a web application, ensuring appropriate output formatting (e.g., using htmlspecialchars if displaying
results in HTML) and potentially caching the results to avoid hitting the API on every
page load. The
if (basename(__FILE__) === basename($_SERVER['SCRIPT_FILENAME'])) block
prevents the example output from running if the file is included elsewhere.
|
Here is an example demonstrating how to fetch and process data from the Agile Rates API using standard Java (11+). This script uses the built-in HttpClient
for HTTP calls and the popular Jackson library for JSON parsing.
First, ensure you have the necessary Jackson libraries in your project. Add the following dependencies to your pom.xml
file:
<!-- In your pom.xml -->
<dependencies>
<!-- Jackson for JSON Processing -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.1</version> <!-- Use the latest stable version -->
</dependency>
<!-- Required for java.time.* (Instant, ZonedDateTime) support -->
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.17.1</version>
</dependency>
</dependencies>
<!-- Ensure Java 11+ is used -->
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <!-- Recommended -->
</properties>
If you are using Gradle or another build system, add the corresponding Jackson dependencies.
Below is the full Java code (AgileRatesApiClient.java
) that fetches the API data, parses it into Plain Old Java Objects (POJOs, using Records here for brevity), and extracts key information like current rates and cheapest/most expensive windows.
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; // Required for java.time.*
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
public class AgileRatesApiClient {
// --- Configuration ---
// IMPORTANT: Replace with the code for YOUR region! This example uses London (C).
private static final String REGION_CODE = "C";
private static final String API_URL = "https://agilerates.uk/api/agile_rates_region_" + REGION_CODE + ".json";
// Define the window durations you are interested in (in minutes)
private static final int CHEAPEST_IMPORT_WINDOW_MINUTES = 120; // e.g., 2 hours
private static final int MOST_EXPENSIVE_EXPORT_WINDOW_MINUTES = 60; // e.g., 1 hour
private static final HttpClient httpClient = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_2)
.connectTimeout(Duration.ofSeconds(10))
.build();
private static final ObjectMapper objectMapper = new ObjectMapper()
.registerModule(new JavaTimeModule()); // Register module for Instant/ZonedDateTime
// --- POJOs/Records to map JSON structure ---
// Use @JsonIgnoreProperties to avoid errors if API adds new fields
@JsonIgnoreProperties(ignoreUnknown = true)
public record ApiResponse(
String result,
RegionInfo regionInfo,
LastUpdate lastUpdate,
List<RateSlot> rates,
Map<String, Map<String, WindowInfo>> cheapestWindowsPerSlot,
Map<String, Map<String, WindowInfo>> windowsExportPerSlot
) {}
@JsonIgnoreProperties(ignoreUnknown = true)
public record RegionInfo(String name, String code) {}
@JsonIgnoreProperties(ignoreUnknown = true)
public record LastUpdate(CacheInfo cache) {}
@JsonIgnoreProperties(ignoreUnknown = true)
public record CacheInfo(Instant agileRates) {} // Jackson JSR310 module handles ISO string to Instant
@JsonIgnoreProperties(ignoreUnknown = true)
public record RateSlot(
Instant deliveryStart, // Jackson JSR310 module handles ISO string to Instant
Instant deliveryEnd,
AgileRate agileRate,
AgileRate agileOutgoingRate
) {}
@JsonIgnoreProperties(ignoreUnknown = true)
public record AgileRate(AgileRateResult result) {}
@JsonIgnoreProperties(ignoreUnknown = true)
public record AgileRateResult(Double rate, String source, boolean prediction) {}
@JsonIgnoreProperties(ignoreUnknown = true)
public record WindowInfo(
Instant startTime, // Jackson JSR310 module handles ISO string to Instant
Instant endTime,
Double averageRate,
boolean prediction
) {}
// --- Main Logic ---
public static void main(String[] args) {
System.out.println("--- Agile Rates API Java Example ---");
System.out.println("Fetching data for region: " + REGION_CODE + " from " + API_URL);
try {
HttpRequest request = HttpRequest.newBuilder()
.GET()
.uri(URI.create(API_URL))
.header("Accept", "application/json")
.timeout(Duration.ofSeconds(10))
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
// Check response status
if (response.statusCode() != 200) {
System.err.println("HTTP Error: Received status code " + response.statusCode());
System.err.println("Response Body: " + response.body().substring(0, Math.min(500, response.body().length())) + "...");
return;
}
// Check content type
String contentType = response.headers().firstValue("content-type").orElse("");
if (!contentType.toLowerCase().contains("application/json")) {
System.err.println("Error: Unexpected content type: " + contentType);
System.err.println("Response Body: " + response.body().substring(0, Math.min(500, response.body().length())) + "...");
return;
}
// Parse JSON
ApiResponse apiResponse = objectMapper.readValue(response.body(), ApiResponse.class);
if (apiResponse == null || !"ok".equalsIgnoreCase(apiResponse.result())) {
System.err.println("API response result was not 'ok' or parsing failed.");
return;
}
System.out.println("Data fetched and parsed successfully.");
processAndPrintData(apiResponse);
} catch (IOException | InterruptedException e) {
System.err.println("Error fetching data: " + e.getMessage());
Thread.currentThread().interrupt(); // Restore interrupt status
} catch (JsonProcessingException e) {
System.err.println("Error parsing JSON response: " + e.getMessage());
} catch (Exception e) {
System.err.println("An unexpected error occurred: " + e.getMessage());
e.printStackTrace();
}
System.out.println("\n--- End of Example ---");
}
private static void processAndPrintData(ApiResponse data) {
Instant nowUtc = Instant.now();
Double currentImportRate = null;
Double currentExportRate = null;
Double nextHourAvgImportRate = null;
// --- 1. Find Current Import/Export Rates ---
if (data.rates() != null) {
for (RateSlot slot : data.rates()) {
if (slot.deliveryStart() != null && slot.deliveryEnd() != null &&
!nowUtc.isBefore(slot.deliveryStart()) && nowUtc.isBefore(slot.deliveryEnd())) {
currentImportRate = Optional.ofNullable(slot.agileRate())
.map(AgileRate::result)
.map(AgileRateResult::rate)
.orElse(null);
currentExportRate = Optional.ofNullable(slot.agileOutgoingRate())
.map(AgileRate::result)
.map(AgileRateResult::rate)
.orElse(null);
break; // Found current slot
}
}
}
// --- 2. Calculate Next Hour's Average Import Rate ---
if (data.rates() != null) {
List<Double> nextTwoRates = data.rates().stream()
.filter(slot -> slot.deliveryStart() != null && !slot.deliveryStart().isBefore(nowUtc))
.sorted(Comparator.comparing(RateSlot::deliveryStart)) // Ensure correct order
.map(slot -> Optional.ofNullable(slot.agileRate())
.map(AgileRate::result)
.map(AgileRateResult::rate)
.orElse(null))
.filter(rate -> rate != null) // Filter out null rates
.limit(2)
.toList(); // Java 16+
if (nextTwoRates.size() == 2) {
nextHourAvgImportRate = (nextTwoRates.get(0) + nextTwoRates.get(1)) / 2.0;
}
}
// --- 3. Find Cheapest Import Window ---
WindowInfo cheapestWindow = findWindowData(data.cheapestWindowsPerSlot(), String.valueOf(CHEAPEST_IMPORT_WINDOW_MINUTES), nowUtc);
// --- 4. Find Most Expensive Export Window ---
WindowInfo expensiveWindow = findWindowData(data.windowsExportPerSlot(), String.valueOf(MOST_EXPENSIVE_EXPORT_WINDOW_MINUTES), nowUtc);
// --- 5. Print Results ---
System.out.println("\n--- Processed Results ---");
String regionName = Optional.ofNullable(data.regionInfo()).map(RegionInfo::name).orElse("N/A");
String regionCode = Optional.ofNullable(data.regionInfo()).map(RegionInfo::code).orElse("N/A");
String lastUpdateStr = Optional.ofNullable(data.lastUpdate())
.map(LastUpdate::cache)
.map(CacheInfo::agileRates)
.map(inst -> inst.atZone(ZoneOffset.UTC).format(DateTimeFormatter.ISO_INSTANT))
.orElse("N/A");
System.out.println("Region: " + regionName + " (" + regionCode + ")");
System.out.println("API Data Last Updated: " + lastUpdateStr);
System.out.println("-".repeat(20));
System.out.printf("Current Import Rate: %s p/kWh%n", formatDouble(currentImportRate));
System.out.printf("Current Export Rate: %s p/kWh%n", formatDouble(currentExportRate));
System.out.printf("Next Hour Avg Import Rate: %s p/kWh%n", formatDouble(nextHourAvgImportRate));
System.out.println("-".repeat(20));
System.out.println("Cheapest " + CHEAPEST_IMPORT_WINDOW_MINUTES + "min Import Window:");
printWindowInfo(cheapestWindow);
System.out.println("-".repeat(20));
System.out.println("Most Expensive " + MOST_EXPENSIVE_EXPORT_WINDOW_MINUTES + "min Export Window:");
printWindowInfo(expensiveWindow);
}
/**
* Helper function to find the relevant window data based on the current time.
*/
private static WindowInfo findWindowData(Map<String, Map<String, WindowInfo>> windowMap, String durationKey, Instant currentTime) {
if (windowMap == null || windowMap.isEmpty()) {
return null;
}
// Find the latest calculation key (ISO timestamp string) that is <= currentTime
Optional<Instant> latestValidKeyTime = windowMap.keySet().stream()
.map(keyStr -> {
try {
return Instant.parse(keyStr);
} catch (Exception e) {
// System.err.println("Warning: Could not parse window key: " + keyStr);
return null; // Ignore invalid keys
}
})
.filter(keyTime -> keyTime != null && !keyTime.isAfter(currentTime))
.max(Comparator.naturalOrder()); // Find the latest time <= now
if (latestValidKeyTime.isPresent()) {
String latestValidKeyStr = latestValidKeyTime.get().toString(); // Convert back to ISO string format used in map
Map<String, WindowInfo> windowsForSlot = windowMap.get(latestValidKeyStr);
if (windowsForSlot != null) {
return windowsForSlot.get(durationKey); // Get the specific duration window
}
}
return null;
}
/**
* Helper to format window info for printing.
*/
private static void printWindowInfo(WindowInfo window) {
if (window != null) {
// Format Instant to a readable UTC string
DateTimeFormatter formatter = DateTimeFormatter.ISO_INSTANT.withZone(ZoneOffset.UTC);
System.out.printf(" Start Time: %s%n", window.startTime() != null ? formatter.format(window.startTime()) : "N/A");
System.out.printf(" End Time: %s%n", window.endTime() != null ? formatter.format(window.endTime()) : "N/A");
System.out.printf(" Avg Rate: %s p/kWh%n", formatDouble(window.averageRate()));
System.out.printf(" Prediction: %s%n", window.prediction() ? "Yes" : "No");
} else {
System.out.println(" Not available/found");
}
}
/**
* Helper to format Double values nicely, handling nulls.
*/
private static String formatDouble(Double value) {
return value != null ? String.format("%.2f", value) : "N/A";
}
}
Concept | Explanation |
---|---|
Dependencies | Requires Jackson libraries (`jackson-databind` for core functionality, `jackson-datatype-jsr310` for `java.time` support). Ensure they are included via Maven, Gradle, or your build system. |
Region URL | You must change the REGION_CODE constant (and thus the API_URL ) to match your specific UK electricity supply region. Refer to the API Endpoint URLs table. |
Window Durations | Adjust the CHEAPEST_IMPORT_WINDOW_MINUTES and MOST_EXPENSIVE_EXPORT_WINDOW_MINUTES constants if you need different window lengths (e.g., 60, 180, 360). |
HTTP Client | Uses Java 11's built-in java.net.http.HttpClient for making the GET request. Includes basic timeout and header configuration. |
JSON Parsing (Jackson) | An ObjectMapper (with the JavaTimeModule registered) deserialises the JSON response into Java objects (Records/POJOs like ApiResponse , RateSlot , etc.). @JsonIgnoreProperties(ignoreUnknown = true) helps prevent errors if the API adds new fields. |
Time Handling | Uses the java.time package (`Instant`). The Jackson `JavaTimeModule` handles parsing ISO 8601 UTC strings ('Z') directly into `Instant` objects. Instant.now() gets the current UTC time. Comparisons are done using `Instant::isBefore`/`isAfter`. |
Current Rate Logic | Iterates through the rates list, comparing the current `Instant` with the `deliveryStart` and `deliveryEnd` `Instant`s of each slot to find the active rate. |
Window Logic | The findWindowData helper function finds the correct pre-calculated window. It parses the timestamp keys (ISO strings) of the `cheapestWindowsPerSlot` or `windowsExportPerSlot` map, finds the latest key representing a time less than or equal to the current time, and then retrieves the specific `WindowInfo` for the requested duration key (e.g., "120"). |
Error Handling | Includes try-catch blocks for network issues (`IOException`, `InterruptedException`), JSON parsing errors (`JsonProcessingException`), and general exceptions. Basic checks for HTTP status code and content type are performed. |
Usage | Compile and run the Java class. The example prints results to the console. You can adapt this code to integrate into larger Java applications, backend services, or scheduled tasks. |