1use std::fmt;
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
28#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
29pub enum Carrier {
30 Orders,
32 Ordrsp,
34}
35
36impl Carrier {
37 #[must_use]
39 pub fn as_str(self) -> &'static str {
40 match self {
41 Self::Orders => "ORDERS",
42 Self::Ordrsp => "ORDRSP",
43 }
44 }
45
46 #[must_use]
48 pub fn from_unh_code(code: &str) -> Option<Self> {
49 match code {
50 "ORDERS" => Some(Self::Orders),
51 "ORDRSP" => Some(Self::Ordrsp),
52 _ => None,
53 }
54 }
55}
56
57impl fmt::Display for Carrier {
58 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
59 f.write_str(self.as_str())
60 }
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
65#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
66pub enum DvgwMessageType {
67 Alocat,
69 Nomint,
71 Nomres,
73 Ssqnot,
75}
76
77impl DvgwMessageType {
78 #[must_use]
80 pub fn as_str(self) -> &'static str {
81 match self {
82 Self::Alocat => "ALOCAT",
83 Self::Nomint => "NOMINT",
84 Self::Nomres => "NOMRES",
85 Self::Ssqnot => "SSQNOT",
86 }
87 }
88
89 #[must_use]
91 pub fn carrier(self) -> Carrier {
92 match self {
93 Self::Nomint => Carrier::Orders,
94 Self::Alocat | Self::Nomres | Self::Ssqnot => Carrier::Ordrsp,
95 }
96 }
97
98 #[must_use]
102 pub fn anwendungscode(self) -> &'static str {
103 match self {
104 Self::Alocat => "5.11a",
105 Self::Nomint | Self::Nomres | Self::Ssqnot => "DVGW17",
106 }
107 }
108
109 #[must_use]
115 pub fn admitted_units(self) -> &'static [&'static str] {
116 use crate::model::unit;
117 match self {
118 Self::Alocat => &[unit::KWH_PER_HOUR, unit::KWH_PER_DAY],
119 Self::Nomint | Self::Nomres => &[unit::KWH_PER_HOUR, unit::KWH],
120 Self::Ssqnot => &[unit::KWH],
121 }
122 }
123
124 #[must_use]
126 pub fn admitted_quantity_qualifiers(self) -> &'static [&'static str] {
127 use crate::model::qty;
128 match self {
129 Self::Alocat | Self::Nomint | Self::Nomres => &[qty::EINSPEISUNG, qty::AUSSPEISUNG],
130 Self::Ssqnot => &[qty::MEHRMENGE, qty::MINDERMENGE],
131 }
132 }
133
134 #[must_use]
136 pub fn admitted_location_qualifiers(self) -> &'static [&'static str] {
137 match self {
138 Self::Alocat | Self::Ssqnot => &["Z99"],
141 Self::Nomint | Self::Nomres => &["172", "Z17", "Z19"],
142 }
143 }
144
145 #[must_use]
147 pub fn documents(self) -> &'static [DvgwDocument] {
148 use DvgwDocument as D;
149 match self {
150 Self::Alocat => &[
151 D::AllokationSlp,
152 D::KorrigierteMengenmeldungNkp,
153 D::SlpErsatzwerte,
154 D::UntertaegigeAllokation,
155 D::EndgueltigeAllokation,
156 D::KorrigierteAllokationBilanzierungsbrennwert,
157 D::KorrigierteAllokationAbrechnungsbrennwert,
158 D::TaeglicheMengenmeldungNkp,
159 ],
160 Self::Nomint => &[
161 D::NominierungTransportkunde,
162 D::NominierungVirtuellerHandelspunkt,
163 D::Flexibilitaetsuebertragung,
164 D::NominierungGebuendelteKapazitaet,
165 D::NominierungsweitergabeNetzbetreiber,
166 ],
167 Self::Nomres => &[
168 D::MatchingBenachrichtigung,
169 D::Bestaetigung,
170 D::VhpMatchingBenachrichtigung,
171 D::VhpBestaetigung,
172 D::BestaetigungFlexibilitaetsuebertragung,
173 ],
174 Self::Ssqnot => &[D::MehrMindermengenmeldung],
175 }
176 }
177
178 pub const ALL: [Self; 4] = [Self::Alocat, Self::Nomint, Self::Nomres, Self::Ssqnot];
180}
181
182impl fmt::Display for DvgwMessageType {
183 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
184 f.write_str(self.as_str())
185 }
186}
187
188#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
195#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
196#[non_exhaustive]
197pub enum DvgwDocument {
198 AllokationSlp,
201 KorrigierteMengenmeldungNkp,
203 SlpErsatzwerte,
205 UntertaegigeAllokation,
207 EndgueltigeAllokation,
209 KorrigierteAllokationBilanzierungsbrennwert,
211 KorrigierteAllokationAbrechnungsbrennwert,
213 TaeglicheMengenmeldungNkp,
215
216 NominierungTransportkunde,
219 NominierungVirtuellerHandelspunkt,
221 Flexibilitaetsuebertragung,
223 NominierungGebuendelteKapazitaet,
225 NominierungsweitergabeNetzbetreiber,
227
228 MatchingBenachrichtigung,
231 Bestaetigung,
233 VhpMatchingBenachrichtigung,
235 VhpBestaetigung,
237 BestaetigungFlexibilitaetsuebertragung,
239
240 MehrMindermengenmeldung,
243}
244
245const CATALOGUE: &[(&str, DvgwDocument, &str)] = {
248 use DvgwDocument as D;
249 &[
250 (
251 "X1G",
252 D::AllokationSlp,
253 "Allokation anhand von Standardlastprofilen (SLP)",
254 ),
255 (
256 "X2G",
257 D::KorrigierteMengenmeldungNkp,
258 "Korrigierte Mengenmeldung NKP je Netzkonto",
259 ),
260 ("X3G", D::SlpErsatzwerte, "SLP-Ersatzwerte"),
261 (
262 "X4G",
263 D::UntertaegigeAllokation,
264 "Untertägige Allokation (Intraday)",
265 ),
266 (
267 "X5G",
268 D::EndgueltigeAllokation,
269 "Endgültige Allokation (Bilanzierungsbrennwert)",
270 ),
271 (
272 "X6G",
273 D::KorrigierteAllokationBilanzierungsbrennwert,
274 "Korrigierte Allokation (Bilanzierungsbrennwert)",
275 ),
276 (
277 "X7G",
278 D::KorrigierteAllokationAbrechnungsbrennwert,
279 "Korrigierte Allokation (Abrechnungsbrennwert)",
280 ),
281 (
282 "XBG",
283 D::TaeglicheMengenmeldungNkp,
284 "Tägliche Mengenmeldung NKP je Netzkonto",
285 ),
286 (
287 "01G",
288 D::NominierungTransportkunde,
289 "Nominierung von einem Transportkunden",
290 ),
291 (
292 "55G",
293 D::NominierungVirtuellerHandelspunkt,
294 "Nominierung an einem Virtuellen Handelspunkt",
295 ),
296 (
297 "Y1G",
298 D::Flexibilitaetsuebertragung,
299 "Flexibilitätsübertragung",
300 ),
301 (
302 "Y6G",
303 D::NominierungGebuendelteKapazitaet,
304 "Nominierung gebündelter Kapazität an MÜP und GÜP",
305 ),
306 (
307 "Y7G",
308 D::NominierungsweitergabeNetzbetreiber,
309 "Nominierungsweitergabe zwischen Netzbetreibern",
310 ),
311 (
312 "07G",
313 D::MatchingBenachrichtigung,
314 "Matching-Benachrichtigung",
315 ),
316 ("08G", D::Bestaetigung, "Bestätigung"),
317 (
318 "19G",
319 D::VhpMatchingBenachrichtigung,
320 "Virtueller Handelspunkt: Matching-Benachrichtigung",
321 ),
322 (
323 "20G",
324 D::VhpBestaetigung,
325 "Virtueller Handelspunkt: Bestätigung",
326 ),
327 (
328 "Y2G",
329 D::BestaetigungFlexibilitaetsuebertragung,
330 "Bestätigung Flexibilitätsübertragung",
331 ),
332 (
333 "BAG",
334 D::MehrMindermengenmeldung,
335 "Mehr-/Mindermengenmeldung zur Führung des Netzkontos",
336 ),
337 ]
338};
339
340impl DvgwDocument {
341 #[must_use]
343 pub fn code(self) -> &'static str {
344 CATALOGUE
345 .iter()
346 .find(|(_, d, _)| *d == self)
347 .map_or("", |(c, _, _)| *c)
348 }
349
350 #[must_use]
352 pub fn from_code(code: &str) -> Option<Self> {
353 CATALOGUE
354 .iter()
355 .find(|(c, _, _)| *c == code)
356 .map(|(_, d, _)| *d)
357 }
358
359 #[must_use]
361 pub fn description(self) -> &'static str {
362 CATALOGUE
363 .iter()
364 .find(|(_, d, _)| *d == self)
365 .map_or("", |(_, _, t)| *t)
366 }
367
368 #[must_use]
370 pub fn message_type(self) -> DvgwMessageType {
371 for mt in DvgwMessageType::ALL {
372 if mt.documents().contains(&self) {
373 return mt;
374 }
375 }
376 unreachable!("every DvgwDocument is listed in exactly one DvgwMessageType::documents()")
377 }
378
379 #[must_use]
381 pub fn carrier(self) -> Carrier {
382 self.message_type().carrier()
383 }
384
385 pub fn all() -> impl Iterator<Item = Self> {
387 CATALOGUE.iter().map(|(_, d, _)| *d)
388 }
389
390 #[must_use]
396 pub fn for_pid(pid: u32) -> Option<Self> {
397 use DvgwDocument as D;
398 Some(match pid {
399 70001 | 70008 | 70013 | 70018 | 70022 => D::AllokationSlp,
400 70002 | 70011 | 70023 => D::KorrigierteMengenmeldungNkp,
401 70003 | 70012 => D::TaeglicheMengenmeldungNkp,
402 70004 | 70014 => D::UntertaegigeAllokation,
403 70005 | 70015 => D::EndgueltigeAllokation,
404 70006 | 70009 | 70016 | 70019 => D::KorrigierteAllokationBilanzierungsbrennwert,
405 70007 | 70010 | 70017 | 70020 => D::KorrigierteAllokationAbrechnungsbrennwert,
406 70021 => D::SlpErsatzwerte,
407 70030 => D::NominierungTransportkunde,
408 70031 => D::NominierungVirtuellerHandelspunkt,
409 70032 => D::Flexibilitaetsuebertragung,
410 70033 => D::NominierungGebuendelteKapazitaet,
411 70034 => D::NominierungsweitergabeNetzbetreiber,
412 70035 => D::MatchingBenachrichtigung,
413 70036 => D::Bestaetigung,
414 70037 => D::VhpMatchingBenachrichtigung,
415 70038 => D::VhpBestaetigung,
416 70039 => D::BestaetigungFlexibilitaetsuebertragung,
417 70095 | 70096 => D::MehrMindermengenmeldung,
418 _ => return None,
419 })
420 }
421}
422
423pub const DVGW_AGENCY_CODE: &str = "332";
426
427impl fmt::Display for DvgwDocument {
428 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
429 f.write_str(self.code())
430 }
431}
432
433#[cfg(test)]
434mod tests {
435 use super::*;
436
437 #[test]
438 fn every_document_round_trips_through_its_code() {
439 for doc in DvgwDocument::all() {
440 assert!(!doc.code().is_empty(), "{doc:?} has no wire code");
441 assert!(!doc.description().is_empty(), "{doc:?} has no description");
442 assert_eq!(DvgwDocument::from_code(doc.code()), Some(doc));
443 }
444 }
445
446 #[test]
447 fn the_catalogue_has_no_duplicate_codes() {
448 let mut codes: Vec<&str> = CATALOGUE.iter().map(|(c, _, _)| *c).collect();
449 let total = codes.len();
450 codes.sort_unstable();
451 codes.dedup();
452 assert_eq!(
453 codes.len(),
454 total,
455 "duplicate BGM 1001 code in the catalogue"
456 );
457 }
458
459 #[test]
460 fn message_type_documents_partition_the_catalogue() {
461 let listed: usize = DvgwMessageType::ALL
462 .iter()
463 .map(|m| m.documents().len())
464 .sum();
465 assert_eq!(
466 listed,
467 CATALOGUE.len(),
468 "a document code is unreachable from its family"
469 );
470 }
471
472 #[test]
474 fn every_catalogued_pid_names_a_document_of_its_family() {
475 for info in crate::pruefidentifikator::catalogue() {
476 let doc = DvgwDocument::for_pid(info.pid)
477 .unwrap_or_else(|| panic!("{} has no BGM code", info.pid));
478 assert_eq!(doc.message_type(), info.message_type, "{}", info.pid);
479 }
480 assert_eq!(DvgwDocument::for_pid(70_500), None);
481 }
482
483 #[test]
485 fn nomint_rides_orders_and_the_rest_ride_ordrsp() {
486 assert_eq!(DvgwMessageType::Nomint.carrier(), Carrier::Orders);
487 assert_eq!(DvgwMessageType::Alocat.carrier(), Carrier::Ordrsp);
488 assert_eq!(DvgwMessageType::Nomres.carrier(), Carrier::Ordrsp);
489 assert_eq!(DvgwMessageType::Ssqnot.carrier(), Carrier::Ordrsp);
490 assert_eq!(
491 DvgwDocument::from_code("BAG"),
492 Some(DvgwDocument::MehrMindermengenmeldung)
493 );
494 assert_eq!(
495 Carrier::from_unh_code("ALOCAT"),
496 None,
497 "ALOCAT is not a wire carrier"
498 );
499 }
500}