Skip to main content

Energy — electrical grid telemetry

The problem. A distribution operator runs a region of substations, each stepping power down through transformers onto feeders that serve neighbourhoods. Operators need second-by-second load, voltage and frequency; planners need hourly and daily roll-ups to spot trends and size upgrades; and any feeder pushed past its rating must raise an overload alarm.

This scenario centres on aggregation — turning a firehose of telemetry into the roll-ups a SCADA dashboard and a planning report actually consume.

Set up demo data

New workspace? Run this once (Python) to create a feeder's load series with a day of hourly readings including an overload peak — so the roll-up and overload event below have data. The region/substation graph is created by step 1. Safe to re-run.

import datahub_sdk, numpy as np, pandas as pd

client = datahub_sdk.DataHubClient.from_env()

client.timeseries.create([datahub_sdk.TimeSeries(external_id="feeder_f12_load_mw", name="Feeder F12 load", unit="mw")])
idx = pd.date_range(end=pd.Timestamp.now(tz="UTC"), periods=24, freq="1h")
load = 8 + 4 * np.sin(np.arange(24) / 24 * 2 * np.pi) + np.random.normal(0, 0.5, 24)
load[18] = 14.8 # an evening overload past the 12 MW rating
client.timeseries.insert_from_lists(timestamps=idx, values=load, ts="feeder_f12_load_mw")

1. Model the network

A region contains substations, a substation contains transformers, a transformer feeds feeders. The same graph pattern as any asset hierarchy.

ResourceForm region = new ResourceForm();
region.setExternalId("grid_region_east");
region.setName("Eastern region");
region.setLabels(List.of("Region"));

ResourceForm substation = new ResourceForm();
substation.setExternalId("substation_oslo_1");
substation.setName("Oslo substation 1");
substation.setLabels(List.of("Substation"));

RelForm contains = new RelForm();
contains.setName("contains");
contains.setFromExternalId("grid_region_east");
contains.setToExternalId("substation_oslo_1");

client.resources().create(List.of(region, substation), List.of(contains));

Per-feeder series follow the same naming: feeder_f12_load_mw, feeder_f12_voltage_kv, substation_oslo_1_frequency_hz.

2. Roll telemetry up for the dashboard

Raw load is far too dense to chart directly. Ask for hourly averages and peaks and the platform returns one value per bucket. The full mechanics are in Query & aggregate.

var filter = new RetrieveFilter();
filter.setExternalId("feeder_f12_load_mw");
filter.setStart(ZonedDateTime.now().minusDays(1));
filter.setEnd(ZonedDateTime.now());
filter.setAggregates(List.of("avg", "max"));
filter.setGranularity("1h");

var request = new DataRetriever<RetrieveFilter>();
request.setItems(List.of(filter));

client.timeseries().retrieve(request).getItems().get(0).getDatapoints()
.forEach(p -> chart(p.getTimestamp(), p.getValue())); // hourly load

3. Overload alarm

When a feeder's hourly peak exceeds its rating, record a feeder_overload event so the planning team sees a ranked list of strained circuits.

EventModel event = new EventModel();
event.setExternalId("feeder_overload_f12_" + System.currentTimeMillis());
event.setType("feeder_overload");
event.setStatus("open");
event.setMetadata(Map.of("feeder", "feeder_f12", "peak_mw", "14.8", "rating_mw", "12.0"));
event.setEventTime(ZonedDateTime.now());
client.events().create(List.of(event));

See the result

Chart the feeder's hourly peak load — one bar pokes above the 12 MW rating line, which is the overload the event flags:

import matplotlib.pyplot as plt

rf = datahub_sdk.RetrieveFilter(ts="feeder_f12_load_mw",
start=pd.Timestamp.now(tz="UTC") - pd.Timedelta(days=1), end=pd.Timestamp.now(tz="UTC"),
aggregates=["max"], granularity="1h")
peaks = [dp.max for dp in client.timeseries.retrieve_datapoints(rf)[0].get_datapoints()]
plt.bar(range(len(peaks)), peaks); plt.axhline(12, color="r", ls="--")
plt.title("Feeder F12 hourly peak load (MW)"); plt.show()

See also