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 have | Send | Notes |
|---|---|---|
| A process historian's archive (PI, eDNA, Canary, a SCADA historian), compressed | Bars built from the points it kept, as kept | Do not interpolate onto a grid first: each kept point marks a change |
| Points with quality codes | Only good-quality points | Map 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 offset | Remove the offset when you draw |
| Interval meter reads (5, 15 or 30 minutes) | One bar per read interval, or per hour | An interval with no flow is a gap |
| Status points (breaker open or closed, pump running) | Not Delve's input | Draw them beside the segments from your own store |
| Asset view | Bar width that usually fits |
|---|---|
| A substation or plant over days to weeks | 15 minutes |
| A feeder or inverter over hours to days | 1 to 5 minutes |
| A fleet over months | 1 hour |
The plan for the job
| Use | Plan |
|---|---|
| An engineering or asset-management product, used in office hours | Production |
| A tool in the control room, used in real-time operations | Mission Critical, in your jurisdiction, with private connectivity |
| A transmission operator, or anything your regulator treats as critical outsourcing | Dedicated |
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
"""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
| Question | Answer |
|---|---|
| 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) |