1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use uuid::Uuid;
4
5#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
8pub struct IoTReading {
9 pub id: Uuid,
10 pub building_id: Uuid,
11 pub device_type: DeviceType,
12 pub metric_type: MetricType,
13 pub value: f64,
14 pub unit: String,
15 pub timestamp: DateTime<Utc>,
16 pub source: String, pub metadata: Option<serde_json::Value>, pub created_at: DateTime<Utc>,
19}
20
21impl IoTReading {
22 pub fn new(
24 building_id: Uuid,
25 device_type: DeviceType,
26 metric_type: MetricType,
27 value: f64,
28 unit: String,
29 timestamp: DateTime<Utc>,
30 source: String,
31 ) -> Result<Self, String> {
32 Self::validate_unit(&metric_type, &unit)?;
34
35 Self::validate_value(&metric_type, value, &unit)?;
37
38 if source.trim().is_empty() {
40 return Err("Source cannot be empty".to_string());
41 }
42
43 if timestamp > Utc::now() {
45 return Err("Timestamp cannot be in the future".to_string());
46 }
47
48 Ok(Self {
49 id: Uuid::new_v4(),
50 building_id,
51 device_type,
52 metric_type,
53 value,
54 unit,
55 timestamp,
56 source,
57 metadata: None,
58 created_at: Utc::now(),
59 })
60 }
61
62 pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self {
64 self.metadata = Some(metadata);
65 self
66 }
67
68 fn validate_value(metric_type: &MetricType, value: f64, unit: &str) -> Result<(), String> {
70 match metric_type {
71 MetricType::ElectricityConsumption
72 | MetricType::WaterConsumption
73 | MetricType::GasConsumption => {
74 if value < 0.0 {
75 return Err(format!("Consumption value cannot be negative: {}", value));
76 }
77 if value > 1_000_000.0 {
78 return Err(format!("Consumption value too large (max 1M): {}", value));
79 }
80 }
81 MetricType::Temperature => {
82 let normalized = match unit {
84 "F" | "°F" => (value - 32.0) * 5.0 / 9.0,
85 "K" => value - 273.15,
86 _ => value, };
88
89 if normalized < -40.0 || normalized > 80.0 {
90 return Err(format!(
91 "Temperature value out of range (-40 to +80°C): {}",
92 value
93 ));
94 }
95 }
96 MetricType::Humidity => {
97 if value < 0.0 || value > 100.0 {
98 return Err(format!("Humidity value out of range (0-100%): {}", value));
99 }
100 }
101 MetricType::Power => {
102 if value < 0.0 {
103 return Err(format!("Power value cannot be negative: {}", value));
104 }
105 if value > 100_000.0 {
106 return Err(format!("Power value too large (max 100kW): {}", value));
107 }
108 }
109 MetricType::Voltage => {
110 if value < 0.0 || value > 500.0 {
111 return Err(format!("Voltage value out of range (0-500V): {}", value));
112 }
113 }
114 }
115 Ok(())
116 }
117
118 fn validate_unit(metric_type: &MetricType, unit: &str) -> Result<(), String> {
120 let valid_units = match metric_type {
121 MetricType::ElectricityConsumption => vec!["kWh", "Wh", "MWh"],
122 MetricType::WaterConsumption => vec!["m3", "L", "gal"],
123 MetricType::GasConsumption => vec!["m3", "kWh"],
124 MetricType::Temperature => vec!["C", "°C", "F", "°F", "K"],
125 MetricType::Humidity => vec!["%", "percent"],
126 MetricType::Power => vec!["W", "kW", "MW"],
127 MetricType::Voltage => vec!["V", "kV"],
128 };
129
130 if !valid_units.contains(&unit) {
131 return Err(format!(
132 "Invalid unit '{}' for metric type '{:?}'. Valid units: {:?}",
133 unit, metric_type, valid_units
134 ));
135 }
136
137 Ok(())
138 }
139
140 pub fn is_anomalous(&self, average_value: f64, threshold_percentage: f64) -> bool {
143 if average_value == 0.0 {
144 return false;
145 }
146
147 let deviation_percentage = ((self.value - average_value) / average_value).abs() * 100.0;
148 deviation_percentage > threshold_percentage
149 }
150
151 pub fn normalized_value(&self) -> f64 {
153 match &self.metric_type {
154 MetricType::ElectricityConsumption => match self.unit.as_str() {
155 "Wh" => self.value / 1000.0,
156 "MWh" => self.value * 1000.0,
157 _ => self.value, },
159 MetricType::Temperature => match self.unit.as_str() {
160 "F" | "°F" => (self.value - 32.0) * 5.0 / 9.0, "K" => self.value - 273.15, _ => self.value, },
164 _ => self.value, }
166 }
167}
168
169#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
171#[serde(rename_all = "snake_case")]
172pub enum DeviceType {
173 ElectricityMeter, WaterMeter, GasMeter, TemperatureSensor, HumiditySensor, PowerMeter, }
180
181impl std::fmt::Display for DeviceType {
182 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
183 match self {
184 DeviceType::ElectricityMeter => write!(f, "ElectricityMeter"),
185 DeviceType::WaterMeter => write!(f, "WaterMeter"),
186 DeviceType::GasMeter => write!(f, "GasMeter"),
187 DeviceType::TemperatureSensor => write!(f, "TemperatureSensor"),
188 DeviceType::HumiditySensor => write!(f, "HumiditySensor"),
189 DeviceType::PowerMeter => write!(f, "PowerMeter"),
190 }
191 }
192}
193
194impl std::str::FromStr for DeviceType {
195 type Err = String;
196 fn from_str(s: &str) -> Result<Self, Self::Err> {
197 match s {
198 "ElectricityMeter" => Ok(DeviceType::ElectricityMeter),
199 "WaterMeter" => Ok(DeviceType::WaterMeter),
200 "GasMeter" => Ok(DeviceType::GasMeter),
201 "TemperatureSensor" => Ok(DeviceType::TemperatureSensor),
202 "HumiditySensor" => Ok(DeviceType::HumiditySensor),
203 "PowerMeter" => Ok(DeviceType::PowerMeter),
204 _ => Err(format!("Invalid DeviceType: {}", s)),
205 }
206 }
207}
208
209#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
211#[serde(rename_all = "snake_case")]
212pub enum MetricType {
213 ElectricityConsumption, WaterConsumption, GasConsumption, Temperature, Humidity, Power, Voltage, }
221
222impl std::fmt::Display for MetricType {
223 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
224 match self {
225 MetricType::ElectricityConsumption => write!(f, "ElectricityConsumption"),
226 MetricType::WaterConsumption => write!(f, "WaterConsumption"),
227 MetricType::GasConsumption => write!(f, "GasConsumption"),
228 MetricType::Temperature => write!(f, "Temperature"),
229 MetricType::Humidity => write!(f, "Humidity"),
230 MetricType::Power => write!(f, "Power"),
231 MetricType::Voltage => write!(f, "Voltage"),
232 }
233 }
234}
235
236impl std::str::FromStr for MetricType {
237 type Err = String;
238 fn from_str(s: &str) -> Result<Self, Self::Err> {
239 match s {
240 "ElectricityConsumption" => Ok(MetricType::ElectricityConsumption),
241 "WaterConsumption" => Ok(MetricType::WaterConsumption),
242 "GasConsumption" => Ok(MetricType::GasConsumption),
243 "Temperature" => Ok(MetricType::Temperature),
244 "Humidity" => Ok(MetricType::Humidity),
245 "Power" => Ok(MetricType::Power),
246 "Voltage" => Ok(MetricType::Voltage),
247 _ => Err(format!("Invalid MetricType: {}", s)),
248 }
249 }
250}
251
252#[cfg(test)]
253mod tests {
254 use super::*;
255
256 fn sample_building_id() -> Uuid {
257 Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap()
258 }
259
260 #[test]
261 fn test_create_iot_reading_success() {
262 let reading = IoTReading::new(
263 sample_building_id(),
264 DeviceType::ElectricityMeter,
265 MetricType::ElectricityConsumption,
266 123.45,
267 "kWh".to_string(),
268 Utc::now() - chrono::Duration::hours(1),
269 "linky_ores".to_string(),
270 );
271
272 assert!(reading.is_ok());
273 let r = reading.unwrap();
274 assert_eq!(r.building_id, sample_building_id());
275 assert_eq!(r.device_type, DeviceType::ElectricityMeter);
276 assert_eq!(r.metric_type, MetricType::ElectricityConsumption);
277 assert_eq!(r.value, 123.45);
278 assert_eq!(r.unit, "kWh");
279 assert_eq!(r.source, "linky_ores");
280 }
281
282 #[test]
283 fn test_validate_electricity_consumption_negative() {
284 let reading = IoTReading::new(
285 sample_building_id(),
286 DeviceType::ElectricityMeter,
287 MetricType::ElectricityConsumption,
288 -10.0,
289 "kWh".to_string(),
290 Utc::now(),
291 "linky_ores".to_string(),
292 );
293
294 assert!(reading.is_err());
295 assert!(reading.unwrap_err().contains("cannot be negative"));
296 }
297
298 #[test]
299 fn test_validate_temperature_out_of_range() {
300 let reading = IoTReading::new(
301 sample_building_id(),
302 DeviceType::TemperatureSensor,
303 MetricType::Temperature,
304 -50.0,
305 "°C".to_string(),
306 Utc::now(),
307 "netatmo".to_string(),
308 );
309
310 assert!(reading.is_err());
311 assert!(reading.unwrap_err().contains("out of range"));
312
313 let reading2 = IoTReading::new(
314 sample_building_id(),
315 DeviceType::TemperatureSensor,
316 MetricType::Temperature,
317 100.0,
318 "°C".to_string(),
319 Utc::now(),
320 "netatmo".to_string(),
321 );
322
323 assert!(reading2.is_err());
324 }
325
326 #[test]
327 fn test_validate_humidity_range() {
328 let reading = IoTReading::new(
329 sample_building_id(),
330 DeviceType::HumiditySensor,
331 MetricType::Humidity,
332 50.0,
333 "%".to_string(),
334 Utc::now(),
335 "netatmo".to_string(),
336 );
337
338 assert!(reading.is_ok());
339
340 let reading2 = IoTReading::new(
341 sample_building_id(),
342 DeviceType::HumiditySensor,
343 MetricType::Humidity,
344 150.0,
345 "%".to_string(),
346 Utc::now(),
347 "netatmo".to_string(),
348 );
349
350 assert!(reading2.is_err());
351 }
352
353 #[test]
354 fn test_validate_invalid_unit() {
355 let reading = IoTReading::new(
356 sample_building_id(),
357 DeviceType::ElectricityMeter,
358 MetricType::ElectricityConsumption,
359 100.0,
360 "gallons".to_string(), Utc::now(),
362 "linky_ores".to_string(),
363 );
364
365 assert!(reading.is_err());
366 assert!(reading.unwrap_err().contains("Invalid unit"));
367 }
368
369 #[test]
370 fn test_validate_future_timestamp() {
371 let reading = IoTReading::new(
372 sample_building_id(),
373 DeviceType::ElectricityMeter,
374 MetricType::ElectricityConsumption,
375 100.0,
376 "kWh".to_string(),
377 Utc::now() + chrono::Duration::hours(1),
378 "linky_ores".to_string(),
379 );
380
381 assert!(reading.is_err());
382 assert!(reading.unwrap_err().contains("cannot be in the future"));
383 }
384
385 #[test]
386 fn test_validate_empty_source() {
387 let reading = IoTReading::new(
388 sample_building_id(),
389 DeviceType::ElectricityMeter,
390 MetricType::ElectricityConsumption,
391 100.0,
392 "kWh".to_string(),
393 Utc::now(),
394 "".to_string(),
395 );
396
397 assert!(reading.is_err());
398 assert!(reading.unwrap_err().contains("Source cannot be empty"));
399 }
400
401 #[test]
402 fn test_with_metadata() {
403 let reading = IoTReading::new(
404 sample_building_id(),
405 DeviceType::ElectricityMeter,
406 MetricType::ElectricityConsumption,
407 100.0,
408 "kWh".to_string(),
409 Utc::now(),
410 "linky_ores".to_string(),
411 )
412 .unwrap()
413 .with_metadata(serde_json::json!({"granularity": "30min", "quality": "good"}));
414
415 assert!(reading.metadata.is_some());
416 assert_eq!(reading.metadata.unwrap()["granularity"], "30min");
417 }
418
419 #[test]
420 fn test_is_anomalous() {
421 let reading = IoTReading::new(
422 sample_building_id(),
423 DeviceType::ElectricityMeter,
424 MetricType::ElectricityConsumption,
425 150.0,
426 "kWh".to_string(),
427 Utc::now(),
428 "linky_ores".to_string(),
429 )
430 .unwrap();
431
432 assert!(reading.is_anomalous(100.0, 40.0)); assert!(!reading.is_anomalous(100.0, 60.0)); }
436
437 #[test]
438 fn test_normalized_value_electricity() {
439 let reading_wh = IoTReading::new(
440 sample_building_id(),
441 DeviceType::ElectricityMeter,
442 MetricType::ElectricityConsumption,
443 5000.0,
444 "Wh".to_string(),
445 Utc::now(),
446 "linky_ores".to_string(),
447 )
448 .unwrap();
449
450 assert_eq!(reading_wh.normalized_value(), 5.0); let reading_mwh = IoTReading::new(
453 sample_building_id(),
454 DeviceType::ElectricityMeter,
455 MetricType::ElectricityConsumption,
456 0.5,
457 "MWh".to_string(),
458 Utc::now(),
459 "linky_ores".to_string(),
460 )
461 .unwrap();
462
463 assert_eq!(reading_mwh.normalized_value(), 500.0); }
465
466 #[test]
467 fn test_normalized_value_temperature() {
468 let reading_f = IoTReading::new(
469 sample_building_id(),
470 DeviceType::TemperatureSensor,
471 MetricType::Temperature,
472 32.0,
473 "°F".to_string(),
474 Utc::now(),
475 "netatmo".to_string(),
476 )
477 .unwrap();
478
479 assert!((reading_f.normalized_value() - 0.0).abs() < 0.01);
481
482 let reading_k = IoTReading::new(
483 sample_building_id(),
484 DeviceType::TemperatureSensor,
485 MetricType::Temperature,
486 273.15,
487 "K".to_string(),
488 Utc::now(),
489 "sensor".to_string(),
490 )
491 .unwrap();
492
493 assert!((reading_k.normalized_value() - 0.0).abs() < 0.01);
495 }
496}