industrial-aiops-energy — Energy Edition (Substation / Utility)
English · 中文
The energy edition of Industrial-AIOps,
split out into its own repo: read-only OT connectors for substation / utility
telecontrol protocols, built on top of iaiops.core.
- IEC 60870-5-104 (
c104) — RTU / substation telemetry - DNP3 / IEEE 1815 (
pydnp3) — outstation monitoring - IEC 61850 MMS (
pyiec61850, linux-only wheel) — substation IED reads
It reuses the base package's shared governance (audit / budget / risk-tier / undo), cross-protocol brain (data-flow / alarm / OEE / downtime RCA on the normalized ISA-95/18.2 model), and MCP server infrastructure — this repo only adds the three energy connectors + their session builders + MCP tools. Read-first: no control-direction writes are exposed.
Current release: 0.1.11 (requires iaiops>=0.20.3,<1.0). New in 0.1.11 — all three
monitor paths are CI-gated for the first time, and a skipped live test now fails the build
instead of passing it. DNP3 was the holdout: pip install pydnp3 fails on any current Linux,
so this repo inherited the ecosystem's "unbuildable on hosted runners" and
tests/test_dnp3_live.py skipped on every build. It is not unbuildable — opendnp3 compiles
clean, and the 2019 binding layer needed three mechanical fixes, now scripted in
scripts/build_pydnp3.sh and running on real GitHub runners. Also inherits two governance
fixes from iaiops 0.20.3 (pin raised): a call that failed is no longer audited as a
success (which also mis-informed the pattern circuit breaker on every failure), and the
runaway guard can now see a caller retrying a denial forever. Previously in 0.1.10 — a security
fix inherited from the base package: three egress tools this edition mirrors
(stream_publish, stream_publish_event, historian_push) wrote their credential — a NATS
auth token, a TSDB password — into the audit log in the clear, and audit_forward shipped
that row to the configured SIEM. They are defined in iaiops, so this edition could not fix
it alone; the pin moves to iaiops>=0.20.2, and a contract test now fails the build if the
whole registered surface ever again carries an undeclared credential parameter. If you have
passed a token or historian password to these tools, rotate it and check existing audit
rows. The energy connectors themselves take no credential parameters. Previously in 0.1.9 — every tool
ships the MCP ToolAnnotations hints (readOnlyHint / destructiveHint / openWorldHint),
derived from the @governed_tool harness rather than hand-written, so a client can tell a
monitor read from a tool that acts without parsing the [READ]/[WRITE] docstring tag. On the
wire that is 59 tools, 55 read-only and 0 destructive — this edition exposes no control
direction, and that is now enforced by a test rather than only documented. They are hints, not a
gate: the MCP spec forbids relying on annotations for security decisions, and enforcement stays in
@governed_tool. The base pin moved to iaiops>=0.20.1 so the hint derivation is imported from
mcp_server.hints instead of duplicated here.
Previously in 0.1.8 — the base
IAIOPS_READ_ONLY gate was removed in iaiops 0.19.0 (read/write authorisation is the
caller's decision — agent judgement / account management — not the tap's; every tool is
governed and audited via the base @governed_tool harness), so this edition drops it too.
It keeps the IAIOPS_NO_EGRESS=1 gate — a data-exfiltration / airgap axis that withholds
data-shipping tools from list_tools() at registration time. This edition runs its own
FastMCP instance, so the gate is wired into its own main(); without it IAIOPS_NO_EGRESS=1
would still expose historian_push, rca_narrate and the stream_publish* pair mirrored in
from the base brain. The energy connectors themselves are monitor-only and survive the gate
intact. See CHANGELOG.md.
Previously in 0.1.6 — an
audit-hardening pass over the three read-only connectors: DNP3 no longer reports an
offline outstation as online or returns a partial integrity-poll database; IEC-61850 gained
a bounded connect/request timeout and stopped fabricating 0.0/empty-success on failure; the
substation analyzer no longer calls a lone breaker-open a "selective trip"; tests isolate
IAIOPS_HOME; and the base pin was raised to iaiops>=0.14 so the governance endpoint-scoping
fix applies. See CHANGELOG.md §0.1.6. (0.1.5 verified the IEC-104 monitor path — a real c104
client↔server round-trip in a Linux container, tests/test_iec104_live.py.)
Physical RTU / IED remains unverified. Since 0.1.3 the server has
its own MCP identity — iaiops-energy-mcp runs a dedicated FastMCP("iaiops-energy")
instance with energy-specific instructions (IEC-104 / DNP3 / IEC-61850, read-first,
no control/operate), with the base cross-protocol brain tools mirrored onto it — plus
an edition skill (skills/iaiops-energy/SKILL.md, anti-drift-tested against the
registered tool surface) and protocol-consistency contract tests (every tool must
carry the governance marker, a [READ]-style risk tag, an Args: section, and the
canonical {error, hint} error shape; the server refuses to start if any registered
tool lacks the governance marker).
🧪 Beta testing & co-creation
Live substation RTU / IED / IEC-104 field testing is what this package needs most.
The IEC-104, DNP3 and IEC-61850 monitor paths are library-loopback-verified, and all three
now run on every CI build — a skip fails the build rather than passing it. (Since
2026-08-01: DNP3's evidence used to be a single manual run on 2026-07-02, because pydnp3
was believed unbuildable on hosted runners. That belief was wrong — see
scripts/build_pydnp3.sh.) Real RTU / IED hardware remains unverified. If you can run
iaiops doctor against real substation gear in an authorised test environment, we would
very much like to hear the result — verified devices are credited by name in the support
matrix. Report results (protocol + device model + iaiops doctor output) via the base
repo's pinned issue:
👉 industrial-aiops#28 — Call for field-testing partners (v0.10.0)
Why a separate repo
Energy targets a distinct buyer (utilities / substations), has heavier
platform-specific deps (pyiec61850 is a linux-only SWIG wheel; pydnp3 builds a
native ext), and its own compliance surface (China's Security Protection of Power
Monitoring Systems regime). Splitting keeps
the base install light. See the base repo's docs/ENERGY-SPINOUT.md for the plan.
Install
pip install iaiops-energy[energy] # all three energy protocols
pip install iaiops-energy[iec104] # just IEC-104
iaiops-energy pulls in iaiops (the shared core) automatically.
Use (MCP)
iaiops-energy-mcp # brain + energy tools over stdio
Point a target at your substation gear in ~/.iaiops/config.yaml
(protocol: iec104|dnp3|iec61850, host, port, common_address / unit_id).
Edge deployment & ecosystem (edge-native / Margo)
Like the base package, the energy edition rides on a hardened, centrally-managed edge host as a
portable, governed edge application — mapping onto the Margo
edge-interoperability roles (immutable host · compliant orchestrator · iaiops-energy = the
OT-domain app), deployable as an OCI Managed Container (outbound-only to substation RTUs/IEDs,
no inbound). A container + margo.org/v1-alpha1 application-description skeleton is in
deploy/margo/; the full alignment + honest gap analysis lives in the base repo's
docs/MARGO-ALIGNMENT.md.
The descriptor is validated against Margo's published margo.org/v1-alpha1 LinkML schema on every
PR (CI job margo-descriptor) and passes clean — structural validity only, see
deploy/margo/schema/PROVENANCE.md.
Honest status: a natural Margo edge application, but NOT Margo-compliant yet — image build, hosted+signed package, and a published conformance result are roadmap
⏳. No claim of compliance until that result exists, and the schema pass above is not a step toward it: the compliance test suite cannot be run today because it does not exist yet (no conformance repo in themargoorg; a first PR1 vertical slice was still being scoped as of 2026-01-15).
Validation status (honest)
The same honesty ladder as the base repo. Driver codec / API surface is verified
against the real libraries; the mock/monkeypatched unit tests run in CI without
hardware. See the base repo's docs/PREVIEW-VERIFICATION.md runbook for how a
protocol is promoted.
| Protocol | Status | CI coverage | Evidence |
|---|---|---|---|
| DNP3 / IEEE 1815 | verified (monitor path) | ✅ runs every push ¹ | Real master↔outstation round-trip against a live opendnp3 outstation (pydnp3): is_online() reflects the real channel OnStateChange, and integrity_poll() (Class 0/1/2/3) returns the seeded binary/analog/counter database grouped by type. See tests/test_dnp3_live.py (@pytest.mark.integration). No physical RTU. |
| IEC 60870-5-104 | verified (monitor path) | ✅ runs every push | Real client↔server round-trip against an in-process c104 server (tests/test_iec104_live.py + tests/iec104_server_harness.py, @pytest.mark.integration, passes in a Linux container): iec104_connection_info discovers the seeded station, iec104_interrogate (general interrogation / C_IC) returns the seeded M_ME_NC_1 + M_SP_NA_1 points with quality, iec104_read_point reads the measurand, a bad IOA yields found=False with no fabricated value, and server-side ASDU capture proves no control ASDU (C_SC / C_DC / C_SE) is ever issued. c104 ships no macOS wheel so the test skips on macOS (runs in CI / Linux). No physical RTU. Monitor/read only. |
| IEC 61850 (MMS) | verified (monitor path) | ✅ runs every push | Real client↔server MMS round-trip against an in-process libiec61850 MMS server built with pyiec61850's server API: iec61850_device_directory lists the logical device (and browses its logical nodes / data objects), and iec61850_read returns a seeded measurand (TotW.mag.f, FC MX) over real ISO-on-TCP; a bad reference surfaces an MMS data-access error instead of a fabricated value. See tests/test_iec61850_live.py (@pytest.mark.integration, skips when pyiec61850 / its server API is absent). No physical IED. Read/monitor only — control / GOOSE / SV out of scope. |
¹ All three monitor paths are now CI-gated, and a skip fails the build rather than
passing it (no live protocol test may skip). This used to read: "only IEC-104 is
CI-gated; DNP3 and IEC-61850 rest on out-of-CI runs, not on the green badge." That was
true and is no longer.
DNP3 was the last one, and the reason is worth recording because the old note here — and
the CI job, and this README — all repeated the same wrong conclusion for months:
pip install pydnp3 fails on any current Linux, so it was taken as unbuildable on
hosted runners. It is not. opendnp3 itself compiles clean; what had rotted was the 2019
binding layer. Three mechanical fixes, no patches to opendnp3:
- Python headers must be present (
python3-dev) — without them the build dies atPython.h: No such file or directory, which is what "unbuildable" looked like; - 214 vendored headers
#include <python2.7/Python.h>, rewritten to<Python.h>; - the vendored pybind11 predates CPython 3.11 (it reads
PyFrameObjectinternals that 3.11 made opaque) and GCC 13 (std::uint16_twithout<cstdint>) — replaced with pybind11 v2.13.6.
scripts/build_pydnp3.sh applies all three and verifies the import. ~3 min cold on two
cores. The lesson generalises: "the ecosystem says it cannot be built" is a claim to
test, not to inherit.
DNP3 notes: read-only / monitor direction only (no control). Its
DNP3Manager.Shutdown() can block in a long-lived interpreter, so the connector bounds
teardown (_Pydnp3MasterAdapter.shutdown) and the test drives the round-trip in a
short-lived child process.
License
MIT — © wei. Part of the vendor-neutral, governed Industrial-AIOps line.