Metadata-Version: 2.5
Name: iaiops-energy
Version: 0.2.1
Summary: Energy edition (变电/电力) for Industrial-AIOps — IEC-104 / DNP3 / IEC-61850 read-only connectors on top of iaiops.core.
Project-URL: Homepage, https://github.com/industrial-aiops/industrial-aiops-energy
Project-URL: Base package, https://github.com/industrial-aiops/industrial-aiops
Author-email: wei <zhouwei008@gmail.com>
License-Expression: MIT
License-File: LICENSE
Keywords: AIOps,DNP3,IEC-104,IEC-61850,OT,SCADA,edge,edge-computing,edge-interoperability,energy,iiot,margo,substation
Requires-Python: >=3.11
Requires-Dist: iaiops<1.0,>=0.29.0
Provides-Extra: dev
Requires-Dist: bandit>=1.7; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Provides-Extra: dnp3
Requires-Dist: pydnp3<1,>=0.1; extra == 'dnp3'
Provides-Extra: energy
Requires-Dist: c104<3,>=2.0; extra == 'energy'
Requires-Dist: pydnp3<1,>=0.1; extra == 'energy'
Requires-Dist: pyiec61850<2,>=1.5.2a1; (platform_system == 'Linux') and extra == 'energy'
Provides-Extra: iec104
Requires-Dist: c104<3,>=2.0; extra == 'iec104'
Provides-Extra: iec61850
Requires-Dist: pyiec61850<2,>=1.5.2a1; (platform_system == 'Linux') and extra == 'iec61850'
Description-Content-Type: text/markdown

<!-- mcp-name: io.github.industrial-aiops/iaiops-energy -->

# industrial-aiops-energy — Energy Edition (Substation / Utility)

**English** · [中文](README.zh-CN.md)

The **energy edition** of [Industrial-AIOps](https://github.com/industrial-aiops/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.

Local compatibility with `iaiops` 0.34.0 was checked on 2026-11-14: 99 tests passed;
three native protocol tests skipped on macOS. See [verification details](docs/VERIFICATION.md).

## MCP directory overview

Industrial-AIOps Energy helps agents inspect substation and utility telemetry
through IEC 60870-5-104, DNP3 and IEC 61850 MMS. Example tasks include inspecting
point values and quality, comparing observations across configured endpoints,
and feeding normalized observations into the shared diagnostic engine.

The energy connectors expose monitoring operations, with no control-direction
writes. MCP calls use the base audit and governance engine. This server uses
stdio; native-library installation requirements and protocol verification differ
by platform. These capabilities do not imply certification or validation against
every IED or RTU. See [Glama listing text](docs/GLAMA.md).

Current release: **0.2.1** (requires `iaiops>=0.29.0,<1.0`; 0.2.0 release tested against `iaiops` 0.31.0; 0.2.1 compatibility checked against 0.34.0). On the wire: **80 tools, 75 read-only, 0 destructive** (the energy connectors plus the base brain tools mounted on this server). New in 0.2.0 — every MCP call on
this server is audited with **which front-end and which client made it** (`origin=mcp`, the
client's self-reported name, one id per connection); before this, calls here reached the trail
as origin *unknown*. **Upgrade note:** `iaiops` 0.29 hashes those fields into the audit chain,
and an older base verifying a trail 0.29 has written to reports `row_hash mismatch` — so
upgrade every process sharing one `IAIOPS_HOME` (this server, `iaiops-mcp`, the CLI,
enterprise) together. The base's new HTTP bearer-token requirement does not apply here: this
server speaks stdio only. See `CHANGELOG.md`. Previously in 0.1.12 — the package
reports **its own** version: `__version__` had been hard-coded at 0.1.3 for eight releases, and
the MCP handshake reported the SDK's version as this server's; both now come from the installed
package metadata. Previously 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 was (in 0.1.9) 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)](https://github.com/industrial-aiops/industrial-aiops/issues/28)

## 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

```bash
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)

```bash
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](https://margo.org/)
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/`](deploy/margo/); the full alignment + honest gap analysis lives in the base repo's
[`docs/MARGO-ALIGNMENT.md`](https://github.com/industrial-aiops/industrial-aiops/blob/main/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`](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 the `margo`
> org; 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:

1. Python headers must be present (`python3-dev`) — without them the build dies at
   `Python.h: No such file or directory`, which is what "unbuildable" looked like;
2. 214 vendored headers `#include <python2.7/Python.h>`, rewritten to `<Python.h>`;
3. the vendored pybind11 predates CPython 3.11 (it reads `PyFrameObject` internals that
   3.11 made opaque) and GCC 13 (`std::uint16_t` without `<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.
