bo4e_core/com/
energy_mix.rs

1//! Energy mix (Energiemix) component.
2
3use serde::{Deserialize, Serialize};
4
5use crate::enums::{Division, EcoCertificate, EcoLabel};
6use crate::traits::{Bo4eMeta, Bo4eObject};
7
8use super::EnergySource;
9
10/// The composition of energy sources for a supplier's energy mix.
11///
12/// German: Energiemix
13///
14/// # Example
15///
16/// ```rust
17/// use bo4e_core::com::{EnergyMix, EnergySource};
18/// use bo4e_core::enums::GenerationType;
19///
20/// let mix = EnergyMix {
21///     energy_mix_number: Some(1),
22///     designation: Some("Green Energy Mix".to_string()),
23///     valid_year: Some(2024),
24///     sources: vec![
25///         EnergySource {
26///             generation_type: Some(GenerationType::Solar),
27///             percentage_share: Some(40.0),
28///             ..Default::default()
29///         },
30///         EnergySource {
31///             generation_type: Some(GenerationType::Wind),
32///             percentage_share: Some(60.0),
33///             ..Default::default()
34///         },
35///     ],
36///     ..Default::default()
37/// };
38/// ```
39#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
40#[serde(rename_all = "camelCase")]
41pub struct EnergyMix {
42    /// BO4E metadata
43    #[serde(flatten)]
44    pub meta: Bo4eMeta,
45
46    /// Unique identifier for the energy mix (Energiemixnummer)
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub energy_mix_number: Option<i32>,
49
50    /// Energy type/division (Sparte)
51    #[serde(skip_serializing_if = "Option::is_none")]
52    pub division: Option<Division>,
53
54    /// Name/designation of the energy mix (Bezeichnung)
55    #[serde(skip_serializing_if = "Option::is_none")]
56    pub designation: Option<String>,
57
58    /// Year for which this mix applies (Gültigkeitsjahr)
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub valid_year: Option<i32>,
61
62    /// Individual energy sources and their shares (Anteil)
63    #[serde(default, skip_serializing_if = "Vec::is_empty")]
64    pub sources: Vec<EnergySource>,
65
66    /// Notes about the mix (Bemerkung)
67    #[serde(skip_serializing_if = "Option::is_none")]
68    pub notes: Option<String>,
69
70    /// CO₂ emissions in g/kWh (CO2-Emission)
71    #[serde(skip_serializing_if = "Option::is_none")]
72    pub co2_emission: Option<f64>,
73
74    /// Nuclear waste in g/kWh (Atommüll)
75    #[serde(skip_serializing_if = "Option::is_none")]
76    pub nuclear_waste: Option<f64>,
77
78    /// Environmental certificates (Ökozertifikate)
79    #[serde(default, skip_serializing_if = "Vec::is_empty")]
80    pub eco_certificates: Vec<EcoCertificate>,
81
82    /// Eco-labels (Ökolabel)
83    #[serde(default, skip_serializing_if = "Vec::is_empty")]
84    pub eco_labels: Vec<EcoLabel>,
85
86    /// Whether provider is in eco top ten (Ist in Öko Top Ten)
87    #[serde(skip_serializing_if = "Option::is_none")]
88    pub in_eco_top_ten: Option<bool>,
89
90    /// Website for published energy mix data (Website)
91    #[serde(skip_serializing_if = "Option::is_none")]
92    pub website: Option<String>,
93}
94
95impl Bo4eObject for EnergyMix {
96    fn type_name_german() -> &'static str {
97        "Energiemix"
98    }
99
100    fn type_name_english() -> &'static str {
101        "EnergyMix"
102    }
103
104    fn meta(&self) -> &Bo4eMeta {
105        &self.meta
106    }
107
108    fn meta_mut(&mut self) -> &mut Bo4eMeta {
109        &mut self.meta
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116    use crate::enums::GenerationType;
117
118    #[test]
119    fn test_green_energy_mix() {
120        let mix = EnergyMix {
121            energy_mix_number: Some(1),
122            designation: Some("100% Renewable".to_string()),
123            valid_year: Some(2024),
124            sources: vec![
125                EnergySource {
126                    generation_type: Some(GenerationType::Solar),
127                    percentage_share: Some(40.0),
128                    ..Default::default()
129                },
130                EnergySource {
131                    generation_type: Some(GenerationType::Wind),
132                    percentage_share: Some(60.0),
133                    ..Default::default()
134                },
135            ],
136            co2_emission: Some(0.0),
137            nuclear_waste: Some(0.0),
138            in_eco_top_ten: Some(true),
139            ..Default::default()
140        };
141
142        assert_eq!(mix.sources.len(), 2);
143        assert_eq!(mix.co2_emission, Some(0.0));
144        assert_eq!(mix.in_eco_top_ten, Some(true));
145    }
146
147    #[test]
148    fn test_default() {
149        let mix = EnergyMix::default();
150        assert!(mix.energy_mix_number.is_none());
151        assert!(mix.designation.is_none());
152        assert!(mix.sources.is_empty());
153    }
154
155    #[test]
156    fn test_serialize() {
157        let mix = EnergyMix {
158            energy_mix_number: Some(42),
159            designation: Some("Test Mix".to_string()),
160            valid_year: Some(2024),
161            co2_emission: Some(150.5),
162            ..Default::default()
163        };
164
165        let json = serde_json::to_string(&mix).unwrap();
166        assert!(json.contains(r#""energyMixNumber":42"#));
167        assert!(json.contains(r#""designation":"Test Mix""#));
168        assert!(json.contains(r#""co2Emission":150.5"#));
169    }
170
171    #[test]
172    fn test_roundtrip() {
173        let mix = EnergyMix {
174            energy_mix_number: Some(1),
175            division: Some(Division::Electricity),
176            designation: Some("Ökostrom".to_string()),
177            valid_year: Some(2024),
178            sources: vec![EnergySource {
179                generation_type: Some(GenerationType::Hydro),
180                percentage_share: Some(100.0),
181                ..Default::default()
182            }],
183            co2_emission: Some(0.0),
184            eco_labels: vec![EcoLabel::GruenerStrom],
185            in_eco_top_ten: Some(true),
186            website: Some("https://example.com/energiemix".to_string()),
187            ..Default::default()
188        };
189
190        let json = serde_json::to_string(&mix).unwrap();
191        let parsed: EnergyMix = serde_json::from_str(&json).unwrap();
192        assert_eq!(mix, parsed);
193    }
194
195    #[test]
196    fn test_bo4e_object_impl() {
197        assert_eq!(EnergyMix::type_name_german(), "Energiemix");
198        assert_eq!(EnergyMix::type_name_english(), "EnergyMix");
199    }
200}