1#![deny(unsafe_code)]
2
3pub mod error;
24pub mod types;
25pub mod uuid;
26
27pub use error::{ParseError, ParseResult};
28pub use types::{
29 CurrentReading, CurrentReadingBuilder, DeviceInfo, DeviceInfoBuilder, DeviceType,
30 HistoryRecord, HistoryRecordBuilder, MIN_CURRENT_READING_BYTES, Status,
31};
32
33pub use uuid as ble;
36#[doc(hidden)]
37pub use uuid as uuids;
38
39#[cfg(test)]
88mod tests {
89 use super::*;
90
91 #[test]
96 fn test_parse_current_reading_from_valid_bytes() {
97 let bytes: [u8; 13] = [
107 0x20, 0x03, 0xC2, 0x01, 0x94, 0x27, 45, 85, 1, 0x2C, 0x01, 0x78, 0x00, ];
116
117 let reading = CurrentReading::from_bytes(&bytes).unwrap();
118
119 assert_eq!(reading.co2, 800);
120 assert!((reading.temperature - 22.5).abs() < 0.01);
121 assert!((reading.pressure - 1013.2).abs() < 0.1);
122 assert_eq!(reading.humidity, 45);
123 assert_eq!(reading.battery, 85);
124 assert_eq!(reading.status, Status::Green);
125 assert_eq!(reading.interval, 300);
126 assert_eq!(reading.age, 120);
127 }
128
129 #[test]
130 fn test_parse_current_reading_from_insufficient_bytes() {
131 let bytes: [u8; 10] = [0; 10]; let result = CurrentReading::from_bytes(&bytes);
134
135 assert!(result.is_err());
136 let err = result.unwrap_err();
137 assert_eq!(
138 err,
139 ParseError::InsufficientBytes {
140 expected: 13,
141 actual: 10
142 }
143 );
144 assert!(err.to_string().contains("expected 13"));
145 assert!(err.to_string().contains("got 10"));
146 }
147
148 #[test]
149 fn test_parse_current_reading_zero_bytes() {
150 let bytes: [u8; 0] = [];
151
152 let result = CurrentReading::from_bytes(&bytes);
153 assert!(result.is_err());
154 }
155
156 #[test]
157 fn test_parse_current_reading_all_zeros() {
158 let bytes: [u8; 13] = [0; 13];
159
160 let reading = CurrentReading::from_bytes(&bytes).unwrap();
161 assert_eq!(reading.co2, 0);
162 assert!((reading.temperature - 0.0).abs() < 0.01);
163 assert!((reading.pressure - 0.0).abs() < 0.1);
164 assert_eq!(reading.humidity, 0);
165 assert_eq!(reading.battery, 0);
166 assert_eq!(reading.status, Status::Error);
167 assert_eq!(reading.interval, 0);
168 assert_eq!(reading.age, 0);
169 }
170
171 #[test]
172 fn test_parse_current_reading_max_values() {
173 let bytes: [u8; 13] = [
174 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 3, 0xFF, 0xFF, 0xFF, 0xFF, ];
183
184 let reading = CurrentReading::from_bytes(&bytes).unwrap();
185 assert_eq!(reading.co2, 65535);
186 assert!((reading.temperature - (-0.05)).abs() < 0.01); assert!((reading.pressure - 6553.5).abs() < 0.1); assert_eq!(reading.humidity, 255);
189 assert_eq!(reading.battery, 255);
190 assert_eq!(reading.interval, 65535);
191 assert_eq!(reading.age, 65535);
192 }
193
194 #[test]
195 fn test_parse_current_reading_high_co2_red_status() {
196 let bytes: [u8; 13] = [
198 0xD0, 0x07, 0xC2, 0x01, 0x94, 0x27, 50, 80, 3, 0x2C, 0x01, 0x78, 0x00,
203 ];
204
205 let reading = CurrentReading::from_bytes(&bytes).unwrap();
206 assert_eq!(reading.co2, 2000);
207 assert_eq!(reading.status, Status::Red);
208 }
209
210 #[test]
211 fn test_parse_current_reading_moderate_co2_yellow_status() {
212 let bytes: [u8; 13] = [
214 0xB0, 0x04, 0xC2, 0x01, 0x94, 0x27, 50, 80, 2, 0x2C, 0x01, 0x78, 0x00,
217 ];
218
219 let reading = CurrentReading::from_bytes(&bytes).unwrap();
220 assert_eq!(reading.co2, 1200);
221 assert_eq!(reading.status, Status::Yellow);
222 }
223
224 #[test]
225 fn test_parse_current_reading_extra_bytes_ignored() {
226 let bytes: [u8; 16] = [
228 0x20, 0x03, 0xC2, 0x01, 0x94, 0x27, 45, 85, 1, 0x2C, 0x01, 0x78, 0x00, 0xAA, 0xBB, 0xCC,
229 ];
230
231 let reading = CurrentReading::from_bytes(&bytes).unwrap();
232 assert_eq!(reading.co2, 800);
233 }
234
235 #[test]
238 fn test_status_from_u8() {
239 assert_eq!(Status::from(0), Status::Error);
240 assert_eq!(Status::from(1), Status::Green);
241 assert_eq!(Status::from(2), Status::Yellow);
242 assert_eq!(Status::from(3), Status::Red);
243 assert_eq!(Status::from(4), Status::Error);
245 assert_eq!(Status::from(255), Status::Error);
246 }
247
248 #[test]
249 fn test_status_repr_values() {
250 assert_eq!(Status::Error as u8, 0);
251 assert_eq!(Status::Green as u8, 1);
252 assert_eq!(Status::Yellow as u8, 2);
253 assert_eq!(Status::Red as u8, 3);
254 }
255
256 #[test]
257 fn test_status_debug() {
258 assert_eq!(format!("{:?}", Status::Green), "Green");
259 assert_eq!(format!("{:?}", Status::Yellow), "Yellow");
260 assert_eq!(format!("{:?}", Status::Red), "Red");
261 assert_eq!(format!("{:?}", Status::Error), "Error");
262 }
263
264 #[test]
265 fn test_status_clone() {
266 let status = Status::Green;
267 let cloned = status;
269 assert_eq!(status, cloned);
270 }
271
272 #[test]
273 fn test_status_copy() {
274 let status = Status::Red;
275 let copied = status; assert_eq!(status, copied); }
278
279 #[test]
282 fn test_device_type_values() {
283 assert_eq!(DeviceType::Aranet4 as u8, 0xF1);
284 assert_eq!(DeviceType::Aranet2 as u8, 0xF2);
285 assert_eq!(DeviceType::AranetRadon as u8, 0xF3);
286 assert_eq!(DeviceType::AranetRadiation as u8, 0xF4);
287 }
288
289 #[test]
290 fn test_device_type_debug() {
291 assert_eq!(format!("{:?}", DeviceType::Aranet4), "Aranet4");
292 assert_eq!(format!("{:?}", DeviceType::Aranet2), "Aranet2");
293 assert_eq!(format!("{:?}", DeviceType::AranetRadon), "AranetRadon");
294 assert_eq!(
295 format!("{:?}", DeviceType::AranetRadiation),
296 "AranetRadiation"
297 );
298 }
299
300 #[test]
301 fn test_device_type_clone() {
302 let device_type = DeviceType::Aranet4;
303 let cloned = device_type;
305 assert_eq!(device_type, cloned);
306 }
307
308 #[test]
309 fn test_device_type_try_from_u8() {
310 assert_eq!(DeviceType::try_from(0xF1), Ok(DeviceType::Aranet4));
311 assert_eq!(DeviceType::try_from(0xF2), Ok(DeviceType::Aranet2));
312 assert_eq!(DeviceType::try_from(0xF3), Ok(DeviceType::AranetRadon));
313 assert_eq!(DeviceType::try_from(0xF4), Ok(DeviceType::AranetRadiation));
314 }
315
316 #[test]
317 fn test_device_type_try_from_u8_invalid() {
318 let result = DeviceType::try_from(0x00);
319 assert!(result.is_err());
320 assert_eq!(result.unwrap_err(), ParseError::UnknownDeviceType(0x00));
321
322 let result = DeviceType::try_from(0xFF);
323 assert!(result.is_err());
324 assert_eq!(result.unwrap_err(), ParseError::UnknownDeviceType(0xFF));
325 }
326
327 #[test]
328 fn test_device_type_display() {
329 assert_eq!(format!("{}", DeviceType::Aranet4), "Aranet4");
330 assert_eq!(format!("{}", DeviceType::Aranet2), "Aranet2");
331 assert_eq!(format!("{}", DeviceType::AranetRadon), "Aranet Radon");
332 assert_eq!(
333 format!("{}", DeviceType::AranetRadiation),
334 "Aranet Radiation"
335 );
336 }
337
338 #[test]
339 fn test_device_type_hash() {
340 use std::collections::HashSet;
341 let mut set = HashSet::new();
342 set.insert(DeviceType::Aranet4);
343 set.insert(DeviceType::Aranet2);
344 set.insert(DeviceType::Aranet4); assert_eq!(set.len(), 2);
346 assert!(set.contains(&DeviceType::Aranet4));
347 assert!(set.contains(&DeviceType::Aranet2));
348 }
349
350 #[test]
351 fn test_status_display() {
352 assert_eq!(format!("{}", Status::Error), "Error");
353 assert_eq!(format!("{}", Status::Green), "Good");
354 assert_eq!(format!("{}", Status::Yellow), "Moderate");
355 assert_eq!(format!("{}", Status::Red), "High");
356 }
357
358 #[test]
359 fn test_status_hash() {
360 use std::collections::HashSet;
361 let mut set = HashSet::new();
362 set.insert(Status::Green);
363 set.insert(Status::Yellow);
364 set.insert(Status::Green); assert_eq!(set.len(), 2);
366 assert!(set.contains(&Status::Green));
367 assert!(set.contains(&Status::Yellow));
368 }
369
370 #[test]
373 fn test_device_info_creation() {
374 let info = types::DeviceInfo {
375 name: "Aranet4 12345".to_string(),
376 model: "Aranet4".to_string(),
377 serial: "12345".to_string(),
378 firmware: "v1.2.0".to_string(),
379 hardware: "1.0".to_string(),
380 software: "1.2.0".to_string(),
381 manufacturer: "SAF Tehnika".to_string(),
382 };
383
384 assert_eq!(info.name, "Aranet4 12345");
385 assert_eq!(info.serial, "12345");
386 assert_eq!(info.manufacturer, "SAF Tehnika");
387 }
388
389 #[test]
390 fn test_device_info_clone() {
391 let info = types::DeviceInfo {
392 name: "Test".to_string(),
393 model: "Model".to_string(),
394 serial: "123".to_string(),
395 firmware: "1.0".to_string(),
396 hardware: "1.0".to_string(),
397 software: "1.0".to_string(),
398 manufacturer: "Mfg".to_string(),
399 };
400
401 let cloned = info.clone();
402 assert_eq!(cloned.name, info.name);
403 assert_eq!(cloned.serial, info.serial);
404 }
405
406 #[test]
407 fn test_device_info_debug() {
408 let info = types::DeviceInfo {
409 name: "Aranet4".to_string(),
410 model: "".to_string(),
411 serial: "".to_string(),
412 firmware: "".to_string(),
413 hardware: "".to_string(),
414 software: "".to_string(),
415 manufacturer: "".to_string(),
416 };
417
418 let debug_str = format!("{:?}", info);
419 assert!(debug_str.contains("Aranet4"));
420 }
421
422 #[test]
423 fn test_device_info_default() {
424 let info = types::DeviceInfo::default();
425 assert_eq!(info.name, "");
426 assert_eq!(info.model, "");
427 assert_eq!(info.serial, "");
428 assert_eq!(info.firmware, "");
429 assert_eq!(info.hardware, "");
430 assert_eq!(info.software, "");
431 assert_eq!(info.manufacturer, "");
432 }
433
434 #[test]
435 fn test_device_info_equality() {
436 let info1 = types::DeviceInfo {
437 name: "Test".to_string(),
438 model: "Model".to_string(),
439 serial: "123".to_string(),
440 firmware: "1.0".to_string(),
441 hardware: "1.0".to_string(),
442 software: "1.0".to_string(),
443 manufacturer: "Mfg".to_string(),
444 };
445 let info2 = info1.clone();
446 let info3 = types::DeviceInfo {
447 name: "Different".to_string(),
448 ..info1.clone()
449 };
450 assert_eq!(info1, info2);
451 assert_ne!(info1, info3);
452 }
453
454 #[test]
457 fn test_history_record_creation() {
458 use time::OffsetDateTime;
459
460 let record = types::HistoryRecord {
461 timestamp: OffsetDateTime::UNIX_EPOCH,
462 co2: 800,
463 temperature: 22.5,
464 pressure: 1013.2,
465 humidity: 45,
466 radon: None,
467 radiation_rate: None,
468 radiation_total: None,
469 };
470
471 assert_eq!(record.co2, 800);
472 assert!((record.temperature - 22.5).abs() < 0.01);
473 assert!((record.pressure - 1013.2).abs() < 0.1);
474 assert_eq!(record.humidity, 45);
475 assert!(record.radon.is_none());
476 assert!(record.radiation_rate.is_none());
477 assert!(record.radiation_total.is_none());
478 }
479
480 #[test]
481 fn test_history_record_clone() {
482 use time::OffsetDateTime;
483
484 let record = types::HistoryRecord {
485 timestamp: OffsetDateTime::UNIX_EPOCH,
486 co2: 500,
487 temperature: 20.0,
488 pressure: 1000.0,
489 humidity: 50,
490 radon: Some(100),
491 radiation_rate: Some(0.15),
492 radiation_total: Some(1.5),
493 };
494
495 let cloned = record.clone();
496 assert_eq!(cloned.co2, record.co2);
497 assert_eq!(cloned.humidity, record.humidity);
498 assert_eq!(cloned.radon, Some(100));
499 assert_eq!(cloned.radiation_rate, Some(0.15));
500 assert_eq!(cloned.radiation_total, Some(1.5));
501 }
502
503 #[test]
504 fn test_history_record_equality() {
505 use time::OffsetDateTime;
506
507 let record1 = types::HistoryRecord {
508 timestamp: OffsetDateTime::UNIX_EPOCH,
509 co2: 800,
510 temperature: 22.5,
511 pressure: 1013.2,
512 humidity: 45,
513 radon: None,
514 radiation_rate: None,
515 radiation_total: None,
516 };
517 let record2 = record1.clone();
518 assert_eq!(record1, record2);
519 }
520
521 #[test]
522 fn test_current_reading_equality() {
523 let reading1 = CurrentReading {
524 co2: 800,
525 temperature: 22.5,
526 pressure: 1013.2,
527 humidity: 45,
528 battery: 85,
529 status: Status::Green,
530 interval: 300,
531 age: 120,
532 captured_at: None,
533 radon: None,
534 radiation_rate: None,
535 radiation_total: None,
536 radon_avg_24h: None,
537 radon_avg_7d: None,
538 radon_avg_30d: None,
539 };
540 let reading2 = reading1;
542 assert_eq!(reading1, reading2);
543 }
544
545 #[test]
546 fn test_min_current_reading_bytes_const() {
547 assert_eq!(MIN_CURRENT_READING_BYTES, 13);
548 let bytes = [0u8; MIN_CURRENT_READING_BYTES];
550 assert!(CurrentReading::from_bytes(&bytes).is_ok());
551 let short_bytes = [0u8; MIN_CURRENT_READING_BYTES - 1];
553 assert!(CurrentReading::from_bytes(&short_bytes).is_err());
554 }
555
556 #[test]
559 fn test_parse_error_display() {
560 let err = ParseError::invalid_value("test message");
561 assert_eq!(err.to_string(), "Invalid value: test message");
562 }
563
564 #[test]
565 fn test_parse_error_insufficient_bytes() {
566 let err = ParseError::InsufficientBytes {
567 expected: 13,
568 actual: 5,
569 };
570 assert_eq!(err.to_string(), "Insufficient bytes: expected 13, got 5");
571 }
572
573 #[test]
574 fn test_parse_error_unknown_device_type() {
575 let err = ParseError::UnknownDeviceType(0xAB);
576 assert_eq!(err.to_string(), "Unknown device type: 0xAB");
577 }
578
579 #[test]
580 fn test_parse_error_invalid_value() {
581 let err = ParseError::InvalidValue("bad value".to_string());
582 assert_eq!(err.to_string(), "Invalid value: bad value");
583 }
584
585 #[test]
586 fn test_parse_error_debug() {
587 let err = ParseError::invalid_value("debug test");
588 let debug_str = format!("{:?}", err);
589 assert!(debug_str.contains("InvalidValue"));
590 assert!(debug_str.contains("debug test"));
591 }
592
593 #[test]
594 fn test_parse_error_equality() {
595 let err1 = ParseError::InsufficientBytes {
596 expected: 10,
597 actual: 5,
598 };
599 let err2 = ParseError::InsufficientBytes {
600 expected: 10,
601 actual: 5,
602 };
603 let err3 = ParseError::InsufficientBytes {
604 expected: 10,
605 actual: 6,
606 };
607 assert_eq!(err1, err2);
608 assert_ne!(err1, err3);
609 }
610
611 #[cfg(feature = "serde")]
614 #[test]
615 fn test_current_reading_serialization() {
616 let reading = CurrentReading {
617 co2: 800,
618 temperature: 22.5,
619 pressure: 1013.2,
620 humidity: 45,
621 battery: 85,
622 status: Status::Green,
623 interval: 300,
624 age: 120,
625 captured_at: None,
626 radon: None,
627 radiation_rate: None,
628 radiation_total: None,
629 radon_avg_24h: None,
630 radon_avg_7d: None,
631 radon_avg_30d: None,
632 };
633
634 let json = serde_json::to_string(&reading).unwrap();
635 assert!(json.contains("\"co2\":800"));
636 assert!(json.contains("\"humidity\":45"));
637 }
638
639 #[cfg(feature = "serde")]
640 #[test]
641 fn test_current_reading_deserialization() {
642 let json = r#"{"co2":800,"temperature":22.5,"pressure":1013.2,"humidity":45,"battery":85,"status":"Green","interval":300,"age":120,"radon":null,"radiation_rate":null,"radiation_total":null}"#;
643
644 let reading: CurrentReading = serde_json::from_str(json).unwrap();
645 assert_eq!(reading.co2, 800);
646 assert_eq!(reading.status, Status::Green);
647 }
648
649 #[cfg(feature = "serde")]
650 #[test]
651 fn test_status_serialization() {
652 assert_eq!(serde_json::to_string(&Status::Green).unwrap(), "\"Green\"");
653 assert_eq!(
654 serde_json::to_string(&Status::Yellow).unwrap(),
655 "\"Yellow\""
656 );
657 assert_eq!(serde_json::to_string(&Status::Red).unwrap(), "\"Red\"");
658 assert_eq!(serde_json::to_string(&Status::Error).unwrap(), "\"Error\"");
659 }
660
661 #[cfg(feature = "serde")]
662 #[test]
663 fn test_device_type_serialization() {
664 assert_eq!(
665 serde_json::to_string(&DeviceType::Aranet4).unwrap(),
666 "\"Aranet4\""
667 );
668 assert_eq!(
669 serde_json::to_string(&DeviceType::AranetRadon).unwrap(),
670 "\"AranetRadon\""
671 );
672 }
673
674 #[cfg(feature = "serde")]
675 #[test]
676 fn test_device_info_serialization_roundtrip() {
677 let info = types::DeviceInfo {
678 name: "Test Device".to_string(),
679 model: "Model X".to_string(),
680 serial: "SN12345".to_string(),
681 firmware: "1.2.3".to_string(),
682 hardware: "2.0".to_string(),
683 software: "3.0".to_string(),
684 manufacturer: "Acme Corp".to_string(),
685 };
686
687 let json = serde_json::to_string(&info).unwrap();
688 let deserialized: types::DeviceInfo = serde_json::from_str(&json).unwrap();
689
690 assert_eq!(deserialized.name, info.name);
691 assert_eq!(deserialized.serial, info.serial);
692 assert_eq!(deserialized.manufacturer, info.manufacturer);
693 }
694
695 #[test]
698 fn test_status_ordering() {
699 assert!(Status::Error < Status::Green);
701 assert!(Status::Green < Status::Yellow);
702 assert!(Status::Yellow < Status::Red);
703
704 assert!(Status::Red > Status::Yellow);
706 assert!(Status::Yellow >= Status::Yellow);
707 assert!(Status::Green <= Status::Yellow);
708 }
709
710 #[test]
711 fn test_device_type_readings_characteristic() {
712 use crate::ble;
713
714 assert_eq!(
716 DeviceType::Aranet4.readings_characteristic(),
717 ble::CURRENT_READINGS_DETAIL
718 );
719
720 assert_eq!(
722 DeviceType::Aranet2.readings_characteristic(),
723 ble::CURRENT_READINGS_DETAIL_ALT
724 );
725 assert_eq!(
726 DeviceType::AranetRadon.readings_characteristic(),
727 ble::CURRENT_READINGS_DETAIL_ALT
728 );
729 assert_eq!(
730 DeviceType::AranetRadiation.readings_characteristic(),
731 ble::CURRENT_READINGS_DETAIL_ALT
732 );
733 }
734
735 #[test]
736 fn test_device_type_from_name_word_boundary() {
737 assert_eq!(
739 DeviceType::from_name("Aranet4 12345"),
740 Some(DeviceType::Aranet4)
741 );
742 assert_eq!(
743 DeviceType::from_name("My Aranet4"),
744 Some(DeviceType::Aranet4)
745 );
746
747 assert_eq!(DeviceType::from_name("ARANET4"), Some(DeviceType::Aranet4));
749 assert_eq!(DeviceType::from_name("aranet2"), Some(DeviceType::Aranet2));
750
751 assert_eq!(
753 DeviceType::from_name("AranetRn+ 306B8"),
754 Some(DeviceType::AranetRadon)
755 );
756 assert_eq!(
757 DeviceType::from_name("aranetrn+ 12345"),
758 Some(DeviceType::AranetRadon)
759 );
760
761 assert_eq!(
763 DeviceType::from_name("Aranet\u{2622} 30ED1"),
764 Some(DeviceType::AranetRadiation)
765 );
766 assert_eq!(
767 DeviceType::from_name("Aranet Radiation"),
768 Some(DeviceType::AranetRadiation)
769 );
770 }
771
772 #[test]
773 fn test_device_type_has_co2() {
774 assert!(DeviceType::Aranet4.has_co2());
775 assert!(!DeviceType::Aranet2.has_co2());
776 assert!(!DeviceType::AranetRadon.has_co2());
777 assert!(!DeviceType::AranetRadiation.has_co2());
778 }
779
780 #[test]
781 fn test_device_type_has_temperature() {
782 assert!(DeviceType::Aranet4.has_temperature());
783 assert!(DeviceType::Aranet2.has_temperature());
784 assert!(DeviceType::AranetRadon.has_temperature());
785 assert!(!DeviceType::AranetRadiation.has_temperature());
786 }
787
788 #[test]
789 fn test_device_type_has_humidity() {
790 assert!(DeviceType::Aranet4.has_humidity());
791 assert!(DeviceType::Aranet2.has_humidity());
792 assert!(DeviceType::AranetRadon.has_humidity());
793 assert!(!DeviceType::AranetRadiation.has_humidity());
794 }
795
796 #[test]
797 fn test_device_type_has_pressure() {
798 assert!(DeviceType::Aranet4.has_pressure());
799 assert!(!DeviceType::Aranet2.has_pressure());
800 assert!(DeviceType::AranetRadon.has_pressure());
801 assert!(!DeviceType::AranetRadiation.has_pressure());
802 }
803
804 #[test]
805 fn test_byte_size_constants() {
806 assert_eq!(MIN_CURRENT_READING_BYTES, 13);
807 assert_eq!(types::MIN_ARANET2_READING_BYTES, 12);
808 assert_eq!(types::MIN_RADON_READING_BYTES, 15);
809 assert_eq!(types::MIN_RADON_GATT_READING_BYTES, 18);
810 assert_eq!(types::MIN_RADIATION_READING_BYTES, 28);
811 }
812
813 #[test]
814 fn test_from_bytes_aranet2() {
815 let data = [
817 0x02, 0x00, 0x2C, 0x01, 0x3C, 0x00, 0x55, 0x90, 0x01, 0xF4, 0x01, 0x04, ];
825
826 let reading = CurrentReading::from_bytes_aranet2(&data).unwrap();
827 assert_eq!(reading.co2, 0); assert!((reading.temperature - 20.0).abs() < 0.1);
829 assert_eq!(reading.humidity, 50);
830 assert_eq!(reading.battery, 85);
831 assert_eq!(reading.status, Status::Green);
832 assert_eq!(reading.interval, 300);
833 assert_eq!(reading.age, 60);
834 assert_eq!(reading.pressure, 0.0); }
836
837 #[test]
838 fn test_from_bytes_aranet2_insufficient() {
839 let data = [0u8; 11]; let result = CurrentReading::from_bytes_aranet2(&data);
841 assert!(result.is_err());
842 }
843
844 #[test]
845 fn test_from_bytes_for_device() {
846 let aranet4_data = [0u8; 13];
848 let result = CurrentReading::from_bytes_for_device(&aranet4_data, DeviceType::Aranet4);
849 assert!(result.is_ok());
850
851 let aranet2_data = [0u8; 12];
852 let result = CurrentReading::from_bytes_for_device(&aranet2_data, DeviceType::Aranet2);
853 assert!(result.is_ok());
854 }
855
856 #[test]
857 fn test_builder_with_captured_at() {
858 use time::OffsetDateTime;
859
860 let now = OffsetDateTime::now_utc();
861 let reading = CurrentReading::builder()
862 .co2(800)
863 .temperature(22.5)
864 .captured_at(now)
865 .build();
866
867 assert_eq!(reading.co2, 800);
868 assert_eq!(reading.captured_at, Some(now));
869 }
870
871 #[test]
872 fn test_builder_try_build_valid() {
873 let result = CurrentReading::builder()
874 .co2(800)
875 .temperature(22.5)
876 .pressure(1013.0)
877 .humidity(50)
878 .battery(85)
879 .try_build();
880
881 assert!(result.is_ok());
882 }
883
884 #[test]
885 fn test_builder_try_build_invalid_humidity() {
886 let result = CurrentReading::builder()
887 .humidity(150) .try_build();
889
890 assert!(result.is_err());
891 let err = result.unwrap_err();
892 assert!(err.to_string().contains("humidity"));
893 }
894
895 #[test]
896 fn test_builder_try_build_invalid_battery() {
897 let result = CurrentReading::builder()
898 .battery(120) .try_build();
900
901 assert!(result.is_err());
902 let err = result.unwrap_err();
903 assert!(err.to_string().contains("battery"));
904 }
905
906 #[test]
907 fn test_builder_try_build_invalid_temperature() {
908 let result = CurrentReading::builder()
909 .temperature(-50.0) .try_build();
911
912 assert!(result.is_err());
913 let err = result.unwrap_err();
914 assert!(err.to_string().contains("temperature"));
915 }
916
917 #[test]
918 fn test_builder_try_build_invalid_pressure() {
919 let result = CurrentReading::builder()
920 .temperature(22.0) .pressure(500.0) .try_build();
923
924 assert!(result.is_err());
925 let err = result.unwrap_err();
926 assert!(err.to_string().contains("pressure"));
927 }
928
929 #[test]
930 fn test_with_captured_at() {
931 use time::OffsetDateTime;
932
933 let reading = CurrentReading::builder().age(60).build();
934
935 let now = OffsetDateTime::now_utc();
936 let reading_with_time = reading.with_captured_at(now);
937
938 assert!(reading_with_time.captured_at.is_some());
939 let captured = reading_with_time.captured_at.unwrap();
941 let expected = now - time::Duration::seconds(60);
942 assert!((captured - expected).whole_seconds().abs() < 2);
943 }
944
945 #[test]
946 fn test_parse_error_invalid_value_helper() {
947 let err = ParseError::invalid_value("test error");
948 assert_eq!(err.to_string(), "Invalid value: test error");
949 }
950}
951
952#[cfg(test)]
985mod proptests {
986 use super::*;
987 use proptest::prelude::*;
988
989 proptest! {
990 #[test]
993 fn parse_current_reading_never_panics(data: Vec<u8>) {
994 let _ = CurrentReading::from_bytes(&data);
995 }
996
997 #[test]
999 fn parse_aranet2_never_panics(data: Vec<u8>) {
1000 let _ = CurrentReading::from_bytes_aranet2(&data);
1001 }
1002
1003 #[test]
1005 fn status_from_u8_never_panics(value: u8) {
1006 let status = Status::from(value);
1007 let _ = format!("{:?}", status);
1009 }
1010
1011 #[test]
1013 fn device_type_try_from_never_panics(value: u8) {
1014 let _ = DeviceType::try_from(value);
1015 }
1016
1017 #[test]
1019 fn parse_valid_aranet4_bytes(
1020 co2 in 0u16..10000u16,
1021 temp_raw in 0u16..2000u16,
1022 pressure_raw in 8000u16..12000u16,
1023 humidity in 0u8..100u8,
1024 battery in 0u8..100u8,
1025 status_byte in 0u8..4u8,
1026 interval in 60u16..3600u16,
1027 age in 0u16..3600u16,
1028 ) {
1029 let mut data = [0u8; 13];
1030 data[0..2].copy_from_slice(&co2.to_le_bytes());
1031 data[2..4].copy_from_slice(&temp_raw.to_le_bytes());
1032 data[4..6].copy_from_slice(&pressure_raw.to_le_bytes());
1033 data[6] = humidity;
1034 data[7] = battery;
1035 data[8] = status_byte;
1036 data[9..11].copy_from_slice(&interval.to_le_bytes());
1037 data[11..13].copy_from_slice(&age.to_le_bytes());
1038
1039 let result = CurrentReading::from_bytes(&data);
1040 prop_assert!(result.is_ok());
1041
1042 let reading = result.unwrap();
1043 prop_assert_eq!(reading.co2, co2);
1044 prop_assert_eq!(reading.humidity, humidity);
1045 prop_assert_eq!(reading.battery, battery);
1046 prop_assert_eq!(reading.interval, interval);
1047 prop_assert_eq!(reading.age, age);
1048 }
1049
1050 #[test]
1052 fn parse_valid_aranet2_bytes(
1053 temp_raw in 0u16..2000u16,
1054 humidity_raw in 0u16..1000u16,
1055 battery in 0u8..100u8,
1056 status_flags in 0u8..16u8,
1057 interval in 60u16..3600u16,
1058 age in 0u16..3600u16,
1059 ) {
1060 let mut data = [0u8; 12];
1061 data[0..2].copy_from_slice(&0x0002u16.to_le_bytes()); data[2..4].copy_from_slice(&interval.to_le_bytes());
1063 data[4..6].copy_from_slice(&age.to_le_bytes());
1064 data[6] = battery;
1065 data[7..9].copy_from_slice(&temp_raw.to_le_bytes());
1066 data[9..11].copy_from_slice(&humidity_raw.to_le_bytes());
1067 data[11] = status_flags;
1068
1069 let result = CurrentReading::from_bytes_aranet2(&data);
1070 prop_assert!(result.is_ok());
1071
1072 let reading = result.unwrap();
1073 prop_assert_eq!(reading.humidity, (humidity_raw / 10) as u8);
1074 prop_assert_eq!(reading.battery, battery);
1075 prop_assert_eq!(reading.interval, interval);
1076 prop_assert_eq!(reading.age, age);
1077 }
1078
1079 #[cfg(feature = "serde")]
1081 #[test]
1082 fn current_reading_json_roundtrip(
1083 co2 in 0u16..10000u16,
1084 temperature in -20.0f32..60.0f32,
1085 pressure in 800.0f32..1200.0f32,
1086 humidity in 0u8..100u8,
1087 battery in 0u8..100u8,
1088 interval in 60u16..3600u16,
1089 age in 0u16..3600u16,
1090 ) {
1091 let reading = CurrentReading {
1092 co2,
1093 temperature,
1094 pressure,
1095 humidity,
1096 battery,
1097 status: Status::Green,
1098 interval,
1099 age,
1100 captured_at: None,
1101 radon: None,
1102 radiation_rate: None,
1103 radiation_total: None,
1104 radon_avg_24h: None,
1105 radon_avg_7d: None,
1106 radon_avg_30d: None,
1107 };
1108
1109 let json = serde_json::to_string(&reading).unwrap();
1110 let parsed: CurrentReading = serde_json::from_str(&json).unwrap();
1111
1112 prop_assert_eq!(parsed.co2, reading.co2);
1113 prop_assert_eq!(parsed.humidity, reading.humidity);
1114 prop_assert_eq!(parsed.battery, reading.battery);
1115 prop_assert_eq!(parsed.interval, reading.interval);
1116 prop_assert_eq!(parsed.age, reading.age);
1117 }
1118 }
1119}