eebus

SHIP · SPINE · certifiable grid use cases

EEBUS, in Rust,
down to the socket.

An unofficial EEBUS implementation in Rust: the SHIP transport, the SPINE information model, and twenty-eight use cases on top — built to pass certification, not merely to interoperate.

Under construction. The stack is complete from the socket to the use case, and the crate is on crates.io — but it is pre-1.0, and the API will change.

energy_guard.rs
// A heat pump answers the grid operator's limit itself —
// under §14a EnWG the acknowledgement *is* the record.
if let Some(SpineEvent::WriteRequested(w)) = engine.poll_event() {
    let write = limitation::read_limit_write(&w.resolved, LIMIT_ID)?;
    let outcome = system.on_limit_write(&write, decide(&write), now);
    if outcome.is_accepted() {
        engine.accept_write(w.token, now)?;                         // ACK
    } else {
        engine.reject_write(w.token, outcome.error_number(), now);  // NACK
    }
}
830SPINE types, generated
28use cases, both actors
no_stdbelow the runtime
TLS 1.2mutual auth, SKI-pinned

Why this one

Built to pass certification, not merely to interoperate.

Sans-IO protocol core

The SHIP handshake and the SPINE engine touch neither sockets nor clocks. Time is a parameter, so the two-minute hello, the SPINE response deadline and LPC's two-hour failsafe are ordinary unit tests against a virtual clock.

Certifiable, not just interoperable

The 203 abstract test cases of the four High-Level Test Specifications are carried as data and every one is driven; cargo test prints 189/203. The fourteen missing are exactly the device-level cases — a factory reset, a power cut — each listed with its reason, and shipped as a harness (eebus::conformance::harness) to run against your own binary.

Illegal states are unrepresentable

A command carries a payload choice, so two payloads in one command cannot be built. A LoadControlLimitId cannot be passed where a MeasurementId belongs — a mix-up that matters, because LPC requires the two to match.

One implementation per pair

LPC and LPP are the same use case pointed in opposite directions, as are OPEV and OSCEV. Eleven use cases share one measurement layer; the twelve HVAC ones are three exchanges. Each is written once, so a fix to one is a fix to all.

Runs on the controller

Everything below the runtime builds for no_std + alloc, checked in CI against thumbv7em-none-eabihf. Five fuzz targets cover everything the network can reach, because on a heat-pump controller a panic is a reboot.

Checked against other implementations

Fifteen datagrams captured from eight real devices are driven through an engine, resolved into a device model, and used to seed the fuzzers. An opt-in suite runs eebus-go's own examples in a container and drives the whole §14a exchange against them — this crate dialling, and the peer dialling in.

The wire format, directly

EEBUS JSON-UTF8 is encoded through serde in a single streaming pass — no intermediate tree to rewrite, no allocation for field names. A schema-valid message round-trips byte for byte.

The exchange, end to end

A distribution grid operator's control box tells a heat pump how much power it may draw. Under §14a EnWG the building has to honour that limit, and the acknowledgement it sends back is the record that it did. Every layer below is in this crate.

flowchart LR
  mdns["mDNS-SD<br><small>find it</small>"]
  tcp["TCP<br><small>connect</small>"]
  tls["TLS 1.2<br><small>mutual auth</small>"]
  ws["WebSocket<br><small>framing</small>"]
  ship["SHIP<br><small>handshake, trust</small>"]
  spine["SPINE<br><small>model, bindings</small>"]
  uc["use case<br><small>the decision</small>"]

  mdns --> tcp --> tls --> ws --> ship --> spine --> uc

  classDef s fill:#e6f5ef,stroke:#0b8f63,color:#101620;
  classDef d fill:#0b8f63,stroke:#0b8f63,color:#fff;
  class mdns,tcp,tls,ws,ship,spine s
  class uc d

Who it is for

You are buildingYou needStart at
A heat pump, wallbox, battery or inverter that must accept a §14a limitthe Controllable System actors, the tester hooks, no_stdGetting started, then LPC and LPP
An energy manager or grid control box that issues limitsthe Energy Guard and Monitoring Appliance actors, Hub with its listener, browse and pairingOn a network
A gateway, simulator or test harnessthe sans-IO core, without socketsArchitecture

Both sides, not just the appliance

A heat pump needs the Controllable System. An energy manager needs the Energy Guard, and the 2026 implementation guides spend most of their pages on that half. Both are here, and the guard's rules — heartbeat before write, no deactivation as the first limit after a reconnection, a refusal retried a minute later, one write per five minutes otherwise — live behind a single call.

guard.require(&device, Some(LimitWrite::active(4_200.0)), now);

Generated from the schemas, not from a reading of them

830 SPINE types and 47 SHIP messages are emitted from the XML Schemas the specifications ship, so the model cannot drift from the standard. The generated Rust is committed: building the crate never needs the specifications.

Checked against implementations it did not write

Two checks go beyond round trips over self-written fixtures:

Recorded traffic. Fifteen datagrams captured from eight real devices — Elli, evcc, Kostal, Porsche, SMA, Spelsberg, Vaillant, Viessmann — are driven through an engine, resolved into a device model, and used to seed the fuzzers, so the corpus explores the shapes real hardware produces.

A live peer. An opt-in suite runs eebus-go's own examples in a container at a pinned revision and drives the whole §14a exchange against them three ways: their control box against this crate's Controllable System, their EVSE against its Energy Guard, and their control box dialling in to a listener of ours.

One rule you cannot get wrong twice

Restricted Function Exchange merges partial updates element by element: an omitted element means unchanged, all the way down. Send a scaledNumber carrying a bare number and the stored scale has to survive — otherwise a 4.2 kW limit silently becomes 42 MW. The generator resolves the type-to-selector link the schemas leave implicit into a table over 141 functions, and the merge is written once.

Start with the exchange itself

A grid operator's control box limits a heat pump, first against a virtual clock and then over a real socket. Both are runnable examples in the repository.