1use serde::{Deserialize, Serialize};
13use time::OffsetDateTime;
14
15use aranet_types::{CurrentReading, DeviceType, HistoryRecord, Status};
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct StoredDevice {
38 pub id: String,
40 pub name: Option<String>,
42 pub device_type: Option<DeviceType>,
44 pub serial: Option<String>,
46 pub firmware: Option<String>,
48 pub hardware: Option<String>,
50 #[serde(with = "time::serde::rfc3339")]
52 pub first_seen: OffsetDateTime,
53 #[serde(with = "time::serde::rfc3339")]
55 pub last_seen: OffsetDateTime,
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct StoredReading {
74 pub id: i64,
76 pub device_id: String,
78 #[serde(with = "time::serde::rfc3339")]
80 pub captured_at: OffsetDateTime,
81 pub co2: u16,
83 pub temperature: f32,
85 pub pressure: f32,
87 pub humidity: u8,
89 pub battery: u8,
91 pub status: Status,
93 pub radon: Option<u32>,
95 pub radiation_rate: Option<f32>,
97 pub radiation_total: Option<f64>,
99 pub radon_avg_24h: Option<u32>,
101 pub radon_avg_7d: Option<u32>,
103 pub radon_avg_30d: Option<u32>,
105}
106
107impl StoredReading {
108 pub fn from_reading_with_id(device_id: &str, reading: &CurrentReading, id: i64) -> Self {
110 Self {
111 id,
112 device_id: device_id.to_string(),
113 captured_at: reading.captured_at.unwrap_or_else(OffsetDateTime::now_utc),
114 co2: reading.co2,
115 temperature: reading.temperature,
116 pressure: reading.pressure,
117 humidity: reading.humidity,
118 battery: reading.battery,
119 status: reading.status,
120 radon: reading.radon,
121 radiation_rate: reading.radiation_rate,
122 radiation_total: reading.radiation_total,
123 radon_avg_24h: reading.radon_avg_24h,
124 radon_avg_7d: reading.radon_avg_7d,
125 radon_avg_30d: reading.radon_avg_30d,
126 }
127 }
128
129 pub fn from_reading(device_id: &str, reading: &CurrentReading) -> Self {
139 Self::from_reading_with_id(device_id, reading, 0)
140 }
141
142 pub fn to_reading(&self) -> CurrentReading {
149 CurrentReading {
150 co2: self.co2,
151 temperature: self.temperature,
152 pressure: self.pressure,
153 humidity: self.humidity,
154 battery: self.battery,
155 status: self.status,
156 interval: 0,
157 age: 0,
158 captured_at: Some(self.captured_at),
159 radon: self.radon,
160 radiation_rate: self.radiation_rate,
161 radiation_total: self.radiation_total,
162 radon_avg_24h: self.radon_avg_24h,
163 radon_avg_7d: self.radon_avg_7d,
164 radon_avg_30d: self.radon_avg_30d,
165 }
166 }
167}
168
169#[derive(Debug, Clone, Serialize, Deserialize)]
179pub struct StoredHistoryRecord {
180 pub id: i64,
182 pub device_id: String,
184 #[serde(with = "time::serde::rfc3339")]
186 pub timestamp: OffsetDateTime,
187 #[serde(with = "time::serde::rfc3339")]
189 pub synced_at: OffsetDateTime,
190 pub co2: u16,
192 pub temperature: f32,
194 pub pressure: f32,
196 pub humidity: u8,
198 pub radon: Option<u32>,
200 pub radiation_rate: Option<f32>,
202 pub radiation_total: Option<f64>,
204}
205
206impl StoredHistoryRecord {
207 pub fn from_history(device_id: &str, record: &HistoryRecord) -> Self {
217 Self {
218 id: 0,
219 device_id: device_id.to_string(),
220 timestamp: record.timestamp,
221 synced_at: OffsetDateTime::now_utc(),
222 co2: record.co2,
223 temperature: record.temperature,
224 pressure: record.pressure,
225 humidity: record.humidity,
226 radon: record.radon,
227 radiation_rate: record.radiation_rate,
228 radiation_total: record.radiation_total,
229 }
230 }
231
232 pub fn to_history(&self) -> HistoryRecord {
237 HistoryRecord {
238 timestamp: self.timestamp,
239 co2: self.co2,
240 temperature: self.temperature,
241 pressure: self.pressure,
242 humidity: self.humidity,
243 radon: self.radon,
244 radiation_rate: self.radiation_rate,
245 radiation_total: self.radiation_total,
246 }
247 }
248}
249
250#[derive(Debug, Clone, Serialize, Deserialize)]
284pub struct SyncState {
285 pub device_id: String,
287 pub last_history_index: Option<u16>,
289 pub total_readings: Option<u16>,
291 #[serde(with = "time::serde::rfc3339::option")]
293 pub last_sync_at: Option<OffsetDateTime>,
294}
295
296#[cfg(test)]
297mod tests {
298 use super::*;
299 use time::macros::datetime;
300
301 fn create_current_reading() -> CurrentReading {
304 CurrentReading {
305 co2: 850,
306 temperature: 23.5,
307 pressure: 1015.25,
308 humidity: 48,
309 battery: 75,
310 status: Status::Green,
311 interval: 60,
312 age: 45,
313 captured_at: Some(datetime!(2024-06-15 14:30:00 UTC)),
314 radon: None,
315 radiation_rate: None,
316 radiation_total: None,
317 radon_avg_24h: None,
318 radon_avg_7d: None,
319 radon_avg_30d: None,
320 }
321 }
322
323 fn create_current_reading_radon() -> CurrentReading {
324 CurrentReading {
325 co2: 0,
326 temperature: 21.0,
327 pressure: 1013.0,
328 humidity: 55,
329 battery: 90,
330 status: Status::Yellow,
331 interval: 3600,
332 age: 1800,
333 captured_at: Some(datetime!(2024-06-15 12:00:00 UTC)),
334 radon: Some(150),
335 radiation_rate: None,
336 radiation_total: None,
337 radon_avg_24h: Some(145),
338 radon_avg_7d: Some(140),
339 radon_avg_30d: Some(138),
340 }
341 }
342
343 fn create_current_reading_radiation() -> CurrentReading {
344 CurrentReading {
345 co2: 0,
346 temperature: 20.0,
347 pressure: 1010.0,
348 humidity: 50,
349 battery: 80,
350 status: Status::Green,
351 interval: 60,
352 age: 30,
353 captured_at: Some(datetime!(2024-06-15 16:00:00 UTC)),
354 radon: None,
355 radiation_rate: Some(0.12),
356 radiation_total: Some(0.0025),
357 radon_avg_24h: None,
358 radon_avg_7d: None,
359 radon_avg_30d: None,
360 }
361 }
362
363 #[test]
364 fn test_stored_reading_from_reading_basic() {
365 let reading = create_current_reading();
366 let stored = StoredReading::from_reading("aranet4-abc", &reading);
367
368 assert_eq!(stored.id, 0); assert_eq!(stored.device_id, "aranet4-abc");
370 assert_eq!(stored.co2, 850);
371 assert_eq!(stored.temperature, 23.5);
372 assert_eq!(stored.pressure, 1015.25);
373 assert_eq!(stored.humidity, 48);
374 assert_eq!(stored.battery, 75);
375 assert_eq!(stored.status, Status::Green);
376 assert_eq!(stored.captured_at, datetime!(2024-06-15 14:30:00 UTC));
377 assert!(stored.radon.is_none());
378 assert!(stored.radiation_rate.is_none());
379 assert!(stored.radiation_total.is_none());
380 }
381
382 #[test]
383 fn test_stored_reading_from_reading_with_radon() {
384 let reading = create_current_reading_radon();
385 let stored = StoredReading::from_reading("aranet-rn", &reading);
386
387 assert_eq!(stored.radon, Some(150));
388 assert!(stored.radiation_rate.is_none());
389 assert!(stored.radiation_total.is_none());
390 }
391
392 #[test]
393 fn test_stored_reading_from_reading_with_radiation() {
394 let reading = create_current_reading_radiation();
395 let stored = StoredReading::from_reading("aranet-rad", &reading);
396
397 assert!(stored.radon.is_none());
398 assert_eq!(stored.radiation_rate, Some(0.12));
399 assert_eq!(stored.radiation_total, Some(0.0025));
400 }
401
402 #[test]
403 fn test_stored_reading_from_reading_without_captured_at() {
404 let mut reading = create_current_reading();
405 reading.captured_at = None;
406
407 let before = OffsetDateTime::now_utc();
408 let stored = StoredReading::from_reading("device", &reading);
409 let after = OffsetDateTime::now_utc();
410
411 assert!(stored.captured_at >= before);
413 assert!(stored.captured_at <= after);
414 }
415
416 #[test]
417 fn test_stored_reading_to_reading_roundtrip() {
418 let original = create_current_reading();
419 let stored = StoredReading::from_reading("test-device", &original);
420 let converted = stored.to_reading();
421
422 assert_eq!(converted.co2, original.co2);
423 assert_eq!(converted.temperature, original.temperature);
424 assert_eq!(converted.pressure, original.pressure);
425 assert_eq!(converted.humidity, original.humidity);
426 assert_eq!(converted.battery, original.battery);
427 assert_eq!(converted.status, original.status);
428 assert_eq!(converted.captured_at, original.captured_at);
429 assert_eq!(converted.radon, original.radon);
430 assert_eq!(converted.radiation_rate, original.radiation_rate);
431 assert_eq!(converted.radiation_total, original.radiation_total);
432 }
433
434 #[test]
435 fn test_stored_reading_to_reading_sets_defaults() {
436 let reading = create_current_reading();
437 let stored = StoredReading::from_reading("test", &reading);
438 let converted = stored.to_reading();
439
440 assert_eq!(converted.interval, 0);
442 assert_eq!(converted.age, 0);
443 }
444
445 #[test]
446 fn test_stored_reading_to_reading_with_radon() {
447 let original = create_current_reading_radon();
448 let stored = StoredReading::from_reading("radon-device", &original);
449 let converted = stored.to_reading();
450
451 assert_eq!(converted.radon, Some(150));
452 assert_eq!(converted.radon_avg_24h, Some(145));
453 assert_eq!(converted.radon_avg_7d, Some(140));
454 assert_eq!(converted.radon_avg_30d, Some(138));
455 }
456
457 #[test]
458 fn test_stored_reading_all_status_values() {
459 for status in [Status::Green, Status::Yellow, Status::Red, Status::Error] {
460 let mut reading = create_current_reading();
461 reading.status = status;
462 let stored = StoredReading::from_reading("dev", &reading);
463 assert_eq!(stored.status, status);
464 }
465 }
466
467 #[test]
468 fn test_stored_reading_serialization() {
469 let reading = create_current_reading();
470 let stored = StoredReading::from_reading("test", &reading);
471
472 let json = serde_json::to_string(&stored).unwrap();
473 let deserialized: StoredReading = serde_json::from_str(&json).unwrap();
474
475 assert_eq!(deserialized.device_id, stored.device_id);
476 assert_eq!(deserialized.co2, stored.co2);
477 assert_eq!(deserialized.temperature, stored.temperature);
478 }
479
480 #[test]
481 fn test_stored_reading_clone() {
482 let reading = create_current_reading();
483 let stored = StoredReading::from_reading("test", &reading);
484 let cloned = stored.clone();
485
486 assert_eq!(cloned.device_id, stored.device_id);
487 assert_eq!(cloned.co2, stored.co2);
488 }
489
490 fn create_history_record() -> HistoryRecord {
493 HistoryRecord {
494 timestamp: datetime!(2024-05-20 10:00:00 UTC),
495 co2: 720,
496 temperature: 21.5,
497 pressure: 1018.5,
498 humidity: 52,
499 radon: None,
500 radiation_rate: None,
501 radiation_total: None,
502 }
503 }
504
505 fn create_history_record_radon() -> HistoryRecord {
506 HistoryRecord {
507 timestamp: datetime!(2024-05-20 11:00:00 UTC),
508 co2: 0,
509 temperature: 20.0,
510 pressure: 1012.0,
511 humidity: 60,
512 radon: Some(180),
513 radiation_rate: None,
514 radiation_total: None,
515 }
516 }
517
518 fn create_history_record_radiation() -> HistoryRecord {
519 HistoryRecord {
520 timestamp: datetime!(2024-05-20 12:00:00 UTC),
521 co2: 0,
522 temperature: 19.5,
523 pressure: 1011.0,
524 humidity: 58,
525 radon: None,
526 radiation_rate: Some(0.15),
527 radiation_total: Some(0.003),
528 }
529 }
530
531 #[test]
532 fn test_stored_history_record_from_history_basic() {
533 let record = create_history_record();
534 let stored = StoredHistoryRecord::from_history("device-123", &record);
535
536 assert_eq!(stored.id, 0);
537 assert_eq!(stored.device_id, "device-123");
538 assert_eq!(stored.timestamp, datetime!(2024-05-20 10:00:00 UTC));
539 assert_eq!(stored.co2, 720);
540 assert_eq!(stored.temperature, 21.5);
541 assert_eq!(stored.pressure, 1018.5);
542 assert_eq!(stored.humidity, 52);
543 assert!(stored.radon.is_none());
544 assert!(stored.radiation_rate.is_none());
545 assert!(stored.radiation_total.is_none());
546 }
547
548 #[test]
549 fn test_stored_history_record_from_history_sets_synced_at() {
550 let record = create_history_record();
551
552 let before = OffsetDateTime::now_utc();
553 let stored = StoredHistoryRecord::from_history("device", &record);
554 let after = OffsetDateTime::now_utc();
555
556 assert!(stored.synced_at >= before);
557 assert!(stored.synced_at <= after);
558 }
559
560 #[test]
561 fn test_stored_history_record_from_history_with_radon() {
562 let record = create_history_record_radon();
563 let stored = StoredHistoryRecord::from_history("radon-dev", &record);
564
565 assert_eq!(stored.radon, Some(180));
566 assert!(stored.radiation_rate.is_none());
567 }
568
569 #[test]
570 fn test_stored_history_record_from_history_with_radiation() {
571 let record = create_history_record_radiation();
572 let stored = StoredHistoryRecord::from_history("rad-dev", &record);
573
574 assert!(stored.radon.is_none());
575 assert_eq!(stored.radiation_rate, Some(0.15));
576 assert_eq!(stored.radiation_total, Some(0.003));
577 }
578
579 #[test]
580 fn test_stored_history_record_to_history_roundtrip() {
581 let original = create_history_record();
582 let stored = StoredHistoryRecord::from_history("test", &original);
583 let converted = stored.to_history();
584
585 assert_eq!(converted.timestamp, original.timestamp);
586 assert_eq!(converted.co2, original.co2);
587 assert_eq!(converted.temperature, original.temperature);
588 assert_eq!(converted.pressure, original.pressure);
589 assert_eq!(converted.humidity, original.humidity);
590 assert_eq!(converted.radon, original.radon);
591 assert_eq!(converted.radiation_rate, original.radiation_rate);
592 assert_eq!(converted.radiation_total, original.radiation_total);
593 }
594
595 #[test]
596 fn test_stored_history_record_to_history_radon_roundtrip() {
597 let original = create_history_record_radon();
598 let stored = StoredHistoryRecord::from_history("test", &original);
599 let converted = stored.to_history();
600
601 assert_eq!(converted.radon, Some(180));
602 }
603
604 #[test]
605 fn test_stored_history_record_to_history_radiation_roundtrip() {
606 let original = create_history_record_radiation();
607 let stored = StoredHistoryRecord::from_history("test", &original);
608 let converted = stored.to_history();
609
610 assert_eq!(converted.radiation_rate, Some(0.15));
611 assert_eq!(converted.radiation_total, Some(0.003));
612 }
613
614 #[test]
615 fn test_stored_history_record_serialization() {
616 let record = create_history_record();
617 let stored = StoredHistoryRecord::from_history("test", &record);
618
619 let json = serde_json::to_string(&stored).unwrap();
620 let deserialized: StoredHistoryRecord = serde_json::from_str(&json).unwrap();
621
622 assert_eq!(deserialized.device_id, stored.device_id);
623 assert_eq!(deserialized.timestamp, stored.timestamp);
624 assert_eq!(deserialized.co2, stored.co2);
625 }
626
627 #[test]
628 fn test_stored_history_record_clone() {
629 let record = create_history_record();
630 let stored = StoredHistoryRecord::from_history("test", &record);
631 let cloned = stored.clone();
632
633 assert_eq!(cloned.device_id, stored.device_id);
634 assert_eq!(cloned.timestamp, stored.timestamp);
635 }
636
637 #[test]
640 fn test_stored_device_serialization() {
641 let device = StoredDevice {
642 id: "aranet4-xyz".to_string(),
643 name: Some("Living Room".to_string()),
644 device_type: Some(DeviceType::Aranet4),
645 serial: Some("1234567".to_string()),
646 firmware: Some("v1.2.0".to_string()),
647 hardware: Some("1.0".to_string()),
648 first_seen: datetime!(2024-01-01 00:00:00 UTC),
649 last_seen: datetime!(2024-06-15 12:00:00 UTC),
650 };
651
652 let json = serde_json::to_string(&device).unwrap();
653 let deserialized: StoredDevice = serde_json::from_str(&json).unwrap();
654
655 assert_eq!(deserialized.id, device.id);
656 assert_eq!(deserialized.name, device.name);
657 assert_eq!(deserialized.device_type, device.device_type);
658 assert_eq!(deserialized.serial, device.serial);
659 assert_eq!(deserialized.firmware, device.firmware);
660 assert_eq!(deserialized.first_seen, device.first_seen);
661 assert_eq!(deserialized.last_seen, device.last_seen);
662 }
663
664 #[test]
665 fn test_stored_device_all_device_types() {
666 for device_type in [
667 DeviceType::Aranet4,
668 DeviceType::Aranet2,
669 DeviceType::AranetRadon,
670 DeviceType::AranetRadiation,
671 ] {
672 let device = StoredDevice {
673 id: "test".to_string(),
674 name: None,
675 device_type: Some(device_type),
676 serial: None,
677 firmware: None,
678 hardware: None,
679 first_seen: OffsetDateTime::now_utc(),
680 last_seen: OffsetDateTime::now_utc(),
681 };
682
683 let json = serde_json::to_string(&device).unwrap();
684 let deserialized: StoredDevice = serde_json::from_str(&json).unwrap();
685 assert_eq!(deserialized.device_type, Some(device_type));
686 }
687 }
688
689 #[test]
690 fn test_stored_device_optional_fields() {
691 let device = StoredDevice {
692 id: "minimal-device".to_string(),
693 name: None,
694 device_type: None,
695 serial: None,
696 firmware: None,
697 hardware: None,
698 first_seen: datetime!(2024-06-01 00:00:00 UTC),
699 last_seen: datetime!(2024-06-01 00:00:00 UTC),
700 };
701
702 assert!(device.name.is_none());
703 assert!(device.device_type.is_none());
704 assert!(device.serial.is_none());
705 assert!(device.firmware.is_none());
706 assert!(device.hardware.is_none());
707 }
708
709 #[test]
710 fn test_stored_device_clone() {
711 let device = StoredDevice {
712 id: "clone-test".to_string(),
713 name: Some("Test".to_string()),
714 device_type: Some(DeviceType::Aranet4),
715 serial: Some("123".to_string()),
716 firmware: Some("v1.0".to_string()),
717 hardware: Some("1.0".to_string()),
718 first_seen: OffsetDateTime::now_utc(),
719 last_seen: OffsetDateTime::now_utc(),
720 };
721
722 let cloned = device.clone();
723 assert_eq!(cloned.id, device.id);
724 assert_eq!(cloned.name, device.name);
725 }
726
727 #[test]
730 fn test_sync_state_serialization() {
731 let state = SyncState {
732 device_id: "sync-device".to_string(),
733 last_history_index: Some(500),
734 total_readings: Some(500),
735 last_sync_at: Some(datetime!(2024-06-15 18:00:00 UTC)),
736 };
737
738 let json = serde_json::to_string(&state).unwrap();
739 let deserialized: SyncState = serde_json::from_str(&json).unwrap();
740
741 assert_eq!(deserialized.device_id, state.device_id);
742 assert_eq!(deserialized.last_history_index, state.last_history_index);
743 assert_eq!(deserialized.total_readings, state.total_readings);
744 assert_eq!(deserialized.last_sync_at, state.last_sync_at);
745 }
746
747 #[test]
748 fn test_sync_state_with_none_values() {
749 let state = SyncState {
750 device_id: "new-device".to_string(),
751 last_history_index: None,
752 total_readings: None,
753 last_sync_at: None,
754 };
755
756 let json = serde_json::to_string(&state).unwrap();
757 let deserialized: SyncState = serde_json::from_str(&json).unwrap();
758
759 assert!(deserialized.last_history_index.is_none());
760 assert!(deserialized.total_readings.is_none());
761 assert!(deserialized.last_sync_at.is_none());
762 }
763
764 #[test]
765 fn test_sync_state_clone() {
766 let state = SyncState {
767 device_id: "clone-test".to_string(),
768 last_history_index: Some(100),
769 total_readings: Some(100),
770 last_sync_at: Some(OffsetDateTime::now_utc()),
771 };
772
773 let cloned = state.clone();
774 assert_eq!(cloned.device_id, state.device_id);
775 assert_eq!(cloned.last_history_index, state.last_history_index);
776 }
777
778 #[test]
779 fn test_sync_state_debug() {
780 let state = SyncState {
781 device_id: "debug-test".to_string(),
782 last_history_index: Some(42),
783 total_readings: Some(42),
784 last_sync_at: None,
785 };
786
787 let debug_str = format!("{:?}", state);
788 assert!(debug_str.contains("SyncState"));
789 assert!(debug_str.contains("debug-test"));
790 assert!(debug_str.contains("42"));
791 }
792
793 #[test]
796 fn test_stored_reading_extreme_values() {
797 let reading = CurrentReading {
798 co2: u16::MAX,
799 temperature: f32::MAX,
800 pressure: f32::MAX,
801 humidity: u8::MAX,
802 battery: u8::MAX,
803 status: Status::Error,
804 interval: u16::MAX,
805 age: u16::MAX,
806 captured_at: Some(OffsetDateTime::UNIX_EPOCH),
807 radon: Some(u32::MAX),
808 radiation_rate: Some(f32::MAX),
809 radiation_total: Some(f64::MAX),
810 radon_avg_24h: None,
811 radon_avg_7d: None,
812 radon_avg_30d: None,
813 };
814
815 let stored = StoredReading::from_reading("extreme", &reading);
816 let converted = stored.to_reading();
817
818 assert_eq!(converted.co2, u16::MAX);
819 assert_eq!(converted.humidity, u8::MAX);
820 assert_eq!(converted.battery, u8::MAX);
821 assert_eq!(converted.radon, Some(u32::MAX));
822 }
823
824 #[test]
825 fn test_stored_reading_zero_values() {
826 let reading = CurrentReading {
827 co2: 0,
828 temperature: 0.0,
829 pressure: 0.0,
830 humidity: 0,
831 battery: 0,
832 status: Status::Green,
833 interval: 0,
834 age: 0,
835 captured_at: Some(OffsetDateTime::UNIX_EPOCH),
836 radon: Some(0),
837 radiation_rate: Some(0.0),
838 radiation_total: Some(0.0),
839 radon_avg_24h: None,
840 radon_avg_7d: None,
841 radon_avg_30d: None,
842 };
843
844 let stored = StoredReading::from_reading("zero", &reading);
845 let converted = stored.to_reading();
846
847 assert_eq!(converted.co2, 0);
848 assert_eq!(converted.temperature, 0.0);
849 assert_eq!(converted.radon, Some(0));
850 }
851
852 #[test]
853 fn test_stored_history_record_zero_values() {
854 let record = HistoryRecord {
855 timestamp: OffsetDateTime::UNIX_EPOCH,
856 co2: 0,
857 temperature: 0.0,
858 pressure: 0.0,
859 humidity: 0,
860 radon: Some(0),
861 radiation_rate: Some(0.0),
862 radiation_total: Some(0.0),
863 };
864
865 let stored = StoredHistoryRecord::from_history("zero", &record);
866 let converted = stored.to_history();
867
868 assert_eq!(converted.co2, 0);
869 assert_eq!(converted.radon, Some(0));
870 }
871}