Skip to main content

bo4e_core/bo/
meter.rs

1//! Meter (Zaehler) business object.
2//!
3//! Represents a metering device for measuring energy consumption or production.
4
5use serde::{Deserialize, Serialize};
6
7use crate::com::{Address, Hardware, MeterRegister};
8use crate::enums::{Division, MeterSize, MeterType};
9use crate::traits::{Bo4eMeta, Bo4eObject};
10
11/// A meter (Zähler) for measuring energy consumption or production.
12///
13/// German: Zaehler
14///
15/// # Example
16///
17/// ```rust
18/// use bo4e_core::bo::Meter;
19/// use bo4e_core::enums::{Division, MeterType};
20///
21/// let meter = Meter {
22///     meter_number: Some("1EMH0012345678".to_string()),
23///     division: Some(Division::Electricity),
24///     meter_type: Some(MeterType::ModernMeasuringDevice),
25///     ..Default::default()
26/// };
27/// ```
28#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
29#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
30#[cfg_attr(feature = "json-schema", schemars(rename = "Zaehler"))]
31#[serde(rename_all = "camelCase")]
32pub struct Meter {
33    /// BO4E metadata
34    #[serde(flatten)]
35    pub meta: Bo4eMeta,
36
37    /// Unique meter identification number (Zaehlernummer)
38    #[serde(skip_serializing_if = "Option::is_none", alias = "zaehlernummer")]
39    #[cfg_attr(feature = "json-schema", schemars(rename = "zaehlernummer"))]
40    pub meter_number: Option<String>,
41
42    /// Energy division (Sparte)
43    #[serde(skip_serializing_if = "Option::is_none", alias = "sparte")]
44    #[cfg_attr(feature = "json-schema", schemars(rename = "sparte"))]
45    pub division: Option<Division>,
46
47    /// Type of meter (Zaehlertyp)
48    #[serde(skip_serializing_if = "Option::is_none", alias = "zaehlertyp")]
49    #[cfg_attr(feature = "json-schema", schemars(rename = "zaehlertyp"))]
50    pub meter_type: Option<MeterType>,
51
52    /// Meter size classification (Zaehlergroesse)
53    #[serde(skip_serializing_if = "Option::is_none", alias = "zaehlergroesse")]
54    #[cfg_attr(feature = "json-schema", schemars(rename = "zaehlergroesse"))]
55    pub meter_size: Option<MeterSize>,
56
57    /// Installation location address (Standort)
58    #[serde(skip_serializing_if = "Option::is_none", alias = "standort")]
59    #[cfg_attr(feature = "json-schema", schemars(rename = "standort"))]
60    pub location: Option<Address>,
61
62    /// Registers on this meter (Zaehlwerke)
63    #[serde(default, skip_serializing_if = "Vec::is_empty", alias = "zaehlwerke")]
64    #[cfg_attr(feature = "json-schema", schemars(rename = "zaehlwerke"))]
65    pub registers: Vec<MeterRegister>,
66
67    /// Hardware components (Geraeteeigenschaften)
68    #[serde(
69        default,
70        skip_serializing_if = "Vec::is_empty",
71        alias = "geraeteeigenschaften"
72    )]
73    #[cfg_attr(feature = "json-schema", schemars(rename = "geraeteeigenschaften"))]
74    pub hardware: Vec<Hardware>,
75
76    /// Reference to associated market location ID (Marktlokation)
77    #[serde(skip_serializing_if = "Option::is_none", alias = "marktlokationsId")]
78    #[cfg_attr(feature = "json-schema", schemars(rename = "marktlokationsId"))]
79    pub market_location_id: Option<String>,
80
81    /// Reference to associated metering location ID (Messlokation)
82    #[serde(skip_serializing_if = "Option::is_none", alias = "messlokationsId")]
83    #[cfg_attr(feature = "json-schema", schemars(rename = "messlokationsId"))]
84    pub metering_location_id: Option<String>,
85
86    /// Ownership status (Eigentumsverhaeltnis)
87    #[serde(
88        skip_serializing_if = "Option::is_none",
89        alias = "eigentumsverhaeltnis"
90    )]
91    #[cfg_attr(feature = "json-schema", schemars(rename = "eigentumsverhaeltnis"))]
92    pub ownership: Option<String>,
93
94    /// Manufacturer (Hersteller)
95    #[serde(skip_serializing_if = "Option::is_none", alias = "hersteller")]
96    #[cfg_attr(feature = "json-schema", schemars(rename = "hersteller"))]
97    pub manufacturer: Option<String>,
98
99    /// Manufacturing year (Herstellungsjahr)
100    #[serde(skip_serializing_if = "Option::is_none", alias = "herstellungsjahr")]
101    #[cfg_attr(feature = "json-schema", schemars(rename = "herstellungsjahr"))]
102    pub manufacturing_year: Option<i32>,
103
104    /// Installation date (Einbaudatum)
105    #[serde(skip_serializing_if = "Option::is_none", alias = "einbaudatum")]
106    #[cfg_attr(feature = "json-schema", schemars(rename = "einbaudatum"))]
107    pub installation_date: Option<chrono::DateTime<chrono::Utc>>,
108
109    /// Removal date (Ausbaudatum)
110    #[serde(skip_serializing_if = "Option::is_none", alias = "ausbaudatum")]
111    #[cfg_attr(feature = "json-schema", schemars(rename = "ausbaudatum"))]
112    pub removal_date: Option<chrono::DateTime<chrono::Utc>>,
113
114    /// Calibration date (Eichdatum)
115    #[serde(skip_serializing_if = "Option::is_none", alias = "eichdatum")]
116    #[cfg_attr(feature = "json-schema", schemars(rename = "eichdatum"))]
117    pub calibration_date: Option<chrono::DateTime<chrono::Utc>>,
118
119    /// Calibration expiry date (Eichablaufdatum)
120    #[serde(skip_serializing_if = "Option::is_none", alias = "eichablaufdatum")]
121    #[cfg_attr(feature = "json-schema", schemars(rename = "eichablaufdatum"))]
122    pub calibration_expiry_date: Option<chrono::DateTime<chrono::Utc>>,
123}
124
125impl Bo4eObject for Meter {
126    fn type_name_german() -> &'static str {
127        "Zaehler"
128    }
129
130    fn type_name_english() -> &'static str {
131        "Meter"
132    }
133
134    fn meta(&self) -> &Bo4eMeta {
135        &self.meta
136    }
137
138    fn meta_mut(&mut self) -> &mut Bo4eMeta {
139        &mut self.meta
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146
147    #[test]
148    fn test_meter_creation() {
149        let meter = Meter {
150            meter_number: Some("1EMH0012345678".to_string()),
151            division: Some(Division::Electricity),
152            ..Default::default()
153        };
154
155        assert_eq!(meter.meter_number, Some("1EMH0012345678".to_string()));
156        assert_eq!(meter.division, Some(Division::Electricity));
157    }
158
159    #[test]
160    fn test_meter_with_registers() {
161        let register = MeterRegister {
162            obis_code: Some("1-0:1.8.0".to_string()),
163            ..Default::default()
164        };
165
166        let meter = Meter {
167            meter_number: Some("TEST123".to_string()),
168            registers: vec![register],
169            ..Default::default()
170        };
171
172        assert_eq!(meter.registers.len(), 1);
173    }
174
175    #[test]
176    fn test_serialize() {
177        let meter = Meter {
178            meta: Bo4eMeta::with_type("Zaehler"),
179            meter_number: Some("1EMH0012345678".to_string()),
180            division: Some(Division::Electricity),
181            ..Default::default()
182        };
183
184        let json = serde_json::to_string(&meter).unwrap();
185        assert!(json.contains(r#""_typ":"Zaehler""#));
186        assert!(json.contains(r#""meterNumber":"1EMH0012345678""#));
187    }
188
189    #[test]
190    fn test_roundtrip() {
191        let meter = Meter {
192            meta: Bo4eMeta::with_type("Zaehler"),
193            meter_number: Some("TEST123".to_string()),
194            division: Some(Division::Electricity),
195            meter_type: Some(MeterType::ModernMeasuringDevice),
196            meter_size: Some(MeterSize::G4),
197            manufacturer: Some("Acme Corp".to_string()),
198            manufacturing_year: Some(2023),
199            ..Default::default()
200        };
201
202        let json = serde_json::to_string(&meter).unwrap();
203        let parsed: Meter = serde_json::from_str(&json).unwrap();
204        assert_eq!(meter, parsed);
205    }
206
207    #[test]
208    fn test_bo4e_object_impl() {
209        assert_eq!(Meter::type_name_german(), "Zaehler");
210        assert_eq!(Meter::type_name_english(), "Meter");
211    }
212}