Agile Rates API Documentation

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.

Data Refresh Schedule

The data is refreshed twice daily.

  • Around 10:00 AM UK time: Incorporates the 60-minute auction data from Nord Pool (N2EX Day Ahead UK) and Epex Spot (GB Day Ahead 60min).
  • Around 4:00 PM UK time: Incorporates the 30-minute auction data from Epex Spot (GB Day Ahead 30min).

New rates typically become available shortly after these times for the following day (00:00 to 23:59).

Data Sources & Copyright

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.

Usage Disclaimer

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).

Table of Contents

API Endpoint URLs (by Region)

Switch to Octopus Energy & Get Free Credit!

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!

JSON Structure Overview

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 Object

This 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)
}
  • MPAN (Meter Point Administration Number) - The first two digits identify the distribution network operator (DNO) region.
  • W - This value is a wholesale rate used for the calculation.
  • B, C, M Parameters (Agile Outgoing) - These values are used in the formula 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.
  • D, P Parameters (Agile Import) - These values are used in the formula 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 Object

Provides 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 Object

Contains 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"
        }
      }
    ]
  }
}

Contains 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 Array

This 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 Object

This 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 Object

Similar 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 ...
}

Disclaimers

Usage

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 Data

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 Data

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

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.

Home Assistant Integration

Here are examples for integrating the Agile Rates API into Home Assistant:

1. Fetching the Full API Data (REST Sensor)

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.

2. Extracting Specific Values (Template Sensors)

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 }}

Explanation & Considerations

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").

Agile Rates on Zapier

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:

  • Trigger: Agile Rates Are Updated

    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:
    • Send yourself a summary notification (via Email, SMS, Slack, Discord, Pushbullet) with tomorrow's highest and lowest price points.
    • Log the date and time of the update to a Google Sheet to monitor reliability.
    • Trigger a custom webhook to your Home Assistant, Node-RED, or other system to signal that fresh data is ready for polling.
    • Update a status message on a digital dashboard (e.g., DAKboard, Geckoboard via Zapier) indicating "New Rates Available".
  • Action: Get Cheapest Window

    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:
    • Send the cheapest window start/end times via webhook to your smart home hub (Home Assistant, Hubitat) to automatically schedule EV charging or tumble dryer runs.
    • Create a Google Calendar event named "Run Washing Machine" during the cheapest 3-hour window.
    • Send an SMS or email notification: "Best time to charge your power bank today is between [Start Time] and [End Time] (Avg: [Rate]p/kWh)".
    • Update a value in a smart home system (via webhook) that enables certain automations only during this cheap period.
  • Action: Get Most Expensive Export Windows

    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:
    • If your home battery supports control via Zapier or webhooks, trigger it to start discharging/exporting during the identified peak payout window.
    • Send a notification: "Maximise earnings! Best export window today is [Start Time] - [End Time] (Avg: [Rate]p/kWh)".
    • Log the peak export times and rates to a Google Sheet to track potential earnings from solar/battery export.
    • Use Zapier's scheduling features to run this action daily and plan export strategies.
  • Action: Get Current Rate

    Retrieves the Agile import and/or export rate applicable for the current 30-minute slot in your specified region.

    Use Case Examples:
    • Create a Zap that runs every 30 minutes: If the current import rate is below 0p/kWh, send a webhook to turn on immersion heater/smart plugs.
    • Log the current import rate every hour to a database (MySQL, PostgreSQL via Zapier) or a Google Sheet for detailed usage analysis.
    • Set up a Zap filter: If the current export rate exceeds 25p/kWh, send a priority notification to consider discharging your battery.
    • Display the current rate on a compatible smart display or dashboard service connected to Zapier.
  • Action: Get Rates for Given Time

    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:
    • Check the predicted rate for tomorrow afternoon: If it's high, send an email reminder to pre-charge devices today.
    • Generate a custom daily briefing email listing the rates during your family's peak evening usage hours (e.g., 5 PM - 8 PM).
    • Feed specific future rate data via webhook into a custom planning tool or script.

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!

Node-RED Integration

Here's an example flow for integrating the Agile Rates API into Node-RED:

1. Basic Flow Structure

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:

  • Replace 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).
  • In the 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).

2. Explanation of Nodes

  • Inject ("Fetch every 15 mins"): Triggers the flow automatically every 900 seconds (15 minutes) after deployment and also allows manual triggering by clicking its button.
  • HTTP Request ("Get Agile Rates API Data"): Fetches the JSON data from the specified Agile Rates API URL. It's configured to automatically parse the response as a JSON object (msg.payload).
  • Function ("Extract Agile Rates Info"): This node contains JavaScript code to:
    • Get the current time.
    • Iterate through the rates array in the payload to find the agileRate.result.rate and agileOutgoingRate.result.rate for the current 30-minute slot.
    • Calculate the average import rate for the next two 30-minute slots (next hour).
    • Find the cheapest 2-hour (120 min) import window data (start time, average rate) based on the latest calculation available relative to the current time.
    • Find the most expensive 1-hour (60 min) export window data similarly.
    • Extract the API's last update timestamp.
    • Packages all this extracted information into `msg.payload` (or optionally sends multiple messages, see commented code).
  • Catch ("Catch HTTP Errors"): Catches errors specifically from the HTTP Request node (e.g., network timeout, invalid URL).
  • Debug ("Processed Agile Data" / "HTTP Request Error"): Displays the output message (either the processed data or the error details) in the Node-RED Debug sidebar.

3. Using the Data

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:

  • Dashboard Nodes: Use ui_text, ui_gauge, or ui_chart nodes (from the node-red-dashboard package) to display rates and times.
  • Change/Set Nodes: Store values in flow or global context variables (e.g., flow.set("currentAgileImport", msg.payload.currentImportRate)) for use in other flows or logic.
  • Switch Nodes: Route the flow based on rate values (e.g., if msg.payload.currentImportRate < 10).
  • Trigger Nodes / Automations: Start processes (like controlling smart plugs, sending notifications) based on current rates or upcoming cheap/expensive windows.

4. Considerations

  • Error Handling: The example includes basic HTTP error catching. You might want to add more checks inside the Function node (e.g., verifying the structure of `msg.payload` before accessing nested properties) for robustness.
  • Timezones: The API provides UTC times. Node-RED's `new Date()` uses the server's local timezone by default for display, but `getTime()` provides UTC milliseconds, ensuring correct comparisons with the API's UTC timestamps. Be mindful of this if displaying times directly.
  • Rate Limits/Polling: The API is intended for personal use. Fetching every 15 minutes is generally fine, but avoid excessively frequent requests. The data only updates twice a day (around 10 AM and 4 PM UK time).

Domoticz Integration

Here is an example for integrating the Agile Rates API into Domoticz using dzVents (Lua scripting):

1. Domoticz Setup: Create Virtual Devices

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):

  • Agile Current Import Rate: Type "Custom Sensor", Axis Label "p/kWh". Note its IDX.
  • Agile Current Export Rate: Type "Custom Sensor", Axis Label "p/kWh". Note its IDX.
  • Agile Next Hour Average Import Rate: Type "Custom Sensor", Axis Label "p/kWh". Note its IDX.
  • Agile Cheapest 2h Window Start Time: Type "Text". Note its IDX.
  • Agile Cheapest 2h Window Average Rate: Type "Custom Sensor", Axis Label "p/kWh". Note its IDX.
  • Agile Most Expensive 1h Export Window Start Time: Type "Text". Note its IDX.
  • Agile Most Expensive 1h Export Window Average Rate: Type "Custom Sensor", Axis Label "p/kWh". Note its IDX.
  • Agile Rates API Last Update: Type "Text". Note its IDX.

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.

2. dzVents Script

Create a new Lua script in your Domoticz `scripts/dzVents/scripts` directory (e.g., `AgileRates.lua`). Paste the following code into the script.

IMPORTANT:

  • Replace the placeholder 'https://agilerates.uk/api/agile_rates_region_C.json' with the correct API URL for your region from the API Endpoint URLs table.
  • Replace the device names like '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
}

3. Explanation & Considerations

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").

Python Integration

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.

Python Script Example

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 ---")

Explanation & Considerations

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.

Node.js Integration

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');.

Node.js Script Example

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} A promise that resolves with the parsed JSON data, or null if an error occurs.
         */
        async function getAgileRatesData(apiUrl) {
            console.log(`Fetching data from: ${apiUrl}`);
            try {
                const response = await fetch(apiUrl, { timeout: 10000 }); // Add a 10-second timeout

                if (!response.ok) {
                    throw new Error(`HTTP error! Status: ${response.status} ${response.statusText}`);
                }

                // Check if the content type is JSON before attempting to parse
                const contentType = response.headers.get('content-type');
                if (!contentType || !contentType.includes('application/json')) {
                    console.error(`Error: Unexpected content type: ${contentType}`);
                    const text = await response.text();
                    console.error(`Raw response text: ${text.substring(0, 500)}...`);
                    return null;
                }

                const data = await response.json();
                console.log("Data fetched and parsed successfully.");
                return data;

            } catch (error) {
                console.error(`Error fetching or parsing data: ${error.message}`);
                if (error.cause) { // Log underlying cause if available (e.g., from fetch)
                     console.error(`Cause: ${error.cause}`);
                }
                return null;
            }
        }

        /**
         * Processes the fetched Agile Rates data to extract key information.
         *
         * @param {object} data - The dictionary containing the parsed JSON data from the API.
         * @returns {object} An object containing extracted information (current rates, windows, etc.).
         */
        function processAgileData(data) {
            const results = {
                currentImportRate: null,
                currentExportRate: null,
                nextHourAvgImportRate: null,
                cheapestImportWindow: null,
                mostExpensiveExportWindow: null,
                apiLastUpdate: null,
                regionInfo: null,
                error: null
            };

            if (!data || data.result !== 'ok') {
                results.error = "Invalid or missing data in API response.";
                console.error(`Error: ${results.error}`);
                return results;
            }

            try {
                results.regionInfo = data.regionInfo;
                results.apiLastUpdate = data.lastUpdate?.cache?.agileRates; // Optional chaining

                const now = new Date(); // Current time
                const now_ts = now.getTime(); // Current time as Unix timestamp (milliseconds, UTC based)
                const rates = data.rates || [];

                // --- 1. Find Current Import/Export Rates ---
                for (const rateSlot of rates) {
                    try {
                        const startTime = new Date(rateSlot.deliveryStart); // Parses ISO 8601 UTC string
                        const endTime = new Date(rateSlot.deliveryEnd);

                        // Check if dates are valid and current time falls within the slot
                        if (!isNaN(startTime) && !isNaN(endTime) && startTime.getTime() <= now_ts && now_ts < endTime.getTime()) {
                            results.currentImportRate = rateSlot.agileRate?.result?.rate;
                            results.currentExportRate = rateSlot.agileOutgoingRate?.result?.rate;
                            break; // Found the current slot
                        }
                    } catch (e) {
                         console.warn(`Warning: Could not process rate slot ${rateSlot?.deliveryStart}: ${e.message}`);
                         continue; // Skip slot on error
                    }
                }

                // --- 2. Calculate Next Hour's Average Import Rate ---
                const nextRatesFound = [];
                for (const rateSlot of rates) {
                    try {
                        const startTime = new Date(rateSlot.deliveryStart);
                        if (!isNaN(startTime) && startTime.getTime() >= now_ts && nextRatesFound.length < 2) {
                            const rateValue = rateSlot.agileRate?.result?.rate;
                            if (typeof rateValue === 'number') {
                                nextRatesFound.push(rateValue);
                            }
                        }
                        if (nextRatesFound.length === 2) {
                            break; // Got the next two slots
                        }
                    } catch (e) {
                         console.warn(`Warning: Could not process rate slot for next hour calc ${rateSlot?.deliveryStart}: ${e.message}`);
                         continue;
                    }
                }

                if (nextRatesFound.length === 2) {
                    results.nextHourAvgImportRate = parseFloat(((nextRatesFound[0] + nextRatesFound[1]) / 2).toFixed(2));
                }

                /**
                 * Helper function to find the relevant window data based on the current time.
                 * @param {object|null} windowObject - The cheapestWindowsPerSlot or windowsExportPerSlot object.
                 * @param {string} durationMinutesKey - The duration key (e.g., '60', '120').
                 * @param {number} currentTimeMs - The current time in milliseconds (UTC).
                 * @returns {object|null} The window data {startTime, endTime, averageRate, prediction} or null.
                 */
                function findWindowData(windowObject, durationMinutesKey, currentTimeMs) {
                    if (!windowObject || typeof windowObject !== 'object') {
                        return null;
                    }

                    let latestValidKeyTs = 0;
                    let latestValidKeyStr = null;

                    // Find the latest calculation key (slot start time) that is <= now
                    for (const keyStr in windowObject) {
                        if (Object.hasOwnProperty.call(windowObject, keyStr)) {
                            try {
                                const keyTime = new Date(keyStr); // Parses ISO 8601 UTC string
                                if (isNaN(keyTime)) continue; // Skip invalid date keys

                                const keyTs = keyTime.getTime();
                                if (keyTs <= currentTimeMs && keyTs > latestValidKeyTs) {
                                    latestValidKeyTs = keyTs;
                                    latestValidKeyStr = keyStr;
                                }
                            } catch (e) {
                                console.warn(`Warning: Could not parse window key: ${keyStr}: ${e.message}`);
                                continue;
                            }
                        }
                    }

                    // If a valid key was found, extract the specific window data
                    if (latestValidKeyStr) {
                        const durationData = windowObject[latestValidKeyStr]?.[durationMinutesKey];
                        // Basic validation
                        if (durationData && typeof durationData === 'object' &&
                            'startTime' in durationData && 'endTime' in durationData && 'averageRate' in durationData) {
                            return durationData;
                        }
                    }
                    return null;
                }

                // --- 3. Find Cheapest Import Window ---
                results.cheapestImportWindow = findWindowData(
                    data.cheapestWindowsPerSlot,
                    String(CHEAPEST_IMPORT_WINDOW_MINUTES), // Key must be a string
                    now_ts
                );

                // --- 4. Find Most Expensive Export Window ---
                results.mostExpensiveExportWindow = findWindowData(
                    data.windowsExportPerSlot,
                    String(MOST_EXPENSIVE_EXPORT_WINDOW_MINUTES), // Key must be a string
                    now_ts
                );

            } catch (error) {
                results.error = `An unexpected error occurred during processing: ${error.message}`;
                console.error(`Error: ${results.error}`);
                console.error(error.stack); // Log stack trace for debugging
            }

            return results;
        }

        // --- Main Execution ---
        async function main() {
            console.log("--- Agile Rates API Node.js Example ---");

            const apiData = await getAgileRatesData(API_URL);

            if (apiData) {
                const processedResults = processAgileData(apiData);

                console.log("\n--- Processed Results ---");
                if (processedResults.error) {
                    console.error(`Processing Error: ${processedResults.error}`);
                } else {
                    const regionName = processedResults.regionInfo?.name ?? 'N/A';
                    const regionCode = processedResults.regionInfo?.code ?? 'N/A';
                    console.log(`Region: ${regionName} (${regionCode})`);
                    console.log(`API Data Last Updated: ${processedResults.apiLastUpdate ?? 'N/A'}`);
                    console.log("-".repeat(20));
                    console.log(`Current Import Rate: ${processedResults.currentImportRate ?? 'N/A'} p/kWh`);
                    console.log(`Current Export Rate: ${processedResults.currentExportRate ?? 'N/A'} p/kWh`);
                    console.log(`Next Hour Avg Import Rate: ${processedResults.nextHourAvgImportRate ?? 'N/A'} p/kWh`);
                    console.log("-".repeat(20));

                    const cheapestWindow = processedResults.cheapestImportWindow;
                    if (cheapestWindow) {
                        console.log(`Cheapest ${CHEAPEST_IMPORT_WINDOW_MINUTES}min Import Window:`);
                        console.log(`  Start Time: ${cheapestWindow.startTime ?? 'N/A'}`);
                        console.log(`  End Time:   ${cheapestWindow.endTime ?? 'N/A'}`);
                        console.log(`  Avg Rate:   ${cheapestWindow.averageRate ?? 'N/A'} p/kWh`);
                        console.log(`  Prediction: ${cheapestWindow.prediction ?? 'N/A'}`);
                    } else {
                        console.log(`Cheapest ${CHEAPEST_IMPORT_WINDOW_MINUTES}min Import Window: Not available/found`);
                    }

                    console.log("-".repeat(20));
                    const expensiveWindow = processedResults.mostExpensiveExportWindow;
                    if (expensiveWindow) {
                        console.log(`Most Expensive ${MOST_EXPENSIVE_EXPORT_WINDOW_MINUTES}min Export Window:`);
                        console.log(`  Start Time: ${expensiveWindow.startTime ?? 'N/A'}`);
                        console.log(`  End Time:   ${expensiveWindow.endTime ?? 'N/A'}`);
                        console.log(`  Avg Rate:   ${expensiveWindow.averageRate ?? 'N/A'} p/kWh`);
                        console.log(`  Prediction: ${expensiveWindow.prediction ?? 'N/A'}`);
                    } else {
                        console.log(`Most Expensive ${MOST_EXPENSIVE_EXPORT_WINDOW_MINUTES}min Export Window: Not available/found`);
                    }
                }
            } else {
                console.error("\nFailed to retrieve or parse API data.");
            }

            console.log("\n--- End of Example ---");
        }

        // Run the main function
        main().catch(error => {
            console.error("An unexpected error occurred in main:", error);
        });

        

Explanation & Considerations

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).

PHP Integration

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).

PHP Script Example

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";
}

?>

Explanation & Considerations

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.

Java Integration

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.

1. Dependencies (Maven)

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.

2. Java Code Example

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";
            }
        }
        

Explanation & Considerations

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.