Skip to main content

energy_api/models/
electricity.rs

1//! Wire-format types for the EDI-Energy electricity market APIs.
2//!
3//! Covers two 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
9use serde::{Deserialize, Serialize};
10use uuid::Uuid;
11
12// ── Shared identifiers ────────────────────────────────────────────────────────
13
14/// External transaction ID (UUID RFC 4122), chosen by the sender.
15pub type TransactionId = Uuid;
16/// Idempotency key for retries (UUID RFC 4122).
17pub type InitialTransactionId = Uuid;
18/// External reference correlating a response to a prior request (UUID RFC 4122).
19pub type ReferenceId = Uuid;
20/// 13-digit market partner identifier.
21pub type MarketPartnerId = i64;
22
23/// Network location identifier — pattern `E[A-Z0-9]{9}[0-9]`.
24#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
25pub struct NeloId(pub String);
26
27/// Controllable resource identifier — pattern `C[A-Z0-9]{9}[0-9]`.
28#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
29pub struct SrId(pub String);
30
31/// Either a network location ID or a controllable resource ID.
32#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
33#[serde(untagged)]
34pub enum LocationId {
35    NetworkLocation(NeloId),
36    ControllableResource(SrId),
37}
38
39impl std::fmt::Display for NeloId {
40    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41        f.write_str(&self.0)
42    }
43}
44impl std::fmt::Display for SrId {
45    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46        f.write_str(&self.0)
47    }
48}
49impl std::fmt::Display for LocationId {
50    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51        match self {
52            LocationId::NetworkLocation(id) => id.fmt(f),
53            LocationId::ControllableResource(id) => id.fmt(f),
54        }
55    }
56}
57
58// ── Control Measures ──────────────────────────────────────────────────────────
59
60/// Maximum power value in kW (`"\d{0,6}(\.\d{1,3})?"`).
61#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
62pub struct MaximumPowerValue(pub String);
63
64/// Regulate a location to a specific maximum power value.
65#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
66#[serde(rename_all = "camelCase")]
67pub struct CommandControl {
68    pub maximum_power_value: MaximumPowerValue,
69    /// Start of effect period — ISO 8601 UTC, second precision (e.g. `"2023-08-01T12:30:00Z"`).
70    pub execution_time_from: String,
71    /// Optional end of effect period.
72    #[serde(skip_serializing_if = "Option::is_none")]
73    pub execution_time_until: Option<String>,
74}
75
76/// Reset a location to its initial / uncontrolled state.
77#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
78#[serde(rename_all = "camelCase")]
79pub struct CommandRegular {
80    /// Start of effect period — ISO 8601 UTC, second precision.
81    pub execution_time_from: String,
82}
83
84/// Reason for a negative response from the MSB.
85#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
86pub enum ReasonNegative {
87    /// Communication to the control box was disrupted.
88    #[serde(rename = "communicationFailure")]
89    CommunicationFailure,
90    /// MSB back-end is overloaded.
91    #[serde(rename = "overload")]
92    Overload,
93    /// MSB is procedurally unable to fulfil the request.
94    #[serde(rename = "unable")]
95    Unable,
96}
97
98/// Terminal state for negative (failure) responses.
99#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
100pub enum StateNegative {
101    #[serde(rename = "failed")]
102    Failed,
103}
104
105/// Terminal state for positive (success) responses.
106#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
107pub enum StatePositive {
108    #[serde(rename = "succeeded")]
109    Succeeded,
110}
111
112/// Preliminary state — command is executable in principle.
113#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
114pub enum PreliminaryStatePositive {
115    #[serde(rename = "possible")]
116    Possible,
117}
118
119/// State indicating the final outcome is not yet known.
120#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
121pub enum StateUnknown {
122    #[serde(rename = "unknown")]
123    Unknown,
124}
125
126// ── MaLo Identification ───────────────────────────────────────────────────────
127
128/// Market location identifier — 11-digit string.
129#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
130pub struct MaloId(pub String);
131
132/// Metering location identifier — pattern `DE\d{11}[A-Z,\d]{20}`.
133#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
134pub struct MeloId(pub String);
135
136/// Technical resource identifier — pattern `D[A-Z0-9]{9}[0-9]`.
137#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
138pub struct TrId(pub String);
139
140/// Energy flow direction at a market location.
141#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
142pub enum EnergyDirection {
143    #[serde(rename = "consumption")]
144    Consumption,
145    #[serde(rename = "production")]
146    Production,
147}
148
149/// Metering technology classification of a market location.
150#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
151pub enum MeasurementTechnologyClassification {
152    #[serde(rename = "intelligentMeasuringSystem")]
153    IntelligentMeasuringSystem,
154    #[serde(rename = "conventionalMeasuringSystem")]
155    ConventionalMeasuringSystem,
156    #[serde(rename = "noMeasurement")]
157    NoMeasurement,
158}
159
160/// Whether the forecast basis may be changed.
161#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
162pub enum OptionalChangeForecastBasis {
163    #[serde(rename = "possible")]
164    Possible,
165    #[serde(rename = "notPossible")]
166    NotPossible,
167}
168
169/// Lifecycle property / category of a market location.
170#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
171pub enum MarketLocationProperty {
172    #[serde(rename = "customerFacility")]
173    CustomerFacility,
174    /// Dormant market location (spec spelling: `"nonActice"`).
175    #[serde(rename = "nonActice")]
176    NonActive,
177    #[serde(rename = "standard")]
178    Standard,
179}
180
181/// Tranche proportion type.
182#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
183pub enum ProportionType {
184    #[serde(rename = "bilateralAgreement")]
185    BilateralAgreement,
186    #[serde(rename = "percent")]
187    Percent,
188}
189
190/// Input parameters for a MaLo identification request.
191#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
192#[serde(rename_all = "camelCase")]
193pub struct IdentificationParameter {
194    /// Effective date for identification — ISO 8601 UTC, day-boundary midnight.
195    pub identification_date_time: String,
196    pub energy_direction: EnergyDirection,
197    /// Optional ID-based search criteria.
198    #[serde(skip_serializing_if = "Option::is_none")]
199    pub identification_parameter_id: Option<IdentificationParameterId>,
200    /// Address-based search criteria.
201    pub identification_parameter_address: IdentificationParameterAddress,
202}
203
204/// Optional ID-based identification parameters.
205#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
206#[serde(rename_all = "camelCase")]
207pub struct IdentificationParameterId {
208    #[serde(skip_serializing_if = "Option::is_none")]
209    pub malo_id: Option<MaloId>,
210    #[serde(skip_serializing_if = "Option::is_none")]
211    pub tranchen_ids: Option<Vec<String>>,
212    #[serde(skip_serializing_if = "Option::is_none")]
213    pub melo_ids: Option<Vec<MeloId>>,
214    #[serde(skip_serializing_if = "Option::is_none")]
215    pub meter_numbers: Option<Vec<String>>,
216    #[serde(skip_serializing_if = "Option::is_none")]
217    pub customer_number: Option<String>,
218}
219
220/// Address-based identification parameters.
221#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
222#[serde(rename_all = "camelCase")]
223pub struct IdentificationParameterAddress {
224    #[serde(skip_serializing_if = "Option::is_none")]
225    pub name: Option<PersonName>,
226    #[serde(skip_serializing_if = "Option::is_none")]
227    pub address: Option<PostalAddress>,
228}
229
230/// Person or company name.
231#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
232#[serde(rename_all = "camelCase")]
233pub struct PersonName {
234    #[serde(skip_serializing_if = "Option::is_none")]
235    pub surnames: Option<String>,
236    #[serde(skip_serializing_if = "Option::is_none")]
237    pub firstnames: Option<String>,
238    #[serde(skip_serializing_if = "Option::is_none")]
239    pub title: Option<String>,
240    #[serde(skip_serializing_if = "Option::is_none")]
241    pub company: Option<String>,
242}
243
244/// German postal address.
245#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
246#[serde(rename_all = "camelCase")]
247pub struct PostalAddress {
248    #[serde(skip_serializing_if = "Option::is_none")]
249    pub country_code: Option<String>,
250    #[serde(skip_serializing_if = "Option::is_none")]
251    pub zip_code: Option<String>,
252    #[serde(skip_serializing_if = "Option::is_none")]
253    pub city: Option<String>,
254    #[serde(skip_serializing_if = "Option::is_none")]
255    pub street: Option<String>,
256    #[serde(skip_serializing_if = "Option::is_none")]
257    pub house_number: Option<i32>,
258    #[serde(skip_serializing_if = "Option::is_none")]
259    pub house_number_addition: Option<String>,
260}
261
262// ── MaLo Identification response types ───────────────────────────────────────
263
264/// Positive identification result — all data the NB holds about the market
265/// location from `identificationDateTime` onwards.
266#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
267#[serde(rename_all = "camelCase")]
268pub struct MaloIdentResultPositive {
269    pub data_market_location: DataMarketLocation,
270    #[serde(skip_serializing_if = "Option::is_none")]
271    pub data_tranches: Option<Vec<DataTranche>>,
272    #[serde(skip_serializing_if = "Option::is_none")]
273    pub data_meter_locations: Option<Vec<DataMeterLocation>>,
274    #[serde(skip_serializing_if = "Option::is_none")]
275    pub data_technical_resources: Option<Vec<DataTechnicalResource>>,
276    #[serde(skip_serializing_if = "Option::is_none")]
277    pub data_controllable_resources: Option<Vec<DataControllableResource>>,
278    #[serde(skip_serializing_if = "Option::is_none")]
279    pub data_network_locations: Option<Vec<DataNetworkLocation>>,
280}
281
282/// Negative identification result, referencing the applicable decision tree.
283#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
284#[serde(rename_all = "camelCase")]
285pub struct MaloIdentResultNegative {
286    /// Decision tree code from EDI@energy, e.g. `"E_0594"`.
287    pub decision_tree: String,
288    /// Response code from that tree, e.g. `"A10"`.
289    pub response_code: String,
290    #[serde(skip_serializing_if = "Option::is_none")]
291    pub reason: Option<String>,
292    /// NB that now holds the location (when it left this NB's grid area).
293    #[serde(skip_serializing_if = "Option::is_none")]
294    pub network_operator: Option<MarketPartnerId>,
295}
296
297/// Full data about the identified market location.
298#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
299#[serde(rename_all = "camelCase")]
300pub struct DataMarketLocation {
301    pub malo_id: MaloId,
302    pub energy_direction: EnergyDirection,
303    pub measurement_technology_classification: MeasurementTechnologyClassification,
304    pub optional_change_forecast_basis: OptionalChangeForecastBasis,
305    pub data_market_location_properties: Vec<MarketLocationProperties>,
306    pub data_market_location_network_operators: Vec<TimeSlicedMarketPartner>,
307    pub data_market_location_transmission_system_operators: Vec<TimeSlicedMarketPartner>,
308    #[serde(skip_serializing_if = "Option::is_none")]
309    pub data_market_location_measuring_point_operators: Option<Vec<TimeSlicedMarketPartner>>,
310    #[serde(skip_serializing_if = "Option::is_none")]
311    pub data_market_location_suppliers: Option<Vec<TimeSlicedMarketPartner>>,
312    #[serde(skip_serializing_if = "Option::is_none")]
313    pub data_market_location_name: Option<PersonName>,
314    #[serde(skip_serializing_if = "Option::is_none")]
315    pub data_market_location_address: Option<PostalAddress>,
316}
317
318/// A market partner assignment valid for a specific time slice.
319#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
320#[serde(rename_all = "camelCase")]
321pub struct TimeSlicedMarketPartner {
322    pub market_partner_id: MarketPartnerId,
323    pub execution_time_from: String,
324    #[serde(skip_serializing_if = "Option::is_none")]
325    pub execution_time_until: Option<String>,
326}
327
328/// Property of a market location valid for a specific time slice.
329#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
330#[serde(rename_all = "camelCase")]
331pub struct MarketLocationProperties {
332    pub market_location_property: MarketLocationProperty,
333    pub execution_time_from: String,
334    #[serde(skip_serializing_if = "Option::is_none")]
335    pub execution_time_until: Option<String>,
336}
337
338/// Data about a metering location (Messlokation) at the market location.
339#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
340#[serde(rename_all = "camelCase")]
341pub struct DataMeterLocation {
342    pub melo_id: MeloId,
343    pub meter_number: String,
344    pub data_meter_location_measuring_point_operators: Vec<TimeSlicedMarketPartner>,
345}
346
347/// Data about a technical resource (Technische Ressource) at the market location.
348#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
349#[serde(rename_all = "camelCase")]
350pub struct DataTechnicalResource {
351    pub tr_id: TrId,
352}
353
354/// Data about a controllable resource (Steuerbare Ressource) at the market location.
355#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
356#[serde(rename_all = "camelCase")]
357pub struct DataControllableResource {
358    pub sr_id: SrId,
359    #[serde(skip_serializing_if = "Option::is_none")]
360    pub data_controllable_resource_measuring_point_operators: Option<Vec<SrMarketPartner>>,
361}
362
363/// Market partner assignment at a controllable resource.
364#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
365#[serde(rename_all = "camelCase")]
366pub struct SrMarketPartner {
367    pub market_partner_id: MarketPartnerId,
368    pub execution_time_from: String,
369    #[serde(skip_serializing_if = "Option::is_none")]
370    pub execution_time_until: Option<String>,
371    pub market_partner_type_sr: String,
372}
373
374/// Data about a network location (Netzlokation) linked to the market location.
375#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
376#[serde(rename_all = "camelCase")]
377pub struct DataNetworkLocation {
378    pub nelo_id: NeloId,
379    pub data_network_location_measuring_point_operators: Vec<TimeSlicedMarketPartner>,
380}
381
382/// A billing tranche at a market location.
383#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
384#[serde(rename_all = "camelCase")]
385pub struct DataTranche {
386    pub tranchen_id: String,
387    pub proportion: ProportionType,
388    #[serde(skip_serializing_if = "Option::is_none")]
389    pub percent: Option<f64>,
390    pub data_tranche_suppliers: Vec<TimeSlicedMarketPartner>,
391}