Skip to main content

energy_api/models/
electricity.rs

1//! Wire-format types for the EDI-Energy electricity market APIs.
2//!
3//! Covers three API families:
4//! - **Control Measures** (`controlMeasuresV1.yaml`) — Steuerungshandlungen
5//!   between NB/LF and MSB.
6//! - **MaLo Identification** (`maloIdentV1.yaml`) — MaLo-ID retrieval for the
7//!   24 h supplier-switch process (GPKE part 2).
8//! - **WiM Order** (`wimOrderV1.yaml`) — iMS Universalbestellprozess for smart
9//!   meter commissioning (PIDs 11021–11023).
10
11use serde::{Deserialize, Serialize};
12use uuid::Uuid;
13
14// ── Shared identifiers ────────────────────────────────────────────────────────
15
16/// External transaction ID (UUID RFC 4122), chosen by the sender.
17pub type TransactionId = Uuid;
18/// Idempotency key for retries (UUID RFC 4122).
19pub type InitialTransactionId = Uuid;
20/// External reference correlating a response to a prior request (UUID RFC 4122).
21pub type ReferenceId = Uuid;
22/// 13-digit market partner identifier.
23pub type MarketPartnerId = i64;
24
25/// Network location identifier — pattern `E[A-Z0-9]{9}[0-9]`.
26#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
27pub struct NeloId(pub String);
28
29/// Controllable resource identifier — pattern `C[A-Z0-9]{9}[0-9]`.
30#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
31pub struct SrId(pub String);
32
33/// Either a network location ID or a controllable resource ID.
34#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
35#[serde(untagged)]
36pub enum LocationId {
37    NetworkLocation(NeloId),
38    ControllableResource(SrId),
39}
40
41impl std::fmt::Display for NeloId {
42    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43        f.write_str(&self.0)
44    }
45}
46impl std::fmt::Display for SrId {
47    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48        f.write_str(&self.0)
49    }
50}
51impl std::fmt::Display for LocationId {
52    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53        match self {
54            LocationId::NetworkLocation(id) => id.fmt(f),
55            LocationId::ControllableResource(id) => id.fmt(f),
56        }
57    }
58}
59
60// ── Control Measures ──────────────────────────────────────────────────────────
61
62/// Maximum power value in kW (`"\d{0,6}(\.\d{1,3})?"`).
63#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
64pub struct MaximumPowerValue(pub String);
65
66/// Regulate a location to a specific maximum power value.
67#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
68#[serde(rename_all = "camelCase")]
69pub struct CommandControl {
70    pub maximum_power_value: MaximumPowerValue,
71    /// Start of effect period — ISO 8601 UTC, second precision (e.g. `"2023-08-01T12:30:00Z"`).
72    pub execution_time_from: String,
73    /// Optional end of effect period.
74    #[serde(skip_serializing_if = "Option::is_none")]
75    pub execution_time_until: Option<String>,
76}
77
78/// Reset a location to its initial / uncontrolled state.
79#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
80#[serde(rename_all = "camelCase")]
81pub struct CommandRegular {
82    /// Start of effect period — ISO 8601 UTC, second precision.
83    pub execution_time_from: String,
84}
85
86/// Reason for a negative response from the MSB.
87#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
88pub enum ReasonNegative {
89    /// Communication to the control box was disrupted.
90    #[serde(rename = "communicationFailure")]
91    CommunicationFailure,
92    /// MSB back-end is overloaded.
93    #[serde(rename = "overload")]
94    Overload,
95    /// MSB is procedurally unable to fulfil the request.
96    #[serde(rename = "unable")]
97    Unable,
98}
99
100/// Terminal state for negative (failure) responses.
101#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
102pub enum StateNegative {
103    #[serde(rename = "failed")]
104    Failed,
105}
106
107/// Terminal state for positive (success) responses.
108#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
109pub enum StatePositive {
110    #[serde(rename = "succeeded")]
111    Succeeded,
112}
113
114/// Preliminary state — command is executable in principle.
115#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
116pub enum PreliminaryStatePositive {
117    #[serde(rename = "possible")]
118    Possible,
119}
120
121/// State indicating the final outcome is not yet known.
122#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
123pub enum StateUnknown {
124    #[serde(rename = "unknown")]
125    Unknown,
126}
127
128// ── MaLo Identification ───────────────────────────────────────────────────────
129
130/// Market location identifier — 11-digit string.
131#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
132pub struct MaloId(pub String);
133
134/// Metering location identifier — pattern `DE\d{11}[A-Z,\d]{20}`.
135#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
136pub struct MeloId(pub String);
137
138/// Technical resource identifier — pattern `D[A-Z0-9]{9}[0-9]`.
139#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
140pub struct TrId(pub String);
141
142/// Energy flow direction at a market location.
143#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
144pub enum EnergyDirection {
145    #[serde(rename = "consumption")]
146    Consumption,
147    #[serde(rename = "production")]
148    Production,
149}
150
151/// Metering technology classification of a market location.
152#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
153pub enum MeasurementTechnologyClassification {
154    #[serde(rename = "intelligentMeasuringSystem")]
155    IntelligentMeasuringSystem,
156    #[serde(rename = "conventionalMeasuringSystem")]
157    ConventionalMeasuringSystem,
158    #[serde(rename = "noMeasurement")]
159    NoMeasurement,
160}
161
162/// Whether the forecast basis may be changed.
163#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
164pub enum OptionalChangeForecastBasis {
165    #[serde(rename = "possible")]
166    Possible,
167    #[serde(rename = "notPossible")]
168    NotPossible,
169}
170
171/// Lifecycle property / category of a market location.
172#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
173pub enum MarketLocationProperty {
174    #[serde(rename = "customerFacility")]
175    CustomerFacility,
176    /// Dormant market location (spec spelling: `"nonActice"`).
177    #[serde(rename = "nonActice")]
178    NonActive,
179    #[serde(rename = "standard")]
180    Standard,
181}
182
183/// Tranche proportion type.
184#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
185pub enum ProportionType {
186    #[serde(rename = "bilateralAgreement")]
187    BilateralAgreement,
188    #[serde(rename = "percent")]
189    Percent,
190}
191
192/// Input parameters for a MaLo identification request.
193#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
194#[serde(rename_all = "camelCase")]
195pub struct IdentificationParameter {
196    /// Effective date for identification — ISO 8601 UTC, day-boundary midnight.
197    pub identification_date_time: String,
198    pub energy_direction: EnergyDirection,
199    /// Optional ID-based search criteria.
200    #[serde(skip_serializing_if = "Option::is_none")]
201    pub identification_parameter_id: Option<IdentificationParameterId>,
202    /// Address-based search criteria.
203    pub identification_parameter_address: IdentificationParameterAddress,
204}
205
206/// Optional ID-based identification parameters.
207#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
208#[serde(rename_all = "camelCase")]
209pub struct IdentificationParameterId {
210    #[serde(skip_serializing_if = "Option::is_none")]
211    pub malo_id: Option<MaloId>,
212    #[serde(skip_serializing_if = "Option::is_none")]
213    pub tranchen_ids: Option<Vec<String>>,
214    #[serde(skip_serializing_if = "Option::is_none")]
215    pub melo_ids: Option<Vec<MeloId>>,
216    #[serde(skip_serializing_if = "Option::is_none")]
217    pub meter_numbers: Option<Vec<String>>,
218    #[serde(skip_serializing_if = "Option::is_none")]
219    pub customer_number: Option<String>,
220}
221
222/// Address-based identification parameters.
223#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
224#[serde(rename_all = "camelCase")]
225pub struct IdentificationParameterAddress {
226    #[serde(skip_serializing_if = "Option::is_none")]
227    pub name: Option<PersonName>,
228    #[serde(skip_serializing_if = "Option::is_none")]
229    pub address: Option<PostalAddress>,
230}
231
232/// Person or company name.
233#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
234#[serde(rename_all = "camelCase")]
235pub struct PersonName {
236    #[serde(skip_serializing_if = "Option::is_none")]
237    pub surnames: Option<String>,
238    #[serde(skip_serializing_if = "Option::is_none")]
239    pub firstnames: Option<String>,
240    #[serde(skip_serializing_if = "Option::is_none")]
241    pub title: Option<String>,
242    #[serde(skip_serializing_if = "Option::is_none")]
243    pub company: Option<String>,
244}
245
246/// German postal address.
247#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
248#[serde(rename_all = "camelCase")]
249pub struct PostalAddress {
250    #[serde(skip_serializing_if = "Option::is_none")]
251    pub country_code: Option<String>,
252    #[serde(skip_serializing_if = "Option::is_none")]
253    pub zip_code: Option<String>,
254    #[serde(skip_serializing_if = "Option::is_none")]
255    pub city: Option<String>,
256    #[serde(skip_serializing_if = "Option::is_none")]
257    pub street: Option<String>,
258    #[serde(skip_serializing_if = "Option::is_none")]
259    pub house_number: Option<i32>,
260    #[serde(skip_serializing_if = "Option::is_none")]
261    pub house_number_addition: Option<String>,
262}
263
264// ── MaLo Identification response types ───────────────────────────────────────
265
266/// Positive identification result — all data the NB holds about the market
267/// location from `identificationDateTime` onwards.
268#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
269#[serde(rename_all = "camelCase")]
270pub struct MaloIdentResultPositive {
271    pub data_market_location: DataMarketLocation,
272    #[serde(skip_serializing_if = "Option::is_none")]
273    pub data_tranches: Option<Vec<DataTranche>>,
274    #[serde(skip_serializing_if = "Option::is_none")]
275    pub data_meter_locations: Option<Vec<DataMeterLocation>>,
276    #[serde(skip_serializing_if = "Option::is_none")]
277    pub data_technical_resources: Option<Vec<DataTechnicalResource>>,
278    #[serde(skip_serializing_if = "Option::is_none")]
279    pub data_controllable_resources: Option<Vec<DataControllableResource>>,
280    #[serde(skip_serializing_if = "Option::is_none")]
281    pub data_network_locations: Option<Vec<DataNetworkLocation>>,
282}
283
284/// Negative identification result, referencing the applicable decision tree.
285#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
286#[serde(rename_all = "camelCase")]
287pub struct MaloIdentResultNegative {
288    /// Decision tree code from EDI@energy, e.g. `"E_0594"`.
289    pub decision_tree: String,
290    /// Response code from that tree, e.g. `"A10"`.
291    pub response_code: String,
292    #[serde(skip_serializing_if = "Option::is_none")]
293    pub reason: Option<String>,
294    /// NB that now holds the location (when it left this NB's grid area).
295    #[serde(skip_serializing_if = "Option::is_none")]
296    pub network_operator: Option<MarketPartnerId>,
297}
298
299/// Full data about the identified market location.
300#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
301#[serde(rename_all = "camelCase")]
302pub struct DataMarketLocation {
303    pub malo_id: MaloId,
304    pub energy_direction: EnergyDirection,
305    pub measurement_technology_classification: MeasurementTechnologyClassification,
306    pub optional_change_forecast_basis: OptionalChangeForecastBasis,
307    pub data_market_location_properties: Vec<MarketLocationProperties>,
308    pub data_market_location_network_operators: Vec<TimeSlicedMarketPartner>,
309    pub data_market_location_transmission_system_operators: Vec<TimeSlicedMarketPartner>,
310    #[serde(skip_serializing_if = "Option::is_none")]
311    pub data_market_location_measuring_point_operators: Option<Vec<TimeSlicedMarketPartner>>,
312    #[serde(skip_serializing_if = "Option::is_none")]
313    pub data_market_location_suppliers: Option<Vec<TimeSlicedMarketPartner>>,
314    #[serde(skip_serializing_if = "Option::is_none")]
315    pub data_market_location_name: Option<PersonName>,
316    #[serde(skip_serializing_if = "Option::is_none")]
317    pub data_market_location_address: Option<PostalAddress>,
318}
319
320/// A market partner assignment valid for a specific time slice.
321#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
322#[serde(rename_all = "camelCase")]
323pub struct TimeSlicedMarketPartner {
324    pub market_partner_id: MarketPartnerId,
325    pub execution_time_from: String,
326    #[serde(skip_serializing_if = "Option::is_none")]
327    pub execution_time_until: Option<String>,
328}
329
330/// Property of a market location valid for a specific time slice.
331#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
332#[serde(rename_all = "camelCase")]
333pub struct MarketLocationProperties {
334    pub market_location_property: MarketLocationProperty,
335    pub execution_time_from: String,
336    #[serde(skip_serializing_if = "Option::is_none")]
337    pub execution_time_until: Option<String>,
338}
339
340/// Data about a metering location (Messlokation) at the market location.
341#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
342#[serde(rename_all = "camelCase")]
343pub struct DataMeterLocation {
344    pub melo_id: MeloId,
345    pub meter_number: String,
346    pub data_meter_location_measuring_point_operators: Vec<TimeSlicedMarketPartner>,
347}
348
349/// Data about a technical resource (Technische Ressource) at the market location.
350#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
351#[serde(rename_all = "camelCase")]
352pub struct DataTechnicalResource {
353    pub tr_id: TrId,
354}
355
356/// Data about a controllable resource (Steuerbare Ressource) at the market location.
357#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
358#[serde(rename_all = "camelCase")]
359pub struct DataControllableResource {
360    pub sr_id: SrId,
361    #[serde(skip_serializing_if = "Option::is_none")]
362    pub data_controllable_resource_measuring_point_operators: Option<Vec<SrMarketPartner>>,
363}
364
365/// Market partner assignment at a controllable resource.
366#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
367#[serde(rename_all = "camelCase")]
368pub struct SrMarketPartner {
369    pub market_partner_id: MarketPartnerId,
370    pub execution_time_from: String,
371    #[serde(skip_serializing_if = "Option::is_none")]
372    pub execution_time_until: Option<String>,
373    pub market_partner_type_sr: String,
374}
375
376/// Data about a network location (Netzlokation) linked to the market location.
377#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
378#[serde(rename_all = "camelCase")]
379pub struct DataNetworkLocation {
380    pub nelo_id: NeloId,
381    pub data_network_location_measuring_point_operators: Vec<TimeSlicedMarketPartner>,
382}
383
384/// A billing tranche at a market location.
385#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
386#[serde(rename_all = "camelCase")]
387pub struct DataTranche {
388    pub tranchen_id: String,
389    pub proportion: ProportionType,
390    #[serde(skip_serializing_if = "Option::is_none")]
391    pub percent: Option<f64>,
392    pub data_tranche_suppliers: Vec<TimeSlicedMarketPartner>,
393}
394
395// ── WiM Order (iMS Universalbestellprozess) ───────────────────────────────────
396
397/// Device category for the iMS Universalbestellprozess.
398///
399/// Specifies which type of smart meter the Netzbetreiber is ordering from the MSB.
400#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
401#[serde(rename_all = "camelCase")]
402pub enum WimDeviceCategory {
403    /// Intelligentes Messsystem (iMSys) — full smart meter system.
404    #[serde(rename = "iMSys")]
405    IMSys,
406    /// Moderne Messeinrichtung (mME) — basic smart meter display.
407    #[serde(rename = "mME")]
408    Mme,
409    /// Moderne Messeinrichtung mit Kommunikationsadapter (mME+KME).
410    #[serde(rename = "mME+KME")]
411    MmeKme,
412}
413
414/// Rejection reason code for a WiM Ablehnung response.
415#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
416#[serde(rename_all = "camelCase")]
417pub enum WimRejectionReason {
418    /// MeLo does not exist in the MSB's service territory.
419    #[serde(rename = "meloUnknown")]
420    MeloUnknown,
421    /// MSB is not responsible for this MeLo.
422    #[serde(rename = "notResponsible")]
423    NotResponsible,
424    /// Requested device category is not installable at this MeLo.
425    #[serde(rename = "deviceCategoryNotSupported")]
426    DeviceCategoryNotSupported,
427    /// Regulatory prerequisites for iMSys rollout not yet met.
428    #[serde(rename = "rolloutPreconditionNotMet")]
429    RolloutPreconditionNotMet,
430    /// MSB technical capacity exhausted.
431    #[serde(rename = "capacityExhausted")]
432    CapacityExhausted,
433    /// Other / unspecified reason; see `reason_text` for details.
434    #[serde(rename = "other")]
435    Other,
436}
437
438/// Payload for a WiM Anmeldung (PID 11021) — NB orders iMS installation from MSB.
439///
440/// Sent by the Netzbetreiber to the Messstellenbetreiber over the REST channel.
441#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
442#[serde(rename_all = "camelCase")]
443pub struct WimAnmeldungRequest {
444    /// Messlokation EIC code at which the device should be installed.
445    pub melo_id: String,
446    /// 13-digit GLN of the Netzbetreiber (sender).
447    pub netzbetreiber_id: i64,
448    /// Requested process date (ISO 8601, date only, e.g. `"2026-06-01"`).
449    pub process_date: String,
450    /// Requested device category.
451    pub device_category: WimDeviceCategory,
452    /// Optional free-text notes (e.g. access instructions).
453    #[serde(skip_serializing_if = "Option::is_none")]
454    pub notes: Option<String>,
455}
456
457/// Payload for a WiM Bestätigung (PID 11022) — MSB confirms the order.
458///
459/// Sent by the MSB to the Netzbetreiber after accepting an Anmeldung.
460#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
461#[serde(rename_all = "camelCase")]
462pub struct WimBestaetigung {
463    /// UUID of the original Anmeldung transaction this response refers to.
464    pub reference_id: Uuid,
465    /// Confirmed installation date (ISO 8601, date only).
466    pub confirmed_process_date: String,
467    /// Assigned device identifier (EIC or MSB-internal reference).
468    #[serde(skip_serializing_if = "Option::is_none")]
469    pub device_id: Option<String>,
470}
471
472/// Payload for a WiM Ablehnung (PID 11023) — MSB rejects the order.
473///
474/// Sent by the MSB to the Netzbetreiber after refusing an Anmeldung.
475#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
476#[serde(rename_all = "camelCase")]
477pub struct WimAblehnung {
478    /// UUID of the original Anmeldung transaction this response refers to.
479    pub reference_id: Uuid,
480    /// Structured rejection reason code.
481    pub reason: WimRejectionReason,
482    /// Optional human-readable explanation (supplementary to `reason`).
483    #[serde(skip_serializing_if = "Option::is_none")]
484    pub reason_text: Option<String>,
485}