Skip to main content

Utilities

For monitoring products and operational tools on historian, SCADA and meter data: transmission and distribution, generation fleets, water and wastewater networks, gas.

Your data, as Delve reads it​

You haveSendNotes
A process historian's archive (PI, eDNA, Canary, a SCADA historian), compressedBars built from the points it kept, as keptDo not interpolate onto a grid first: each kept point marks a change
Points with quality codesOnly good-quality pointsMap your historian's codes; a bad point is a gap, not a value
A measure that crosses zero (net export, reactive power, temperatures below zero)The measure plus a fixed offsetRemove the offset when you draw
Interval meter reads (5, 15 or 30 minutes)One bar per read interval, or per hourAn interval with no flow is a gap
Status points (breaker open or closed, pump running)Not Delve's inputDraw them beside the segments from your own store
Asset viewBar width that usually fits
A substation or plant over days to weeks15 minutes
A feeder or inverter over hours to days1 to 5 minutes
A fleet over months1 hour

The plan for the job​

UsePlan
An engineering or asset-management product, used in office hoursProduction
A tool in the control room, used in real-time operationsMission Critical, in your jurisdiction, with private connectivity
A transmission operator, or anything your regulator treats as critical outsourcingDedicated

Register your emergency conditions (a storm declaration, a system emergency) as blackout periods, so Patternode changes nothing while you are in one. Plan capacity for the storm, not the calm day: every point reporting at once, and backfills after an outage.

Where it sits in your network​

Patternode never needs a connection into your operational network. Your systems call out from the enterprise zone or a DMZ, and events come back to a broker or endpoint you control there. That keeps Patternode outside your NERC CIP electronic security perimeter and outside the control zones of an IEC 62443 design.

Example​

examples/industries/utilities/historian_to_delve.py reads an invented historian export of a zone substation transformer (top-oil temperature and load, compressed points with quality codes). It drops bad points, builds 15-minute bars from the kept points, names each series opaquely, sends the history as finished series with a pinned profile version and its edge, and then extends every tag in one request, each from its stored edge, as each interval closes. Patternode keeps nothing.

python mock/mock_patternode.py &
PATTERNODE_API_KEY=pnk_live_BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB python examples/industries/utilities/historian_to_delve.py
examples/industries/utilities/historian_to_delve.py
"""Utilities: a historian export into Delve, every tag, the way a monitoring product would.

PATTERNODE_API_KEY=pnk_live_... python examples/industries/utilities/historian_to_delve.py

Reads an export of compressed historian points (tag, time, value, quality), as PI, eDNA,
Canary or a SCADA historian produce them. For each tag it:

1. Drops points whose quality is not GOOD (your historian's codes may differ).
2. Builds bars from the points the historian kept, as kept. It never interpolates onto a
grid: a compressed archive's points already mark where the signal changed.
3. Names the series opaquely, from the site and the tag, with a salt only you hold.
4. Sends the history as a finished series, asking for its edge, and keeps both in the
segment store under the client's prefix. Patternode keeps nothing.
5. As each interval closes, extends every tag in one request: each item carries that tag's
stored edge and its new bar, and each answer's new edge goes back in the store.

The data is invented: a zone substation transformer's top-oil temperature and load.
"""

import csv
import hashlib
import json
import sys
from collections import defaultdict
from pathlib import Path

HERE = Path(__file__).resolve().parent
sys.path.insert(0, str(HERE.parents[1] / "python"))

from bars import bars_from_samples # noqa: E402
from patternode_client import Patternode # noqa: E402
from segment_store import SegmentStore # noqa: E402

CLIENT = "example-utility" # your client, never sent
SALT = "per-environment-secret" # from your secret store in production
BAR_MS = 15 * 60 * 1000 # 15-minute bars: a substation's view in hours to weeks
PROFILE = "standard@2026-11" # pinned, so an audit can reproduce every segment


def opaque(*parts: str) -> str:
return "/".join(hashlib.sha256((SALT + p).encode()).hexdigest()[:12] for p in parts)


def read_export(path: Path) -> dict[str, list[tuple[int, float]]]:
points = defaultdict(list)
with path.open() as f:
for row in csv.DictReader(f):
if row["quality"] == "GOOD":
points[row["tag"]].append((int(row["time_ms"]), float(row["value"])))
return points


def main(path: Path = HERE / "historian-export.csv", store_root: Path = Path("out")) -> None:
pn = Patternode()
store = SegmentStore(store_root / opaque(CLIENT)) # the client's prefix in your store
pending = {}
for tag, points in read_export(path).items():
site, measure = tag.split(".", 1)
name = f"{opaque(CLIENT, site)}/{opaque(measure)}"
bars = bars_from_samples(points, BAR_MS, min_samples=2)
history, live = bars[:-8], bars[-8:] # pretend the last two hours are still arriving
seg = pn.post("/v1/segmentations", {"series": name, "profile": PROFILE, "bars": history, "return_edge": True})
store.apply_segmentation(name, seg)
print(f"{tag}: {len(points)} kept points -> {seg['bars']} bars -> {len(seg['segments'])} segments "
f"({seg['profile_version']})")
pending[tag] = (name, live)

# As each 15 minutes closes, one request extends every tag from its stored edge.
for i in range(8):
items, tags = [], []
for tag, (name, live) in pending.items():
edge = store.edge(name)
items.append({"series": name, "profile": edge.profile_version, "edge": edge.token, "bars": [live[i]]})
tags.append((tag, name, edge.version))
results = pn.post("/v1/segmentations/extend/batch", {"items": items})["results"]
for (tag, name, version), r in zip(tags, results):
if r["status"] != 200:
raise SystemExit(f"{tag}: {r['error']['error']['details']['error_code']}")
store.apply_extension(name, r["result"], expected_version=version)
for s in r["result"]["closed"]:
print(f"{tag}: level {s['height']} segment closed at {s['close_time']}")
for tag, (name, _) in pending.items():
print(f"{tag}: trend {store.trend(name)}")


if __name__ == "__main__":
main(Path(sys.argv[1]) if len(sys.argv) > 1 else HERE / "historian-export.csv")

What you will be asked​

QuestionAnswer
Does data leave the country?No: choose the jurisdiction (Resilience)
Does Patternode connect into our OT network?No: outbound only, as above (Compliance)
What does Patternode see?Bars under names you made up (Data boundary)
Can an audit reproduce what operators saw?Yes: pin the profile version (Determinism)
What happens in a regional outage?It fails over within 60 seconds on Mission Critical, losing nothing acknowledged (Resilience)