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:
- Connect to the Enact DPS SignalR hub using your Enact authentication.
- Call
JoinEnactPushwith a single-series request object. - Read the returned push group name from
data.pushName. - Register a client handler using that push group name.
- Process pushed updates in that handler.
- If the SignalR connection drops, reconnect and call
ReconnectToPushwith the previous push group name. - If reconnect cannot restore the old group, subscribe again with
JoinEnactPush. - When intentionally stopping, call
LeaveGroupwith 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-seriesOptionIdis one-dimensionalFor single-series push subscriptions,
OptionIdis a list of strings:["CCGT"]or:
["AboveGround10M", "Z2", "Median"]This differs from multi-series push subscriptions, where
optionIdsis 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:
- Reconnect to the DPS SignalR hub.
- Call
ReconnectToPushwith the previouspushName. - If it succeeds, continue listening on the same push group.
- If it fails for a transient reason, keep retrying
ReconnectToPush. - If the previous group can no longer be restored, call
JoinEnactPushagain with the original request and register a handler for the newpushName.
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 inddMMyyyyformat.id: push identifier, usually in the formatcountryId&seriesId&optionId.data: list of point changes.replaceSeries: whentrue, 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:
- Read the timestamp from
current.datePeriod.datePeriodCombinedGmt. - Read the current value from
current.arrayPoint[1], or fromcurrent.objectPointfor object-based series. - If
deletePointistrue, remove or null the point in your local cache. - If
replaceSeriesistrue, replace your local series with the payload content. - 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
PreferMultiSeriesDPSHelperfor new Python clientsThe legacy
DPSHelper.subscribe_to_series_updatesmethod is still available for existing single-series integrations.For new Python clients, especially long-running processes, use
MultiSeriesDPSHelperinstead. 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 pandasDataFrame.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: whenTrue, the returned DataFrame index is parsed as UTC datetimes. WhenFalse, 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
JoinMultiSeriesonly when the previous group cannot be restored. - Optional
reconnect_callbackbefore 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.