eebus

Home / Docs / LPC and LPP

LPC and LPP

Limitation of Power Consumption and Production: the §14a EnWG use case, its four scenarios, the failsafe state machine, and both actors including the Energy Guard's write rules.

Limitation of Power Consumption is how a grid operator keeps a congested low-voltage grid intact without disconnecting anyone: an Energy Guard tells a Controllable System how much power it may draw, and the system honours it. Limitation of Power Production is the same thing for feed-in. Together they are the technical basis for §14a EnWG and §9 EEG.

The exchange, end to end

Everything between a fresh connection and an acknowledged limit. The pre-scenario half is where most integrations go wrong: the binding grants the right to write, the subscription is what makes the heartbeat arrive, and §2.11 refuses to evaluate a limit that has no recent heartbeat behind it. The heartbeat that counts is the one from the Energy Guard entity that holds both bindings — the feature the Controllable System subscribed to under §3.8 — not one any peer on the network happens to notify.

sequenceDiagram
  autonumber
  participant EG as Energy Guard<br>(control box)
  participant CS as Controllable System<br>(heat pump)

  rect rgb(230, 245, 239)
    Note over EG,CS: pre-scenario communication (§3.3)
    EG->>CS: read nodeManagementDetailedDiscoveryData
    CS-->>EG: entities, features, roles
    EG->>CS: read nodeManagementUseCaseData
    CS-->>EG: LPC · ControllableSystem · scenarios 1–4
    EG->>CS: bindingRequest LoadControl
    EG->>CS: bindingRequest DeviceConfiguration
    CS-->>EG: granted
    EG->>CS: read loadControlLimitDescriptionListData
    CS-->>EG: limitId 7 · obligation · consume
    EG->>CS: read deviceConfigurationKeyValueDescriptionListData
    CS-->>EG: failsafe keys 5 and 6
    EG->>CS: subscriptionRequest ×4 · LoadControl · DeviceConfiguration<br>DeviceDiagnosis · ElectricalConnection
    CS-->>EG: granted
    CS->>EG: subscriptionRequest DeviceDiagnosis
    EG-->>CS: granted
  end

  EG->>CS: notify deviceDiagnosisHeartbeatData
  Note right of CS: §2.11 — a limit is evaluated only<br>with a heartbeat inside 60 s
  EG->>CS: write loadControlLimitListData limitId 7 · 4200 W
  Note left of CS: the appliance decides:<br>can it follow this?
  CS-->>EG: result errorNumber 0
  Note over EG,CS: under §14a EnWG that acknowledgement<br>*is* the record the operator must produce

The four scenarios

  1. Control the active power limit — the limit itself, with an optional duration, acknowledged or refused.
  2. Failsafe values — what applies when the Energy Guard falls silent, and for how long at minimum.
  3. Heartbeat — a message every sixty seconds in each direction; its absence is what triggers the failsafe.
  4. Constraints — the nominal maxima, so the Energy Guard knows what it is limiting.

The state machine

A Controllable System is in exactly one of three states, and the transitions between them are the use case:

stateDiagram-v2
  direction LR
  [*] --> init

  init: init
  note right of init
    A restart begins limited by the
    failsafe value, not unlimited.
  end note

  unlctrl: unlimited/controlled
  limited: limited
  failsafe: failsafe state
  unlauto: unlimited/autonomous

  init --> limited: limit written, heartbeat recent
  init --> unlctrl: limit refused, or deactivated
  init --> unlauto: nothing arrives within 120 s

  unlctrl --> limited: an active limit
  limited --> unlctrl: deactivated, or the duration runs out
  limited --> failsafe: no heartbeat for 120 s
  unlctrl --> failsafe: no heartbeat for 120 s
  failsafe --> limited: heartbeat, then an active limit
  failsafe --> unlctrl: heartbeat, then a refusal
  failsafe --> unlauto: the failsafe duration elapses
  unlauto --> limited: heartbeat, then an active limit
  unlauto --> unlctrl: heartbeat, then a refusal

The two unlimited states are not the same thing. unlimited/controlled means an Energy Guard is present and is currently asking for nothing — a refusal reaches it too, because a refusal proves a controller is there. unlimited/autonomous means nobody is in control and the device is running its own logic, which it may only do once the failsafe duration has elapsed.

The failsafe is mandatory. §14a fixes 4.2 kW as the floor an installation must keep available, and the failsafe duration has a minimum so that a brief network hiccup does not leave a house heating at full power for hours.

use core::time::Duration;
use eebus::usecases::limitation::{ControllableSystem, CsConfig};

// A heat pump that falls back to the 4.2 kW §14a leaves it, for at least two hours.
let system = ControllableSystem::new(
    CsConfig::new(4_200.0, Duration::from_secs(2 * 3_600)).with_nominal_max(11_000.0),
    Duration::ZERO,
);

The application decides

A limit write is answered by the use case, not by a generic setter. Under §14a the acknowledgement is the record that the limit was applied, so it has to say what the appliance actually did — and only the appliance knows that.

match engine.poll_event() {
    Some(SpineEvent::WriteRequested(w)) => {
        let write = limitation::read_limit_write(&w.resolved)?;   // merged, not the fragment
        match system.on_limit_write(&write, decide(&write), now) {
            WriteOutcome::Accepted => engine.accept_write(w.token, now),           // ACK
            outcome => engine.reject_write(w.token, outcome.error_number(), now),  // NACK
        }
    }
    _ => {}
}

The other half: the Energy Guard

The 2026 implementation guides spend most of their pages on the controlling side. Here its rules live behind one call:

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

and nothing before it: the guard takes up a peer that announces the Controllable System by itself. require does not depend on ordering either — a limit asked for before the peer is reachable is held, not discarded.

Behind that line:

  • the peer's own identifiers, read before anything is written. limitId is <l1#(1..1)> in Table 22 and the two failsafe keyIds are <k1#(1..1)> and <k2#(1..1)> in Table 24: the numbers belong to the device, and what the specification fixes is the limit's type, category, direction and scope, and the keys' names. A guard that assumed its own numbering would write a consumption limit into the production limit of an appliance that serves both — and be acknowledged. Nothing is written until the descriptions come back, which is what is_ready means and what peer_ids reports;
  • a heartbeat immediately before the write, and only once the peer has subscribed to it (§2.11) — and the heartbeat that counts is one this peer could have received, not merely one this guard sent, because a notification reaches subscribers and nobody else;
  • an opening write on the limit as soon as the bindings settle, whether or not the grid is asking for anything, because until it lands the Controllable System is not in a controllable state at all (§2.11);
  • never a deactivation as the first limit after a reconnection (§2.13);
  • never a zero duration on an activated limit (§2.2);
  • a refusal retried a minute later, and a minute later again, with the device kept (§2.5);
  • and no more than one write every five minutes otherwise (§2.10) — the opening write excepted, since a limit that follows it must not wait five minutes behind a deactivation.

The duration is a relative time, and an unreadable one is refused

§3.1.8.2: "Durations used within this Use Case SHALL be presented as relative times. The same holds for the endTime Element used for the duration of validity ([LPC-004])." The schema is looser than that — timePeriod.endTime is the AbsoluteOrRelativeTimeType union, so 2026-09-05T10:00:00Z is schema-valid and use-case-invalid.

read_limit_write refuses an endTime it cannot read as a relative duration, and the refusal reaches the guard as a NACK; reading it as absent would mean no expiry. The same rule covers a scaledNumber whose scale overflows, and COB applies it to its own setpoint duration, whose §3.1.8.2 says the same thing. See Where a use case is narrower than the schema.

A peer whose LoadControl feature describes no limit for this guard's direction reports GuardEvent::NoLimitPublished and is then left alone. That installation looks commissioned from both ends and is not — usually a device that implements the other direction, or an actor that was built and never installed — and because the read itself succeeds, this event is the only place it is visible.

Scenario 4: what the Energy Guard is limiting

An operator that works in percentages has nothing to multiply until the Controllable System says what it is. Scenario 4 is that, and the two values are mutually exclusive:

  • Power Consumption Nominal Max ([LPC-041]) — the nameplate, what the appliance can physically draw. UC TS §2.6.4.1: for a device, and not for an energy manager.
  • Contractual Consumption Nominal Max ([LPC-042]) — what the customer's contract allows. For an energy manager, and not for a device.

Which one is published follows from the configuration rather than from a separate call, so the pair cannot drift:

let device = ControllableSystem::new(
    CsConfig::new(4_200.0, failsafe_duration).with_nominal_max(11_000.0),
    now,
);
let manager = ControllableSystem::new(
    CsConfig::new(4_200.0, failsafe_duration).with_contractual_max(30_000.0).on_cem(),
    now,
);

An Energy Guard reads it in the pre-scenario exchange and hears about it as GuardEvent::ConstraintsLearned. LPP names the same two values powerProductionNominalMax and contractualProductionNominalMax.

The §14a record

Every accepted or refused limit is recorded with its timestamp, its value and the outcome. That log is the evidence a grid operator's limit was honoured, which is what the regulation asks for.

guard.audit().records();   // what was asked, what was answered, when