eebus

Home / Docs / Use cases

Use cases

What an EEBUS use case is, how a device publishes the ones it plays, and how a peer discovers them before exchanging anything.

A use case is the application layer of EEBUS. It names the actors involved, the numbered scenarios they run, and the SPINE features and functions each scenario needs. Claiming to play one is a commitment: a peer will expect every scenario you advertise.

Publishing what you play

A device publishes its use cases in nodeManagementUseCaseData, one descriptor per actor:

use eebus::usecases::{lpc, UseCaseDescriptor};

lpc::CONTROLLABLE_SYSTEM;   // the appliance's side
lpc::ENERGY_GUARD;          // the controlling side

Each descriptor carries the use case name, its version, the actor, the scenarios supported and the entity types it is valid for — taken from the use-case technical specification's own tables and, where the 2026 implementation guides revise them, from those.

Discovering what a peer plays

match engine.poll_event() {
    Some(SpineEvent::UseCasesUpdated { device }) => { /* ask what it plays */ }
    _ => {}
}

The answer says which use cases the peer plays, in which actor role, and which scenarios of each. Only then does an application know whether the peer is a heat pump it may limit or a display that merely watches.

Who may write, and who may not

Whether a write needs a binding is a property of the feature, not a rule of the protocol (SPINE §7.3), and the use cases do not agree about their own:

bindsLPC/LPP, OPEV/OSCEV, COB, EVCS, OHPCF scenario 2 — "Actors that write parts of a Feature within this Scenario need to create a binding [...] Only one binding partner is allowed"
does notevery HVAC use case, including all six that write — "Binding SHOULD NOT be used for this Scenario"

So the feature constructors carry it, and the default is Required:

limitation::load_control_feature(1);   // WriteBinding::Required
cdt::setpoint_feature(1);              // WriteBinding::NotRequired — CDT §3.4.1.1

What replaces it on an HVAC feature is the application's own decision: those writes are deferred, so a product that wants "only the manager I was commissioned with" enforces it where it can see who is asking. See Bindings and subscriptions.

Who has to subscribe, and what silence means

Subscription, unlike binding, the use cases do agree about. Every scenario of every one of them says the same sentence — "Actors SHALL create a subscription for each server Feature that is relevant for the corresponding Actor within this Scenario" (§3.4.n.1) — and §3.3.4 names polling only as the fallback for a subscription that was refused. So the descriptor derives the list rather than each actor keeping its own:

mpc::MONITORING_APPLIANCE.features_needing_subscription();
// [ElectricalConnection, Measurement] — five scenarios, two features, each named once

tests/use_case_delivery.rs holds every actor to it: what an actor's attach or follow asks to subscribe to is exactly what its descriptor names. That includes the features easiest to leave out: the Monitoring Appliance's ElectricalConnection, where acMeasuredPhases says what a per-phase value means; the Energy Guard's DeviceConfiguration and ElectricalConnection, where the failsafe pair is writable at the appliance too and the contractual maximum is what a §14a agreement sets; and the EV guard's permittedValueSet, which changes mid-session when a car raises its minimum current.

A value that has not arrived for ten minutes is either a peer that stopped answering or one with nothing to say, and the answer decides whether to drop the unit or leave it where it is. Since everything is subscribed, what tells them apart is whether the notification comes on a clock, and the descriptors carry that as data:

use eebus::model::{FeatureType, Function};
use eebus::usecases::descriptor::Delivery;
use eebus::usecases::{lpc, mpc};

mpc::MONITORING_APPLIANCE.delivery_of(&FeatureType::Measurement, &Function::MeasurementListData);
// Some(Delivery::OnChange) — a room holding its temperature sends nothing, and is fine

lpc::CONTROLLABLE_SYSTEM.delivery_of(&FeatureType::DeviceDiagnosis, &Function::DeviceDiagnosisHeartbeatData);
// Some(Delivery::Periodic(60 s)) — [LPC-005]; silence past this arms the failsafe

The heartbeats are the only functions any of these specifications puts a clock on, and periodic_functions() lists them: 60 s for LPC, LPP and COB, and 4 s for OPEV and OSCEV, because a car follows a current at once. The period is the specification's "at least every", not a tolerance — how many missed beats to allow stays the consumer's, and LPC allows two where OPEV allows none. Everything else is OnChange, and its age is a fact about the world rather than about the peer.

Addressing a peer's data

Knowing which feature to talk to is only half of it. Almost every list in SPINE is keyed by an identifier the device chooses, and the specifications write those identifiers as placeholders: <l1#(1..1)> for a load-control limit, <k1#(1..1)> for a configuration key, <p1#(1..1)> for an electrical-connection parameter. "SHALL be used as the primary identifier" is a promise that the device keeps its own number stable. It is not a number a peer may assume.

What the specifications fix instead is what each entry describes — a key's keyName, a parameter's scopeType and phases, a limit's type and category and direction — and every such list has a description function beside it. Reading that description is the only way to address the peer rather than a coincidence:

use eebus::model::DeviceConfigurationKeyName;
use eebus::usecases::addressing::KeyIds;

let mut keys = KeyIds::new();
keys.learn(&description_payload);              // keyId ⇄ keyName

let failsafe = keys.get(&DeviceConfigurationKeyName::FailsafeDurationMinimum);
let what_is_it = keys.name_of(some_key_from_a_value_list);

Getting this wrong produces no error: the write is well-formed, names a real entry of the peer's, and is applied and acknowledged — with the value somewhere else, invisible to any test whose other end is this same crate.

Each use case wraps the resolver its own data needs — limitation::PeerIds, mgcp::Curtailment, charging::PhaseLimits, evcc::EvReader — and the actors do it for you. What is left for an application is to let the actor read the descriptions before expecting it to write anything, which is what EnergyGuardActor::is_ready reports.

What is implemented

Use caseActorsSpecification
LPC — Limitation of Power ConsumptionControllable System, Energy GuardUC TS 1.0.0 + IG 1.1.0
LPP — Limitation of Power ProductionControllable System, Energy GuardUC TS 1.0.0 + IG 1.1.0
MPC — Monitoring of Power ConsumptionMonitored Unit, Monitoring ApplianceUC TS 1.0.0
MGCP — Monitoring of Grid Connection PointMonitored Unit, Monitoring ApplianceUC TS 1.0.0
EVSECC — EVSE Commissioning and ConfigurationEVSE, CEMEVSECC 1.0.1
EVCC — EV Commissioning and ConfigurationEV, CEMEVCC 1.0.1
OPEV — Overload Protection by EV Charging Current CurtailmentEV, CEMOPEV 1.0.1b
OSCEV — Optimization of Self-Consumption During EV ChargingEV, CEMOSCEV 1.0.1b
EVCEM — EV Charging Electricity MeasurementEV, Energy GuardEVCEM 1.0.1
EVSOC — EV State of ChargeEV, Monitoring ApplianceEVSOC 1.0.0
EVCS — EV Charging SummaryEVSE, Energy Broker (or CEM)EVCS 1.0.1
MOI — Monitoring of InverterInverter, Monitoring ApplianceMOI 1.0.0
MPS — Monitoring of PV StringPVString, Monitoring ApplianceMPS 1.0.0
MOB — Monitoring of BatteryBattery, Monitoring ApplianceMOB 1.0.0
COB — Control of BatteryInverter, CEMCOB 1.0.0
MDSF — Monitoring of DHW System FunctionDHW Circuit, Monitoring ApplianceMDSF 1.0.0
CDSF — Configuration of DHW System FunctionDHW Circuit, Configuration ApplianceCDSF 1.0.0
MDT — Monitoring of DHW TemperatureDHW Circuit, Monitoring ApplianceMDT 1.0.0
CDT — Configuration of DHW TemperatureDHW Circuit, Configuration ApplianceCDT 1.0.0
MRHSF — Monitoring of Room Heating System FunctionHVAC Room, Monitoring ApplianceMRHSF 1.0.0
CRHSF — Configuration of Room Heating System FunctionHVAC Room, Configuration ApplianceCRHSF 1.0.0
MRCSF — Monitoring of Room Cooling System FunctionHVAC Room, Monitoring ApplianceMRCSF 1.0.0
CRCSF — Configuration of Room Cooling System FunctionHVAC Room, Configuration ApplianceCRCSF 1.0.0
MRT — Monitoring of Room TemperatureHVAC Room, Monitoring ApplianceMRT 1.0.0
CRHT — Configuration of Room Heating TemperatureHVAC Room, Configuration ApplianceCRHT 1.0.0
CRCT — Configuration of Room Cooling TemperatureHVAC Room, Configuration ApplianceCRCT 1.0.0
MOT — Monitoring of Outdoor TemperatureOutdoor Temperature Sensor, Monitoring ApplianceMOT 1.0.0
OHPCF — Optimization of Self-Consumption by Heat Pump Compressor FlexibilityCompressor, CEMOHPCF 1.0.0

The first four are the ones certifiable since July 2026. Every one of them is implemented on both sides — the appliance's and the manager's — which is where most of the implementation guides' pages actually go.

The twelve HVAC rows are the complete contents of the two HVAC specification bundles — EEBUS_HVAC_SystemFunction_UseCases_V1.0.0 and EEBUS_HVAC_Temperature_UseCases_V1.0.0. Nothing in either is left out.

Not implemented: CEVC (Coordinated EV Charging — see Not here: CEVC) and ITPCM (Incentive Table Based Power Consumption Management), which is the same shape: a price signal over incentiveTable that an appliance plans against, rather than an instruction it follows. Nothing in the eight-device corpus announces either; the SMA Home Manager 2 announces OHPCF, which is the direct-control answer to the same question and is implemented.

The four levers that can ask for more

The grid, e-mobility and storage use cases can only ask an appliance to do less (see Heat pumps). Four can ask for more, and they are not interchangeable:

  • CDT / CRHT / CRCT raise a setpoint and leave the appliance's own controller to decide what to do about it. A tank already at temperature will not run for a higher setpoint.
  • CDSF / CRHSF / CRCSF change the operation mode — eco while the grid is expensive, on while it is not. Slower, and available only where the appliance says isOperationModeIdChangeable.
  • CDSF scenario 2 starts a one-time hot water loading outright: the button in the bathroom, pressed over the wire; scenario 3 stops it. See Changing the mode, and pressing the button.
  • OHPCF starts a process: the compressor's optional power consumption, at a time the CEM names, with stop, pause and resume afterwards — what makes a heat pump pre-heat while the roof is exporting, which no limit can express. See The other lever: OHPCF.

Two use cases, one implementation

LPC and LPP are the same use case pointed in opposite directions: the same four scenarios, the same table numbers, the same thirteen state transitions, with requirement identifiers differing only in the prefix. So the state machine and both actors are written once and pointed by a Direction:

ControllableSystemActor::builder(system, lpc::DIRECTION, features).install(&mut engine, now)
ControllableSystemActor::builder(system, lpp::DIRECTION, features).install(&mut engine, now)

features is a CsFeatures with named fields rather than a row of FeatureAddress arguments, because nothing but the order would say which is which, and a device that answers a limit write on its heartbeat feature still compiles.

install publishes the limit description and locks the two features to one binding partner, and it is the only way to obtain an actor. Skipping it leaves a device that looks perfectly healthy — discovery answered, bindings and subscription granted, heartbeats flowing — whose empty limit list gives an Energy Guard no limitId to write to.

The same sharing runs elsewhere:

  • MPC and MGCP are the same measurements named from the appliance's side or the grid's.
  • OPEV and OSCEV are the same per-phase current ceiling for opposite reasons, pointed by a Purpose: obligation/overloadProtection against recommendation/selfConsumption. Two words, and a car that confuses them charges on solar only.
  • EVCEM, EVSOC, MOI, MPS, MOB, COB, MDT, MRT and MOT are built on the same monitoring machinery as MPC and MGCP — one implementation of "describe a measurement twice and read it back", serving eleven use cases. What each adds is vocabulary, not mechanism.
  • The twelve HVAC use cases are three exchanges. hvac::system_function is the operation-mode one and serves six — MDSF/CDSF, MRHSF/CRHSF, MRCSF/CRCSF — told apart by systemFunctionType (dhw, heating, cooling) and by whether the actor may write. hvac::setpoint is the temperature-setting one and serves three — CDT, CRHT, CRCT. hvac::temperature is the measurement one and serves three — MDT, MRT, MOT. Twelve modules of constants and descriptors sit on top; the behaviour is written once.
  • MDT, MRT and MOT share a locate of their own for the one thing they do not share with MPC: a tank, a room and the outdoors have no ElectricalConnection, so searching for one would find whatever else the device happens to serve.
  • The other nine share hvac::peer. One locate, one locate_all and one follow, told apart by the use-case descriptor each passes in — so what follow reads is the specification's own scenario tables rather than a second list kept beside them. See Heat pumps.
  • And one actor for all nine. HvacApplianceActor serves both client actors of the family — the Monitoring Appliance reads, the Configuration Appliance also writes — keyed by UnitId, device and entity. That is the same key MonitoringApplianceActor uses, because a room's thermometer and its setpoint are the same entity.

The reference implementations duplicate each pair; here a fix to one is a fix to both, and the only thing the direction changes is what the tests assert.