Single-Series Push Subscription API

Recommended for new integrations: use Multi-Series Push

🔹

For new push integrations, we recommend using the Multi-Series Push Subscription API instead of single-series push subscriptions.

A multi-series subscription costs the same API usage as a single-series push subscription, but allows you to subscribe to multiple series and option sets in one request.

Use this single-series push service if you are maintaining an existing integration or specifically need the older single-series DPS flow. For new long-running Python clients, use MultiSeriesDPSHelper.

The single-series push service delivers real-time updates for one Enact data series over SignalR.

A subscription is created for one SeriesId, with an optional CountryId and optional OptionId. Updates are then pushed to your SignalR client whenever values change.


Usage

You can use the single-series push service from any SignalR-compatible client.

At a high level, your client should:

  1. Connect to the Enact DPS SignalR hub using your Enact authentication.
  2. Call JoinEnactPush with a single-series request object.
  3. Read the returned push group name from data.pushName.
  4. Register a client handler using that push group name.
  5. Process pushed updates in that handler.
  6. If the SignalR connection drops, reconnect and call ReconnectToPush with the previous push group name.
  7. If reconnect cannot restore the old group, subscribe again with JoinEnactPush.
  8. When intentionally stopping, call LeaveGroup with the push group name.

ReconnectToPush is intended to restore an existing push subscription without increasing API usage. Clients should not call JoinEnactPush repeatedly during transient reconnect issues.


Join Request Shape

Call JoinEnactPush with a single request object.

For a series with no options:

{
  "SeriesId": "RealtimeDemand",
  "CountryId": "Gb"
}

For a series with one option:

{
  "SeriesId": "OutturnFuel",
  "CountryId": "Gb",
  "OptionId": ["CCGT"]
}

For a series with multiple option dimensions:

{
  "SeriesId": "ERA5WindByZone",
  "CountryId": "Gb",
  "OptionId": ["AboveGround10M", "Z2", "Median"]
}

Request fields:

  • SeriesId: required Enact series identifier.
  • CountryId: optional country code. Defaults to "Gb" in the Python package.
  • OptionId: optional one-dimensional list of option IDs for series that require options.
🔹

Single-series OptionId is one-dimensional

For single-series push subscriptions, OptionId is a list of strings:

["CCGT"]

or:

["AboveGround10M", "Z2", "Median"]

This differs from multi-series push subscriptions, where optionIds is a two-dimensional list:

[["CCGT"], ["Wind"]]

Option Interpretation

For single-series push, OptionId describes exactly one subscription.

No Options

Some system series do not require options. For these, omit OptionId:

{
  "SeriesId": "RealtimeDemand",
  "CountryId": "Gb"
}

This subscribes to RealtimeDemand for Great Britain.

One Option

For a series with one option dimension:

{
  "SeriesId": "OutturnFuel",
  "CountryId": "Gb",
  "OptionId": ["CCGT"]
}

This subscribes to OutturnFuel for CCGT.

If you want both CCGT and Wind, use the Multi-Series Push Subscription API instead of creating separate single-series subscriptions.

Multiple Option Dimensions

For a series with multiple option dimensions, put each option value in the correct position inside the same OptionId list:

{
  "SeriesId": "ERA5WindByZone",
  "CountryId": "Gb",
  "OptionId": ["AboveGround10M", "Z2", "Median"]
}

This subscribes to ERA5WindByZone for:

  • height: AboveGround10M
  • zone: Z2
  • aggregation: Median

The position of each value matters. The first value is interpreted as the first option dimension, the second value as the second option dimension, and so on.

Plant Series

For plant series, the first option usually identifies the plant or plant group.

A specific plant:

{
  "SeriesId": "Mel",
  "CountryId": "Gb",
  "OptionId": ["T_DRAXX-1"]
}

All Battery-fuelled plants:

{
  "SeriesId": "Mel",
  "CountryId": "Gb",
  "OptionId": ["Battery"]
}

All plants in Zone 4:

{
  "SeriesId": "Mel",
  "CountryId": "Gb",
  "OptionId": ["Z4"]
}

Some plant series support additional option dimensions after the plant selector:

{
  "SeriesId": "SomePlantSeries",
  "CountryId": "Gb",
  "OptionId": ["Battery", "HalfHourly"]
}

This subscribes to Battery plants with the HalfHourly option.

For multiple independent plant subscriptions, such as Battery plants and Zone 4 plants, use the Multi-Series Push Subscription API.


Join Response

JoinEnactPush returns a response containing a push group name:

{
  "messages": [],
  "data": {
    "pushName": "..."
  }
}

Your client should register a SignalR client handler using the returned pushName. Series update payloads will be sent to that handler.

Keep the original request and returned pushName in memory. They are needed for reconnect handling.


Reconnect Flow

If the SignalR connection drops unexpectedly:

  1. Reconnect to the DPS SignalR hub.
  2. Call ReconnectToPush with the previous pushName.
  3. If it succeeds, continue listening on the same push group.
  4. If it fails for a transient reason, keep retrying ReconnectToPush.
  5. If the previous group can no longer be restored, call JoinEnactPush again with the original request and register a handler for the new pushName.

ReconnectToPush is intended to restore an existing push subscription without increasing API usage. Creating a new subscription with JoinEnactPush may count as a new subscription, so clients should not call it unnecessarily during transient reconnect issues.

For new long-running clients, prefer the multi-series helper because it handles this reconnect flow for you.


Push Payload Shape

Single-series push updates are delivered as a series update payload. The same general point-change shape is also used by multi-series pushes.

The top-level payload contains:

  • messages: error or warning messages. Empty when the push is successful.
  • data: the pushed series update.

Inside data:

  • day: trading or delivery day in ddMMyyyy format.
  • id: push identifier, usually in the format countryId&seriesId&optionId.
  • data: list of point changes.
  • replaceSeries: when true, treat the payload as replacing the current local series rather than applying it as an incremental update.

Each point change can include:

  • deletePoint: the point should be removed or treated as deleted.
  • newPoint: the point is newly added.
  • byPoint: indicates a by-point or object-point update.
  • current: the latest value after the update.
  • original: the original value supplied for this update.
  • previous: the previous value before this update.

Each value object can include:

  • arrayPoint: usually [timestampMilliseconds, value].
  • objectPoint: object-based value format for series that return named fields rather than a simple array point.
  • datePeriod: date and settlement-period metadata.

For most clients, use current.datePeriod.datePeriodCombinedGmt as the main UTC timestamp for the pushed point.


Example Payload

{
  "messages": [],
  "data": {
    "day": "26012024",
    "id": "Gb&RealtimeDemand&none",
    "data": [
      {
        "deletePoint": false,
        "newPoint": false,
        "byPoint": false,
        "current": {
          "arrayPoint": [
            1706265660000,
            30923.0
          ],
          "objectPoint": null,
          "datePeriod": {
            "date": "2024-01-26T00:00:00",
            "period": 22,
            "datePeriodCombinedGmt": "2024-01-26T10:41:00Z",
            "datePeriodStartGmt": "2024-01-26T10:30:00Z"
          }
        },
        "original": {
          "arrayPoint": [
            1706265660000,
            30923.0
          ],
          "objectPoint": null,
          "datePeriod": {
            "date": "2024-01-26T00:00:00",
            "period": 22,
            "datePeriodCombinedGmt": "2024-01-26T10:41:00Z",
            "datePeriodStartGmt": "2024-01-26T10:30:00Z"
          }
        },
        "previous": {
          "arrayPoint": [
            1706265600000,
            30954.0
          ],
          "objectPoint": null,
          "datePeriod": {
            "date": "2024-01-26T00:00:00",
            "period": 22,
            "datePeriodCombinedGmt": "2024-01-26T10:40:00Z",
            "datePeriodStartGmt": "2024-01-26T10:30:00Z"
          }
        }
      }
    ],
    "replaceSeries": false
  }
}

Applying Updates

For each point in data.data:

  1. Read the timestamp from current.datePeriod.datePeriodCombinedGmt.
  2. Read the current value from current.arrayPoint[1], or from current.objectPoint for object-based series.
  3. If deletePoint is true, remove or null the point in your local cache.
  4. If replaceSeries is true, replace your local series with the payload content.
  5. Otherwise, apply the pushed point as an incremental update.

previous is useful when you need to compare the new value with the value before the update. Most clients can process the update using current.

arrayPoint[0] is the timestamp in milliseconds since the Unix epoch. In most cases, datePeriodCombinedGmt is easier and clearer to use because it is already expressed as a UTC timestamp string.


Python Package Usage

🔹

Prefer MultiSeriesDPSHelper for new Python clients

The legacy DPSHelper.subscribe_to_series_updates method is still available for existing single-series integrations.

For new Python clients, especially long-running processes, use MultiSeriesDPSHelper instead. It supports multiple series in one subscription, automatic reconnect handling, callback buffering after reconnect, and proactive lease refresh.

Legacy Single-Series Example

import time
from lcp_delta import enact


USERNAME = "your_username"
API_KEY = "your_api_key"


def handle_updates(df):
    print("Received update")
    print(df.tail())


dps_helper = enact.DPSHelper(USERNAME, API_KEY)

dps_helper.subscribe_to_series_updates(
    handle_updates,
    series_id="RealtimeDemand",
    country_id="Gb",
    parse_datetimes=True,
)

try:
    while True:
        time.sleep(60)
except KeyboardInterrupt:
    dps_helper.terminate_hub_connection()

Method Arguments

dps_helper.subscribe_to_series_updates(
    handle_data_method,
    series_id,
    option_id=None,
    country_id="Gb",
    parse_datetimes=False,
)
  • handle_data_method: callback function called whenever the subscribed series receives an update. The callback should accept one argument, a pandas DataFrame.
  • series_id: Enact series ID to subscribe to.
  • option_id: optional one-dimensional list of option IDs for series that require options.
  • country_id: optional country ID. Defaults to "Gb".
  • parse_datetimes: when True, the returned DataFrame index is parsed as UTC datetimes. When False, timestamps are returned as strings.

Example with One Option

dps_helper.subscribe_to_series_updates(
    handle_updates,
    series_id="OutturnFuel",
    option_id=["CCGT"],
    country_id="Gb",
    parse_datetimes=True,
)

Example with Multiple Option Dimensions

dps_helper.subscribe_to_series_updates(
    handle_updates,
    series_id="ERA5WindByZone",
    option_id=["AboveGround10M", "Z2", "Median"],
    country_id="Gb",
    parse_datetimes=True,
)

Callback Data

The callback receives a pandas DataFrame.

The legacy helper maintains an in-memory view of the subscribed series. When a push arrives, it applies the update to that stored DataFrame and passes the updated DataFrame to your callback.

def handle_updates(df):
    latest_rows = df.tail(5)
    print(latest_rows)

The helper also adds a DateTimeLastUpdated column showing when each pushed point was applied locally.

Async Callback Mode

DPSHelper can be created with async_mode=True:

dps_helper = enact.DPSHelper(
    USERNAME,
    API_KEY,
    async_mode=True,
    max_workers=3,
)

In async mode:

  • Callback work is queued per series.
  • Updates for the same series are processed in order.
  • Different series can be processed concurrently, up to max_workers.

This can help if callback processing is slow, but for new multi-series integrations we still recommend MultiSeriesDPSHelper.

Reconnect Behaviour

DPSHelper includes built-in SignalR reconnect handling for unexpected connection drops. It attempts to restore prior push groups with ReconnectToPush.

For new long-running clients, MultiSeriesDPSHelper is more robust and explicit. It adds:

  • Dedicated multi-series group restoration.
  • Retry handling for transient reconnect failures.
  • Fallback to JoinMultiSeries only when the previous group cannot be restored.
  • Optional reconnect_callback before queued pushes resume.
  • Proactive lease refresh for long-running subscriptions.

Shutting Down

When your process exits, close the SignalR connection:

dps_helper.terminate_hub_connection()

This lets the helper stop its background SignalR connection cleanly.


When to Use Single-Series Push

Use single-series push if:

  • You are maintaining existing code already using DPSHelper.subscribe_to_series_updates.
  • You only need one series per subscription.
  • You specifically need the older single-series DPS behaviour.

Use multi-series push if:

  • You are building a new integration.
  • You need more than one series or option set.
  • You want stronger reconnect behaviour.
  • You are building a long-running process.
  • You want to reduce the number of separate push subscriptions your client manages.