1use bytes::Buf;
10
11use crate::error::{Error, Result};
12use aranet_types::{CurrentReading, DeviceType, Status};
13
14fn from_parse_error(e: aranet_types::ParseError) -> Error {
16 Error::InvalidData(e.to_string())
17}
18
19#[derive(Debug, Clone)]
27pub struct ExtendedReading {
28 pub reading: CurrentReading,
30 pub radiation_duration: Option<u64>,
32}
33
34pub fn parse_aranet4_reading(data: &[u8]) -> Result<CurrentReading> {
46 CurrentReading::from_bytes(data).map_err(|e| Error::InvalidData(e.to_string()))
47}
48
49pub fn parse_aranet2_reading(data: &[u8]) -> Result<CurrentReading> {
53 CurrentReading::from_bytes_aranet2(data).map_err(from_parse_error)
54}
55
56pub fn parse_aranet_radon_reading(data: &[u8]) -> Result<ExtendedReading> {
60 if data.len() < 15 {
61 return Err(Error::InvalidData(format!(
62 "Aranet Radon reading requires 15 bytes, got {}",
63 data.len()
64 )));
65 }
66
67 let mut buf = data;
68
69 let co2 = buf.get_u16_le();
71 let temp_raw = buf.get_i16_le();
72 let pressure_raw = buf.get_u16_le();
73 let humidity = buf.get_u8();
74 let battery = buf.get_u8();
75 let status = Status::from(buf.get_u8());
76 let interval = buf.get_u16_le();
77 let age = buf.get_u16_le();
78
79 let radon = buf.get_u16_le() as u32;
81
82 let reading = CurrentReading {
83 co2,
84 temperature: temp_raw as f32 / 20.0,
85 pressure: pressure_raw as f32 / 10.0,
86 humidity,
87 battery,
88 status,
89 interval,
90 age,
91 captured_at: None,
92 radon: Some(radon),
93 radiation_rate: None,
94 radiation_total: None,
95 radon_avg_24h: None,
96 radon_avg_7d: None,
97 radon_avg_30d: None,
98 };
99
100 Ok(ExtendedReading {
101 reading,
102 radiation_duration: None,
103 })
104}
105
106pub fn parse_aranet_radon_gatt(data: &[u8]) -> Result<CurrentReading> {
110 CurrentReading::from_bytes_radon(data).map_err(from_parse_error)
111}
112
113pub fn parse_aranet_radiation_gatt(data: &[u8]) -> Result<ExtendedReading> {
119 let reading = CurrentReading::from_bytes_radiation(data).map_err(from_parse_error)?;
120
121 let duration = (&data[19..27]).get_u64_le();
124
125 Ok(ExtendedReading {
126 reading,
127 radiation_duration: Some(duration),
128 })
129}
130
131pub fn parse_reading_for_device(data: &[u8], device_type: DeviceType) -> Result<CurrentReading> {
135 CurrentReading::from_bytes_for_device(data, device_type).map_err(from_parse_error)
136}
137
138pub fn parse_extended_reading(data: &[u8], device_type: DeviceType) -> Result<ExtendedReading> {
140 match device_type {
141 DeviceType::AranetRadiation => parse_aranet_radiation_gatt(data),
142 _ => {
143 let reading = parse_reading_for_device(data, device_type)?;
144 Ok(ExtendedReading {
145 reading,
146 radiation_duration: None,
147 })
148 }
149 }
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155
156 #[test]
159 fn test_parse_aranet2_reading() {
160 let data: [u8; 12] = [
168 0x02, 0x00, 0x2C, 0x01, 0x78, 0x00, 90, 0xC2, 0x01, 0x26, 0x02, 0x04, ];
176
177 let reading = parse_aranet2_reading(&data).unwrap();
178 assert_eq!(reading.co2, 0);
179 assert!((reading.temperature - 22.5).abs() < 0.01);
180 assert_eq!(reading.humidity, 55);
181 assert_eq!(reading.battery, 90);
182 assert_eq!(reading.status, Status::Green);
183 assert_eq!(reading.interval, 300);
184 assert_eq!(reading.age, 120);
185 }
186
187 #[test]
188 fn test_parse_aranet2_reading_all_status_values() {
189 for (status_flags, expected_status) in [
192 (0x00, Status::Error), (0x04, Status::Green), (0x08, Status::Yellow), (0x0C, Status::Red), ] {
197 let data: [u8; 12] = [
198 0x02,
199 0x00, 0x2C,
201 0x01, 0x78,
203 0x00, 90, 0xC2,
206 0x01, 0x26,
208 0x02, status_flags,
210 ];
211
212 let reading = parse_aranet2_reading(&data).unwrap();
213 assert_eq!(reading.status, expected_status);
214 }
215 }
216
217 #[test]
218 fn test_parse_aranet2_reading_insufficient_bytes() {
219 let data: [u8; 8] = [0x02, 0x00, 0x2C, 0x01, 0x78, 0x00, 90, 0xC2];
220
221 let result = parse_aranet2_reading(&data);
222 assert!(result.is_err());
223
224 let err = result.unwrap_err();
225 assert!(err.to_string().contains("expected 12"));
226 assert!(err.to_string().contains("got 8"));
227 }
228
229 #[test]
230 fn test_parse_aranet2_reading_edge_values() {
231 let data: [u8; 12] = [0; 12];
233
234 let reading = parse_aranet2_reading(&data).unwrap();
235 assert_eq!(reading.co2, 0);
236 assert!((reading.temperature - 0.0).abs() < 0.01);
237 assert_eq!(reading.humidity, 0);
238 assert_eq!(reading.battery, 0);
239 assert_eq!(reading.status, Status::Error);
240 assert_eq!(reading.interval, 0);
241 assert_eq!(reading.age, 0);
242 }
243
244 #[test]
245 fn test_parse_aranet2_reading_max_values() {
246 let data: [u8; 12] = [
247 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 100, 0xFF, 0xFF, 0xFF, 0xFF, 0x0C, ];
255
256 let reading = parse_aranet2_reading(&data).unwrap();
257 assert!((reading.temperature - (-0.05)).abs() < 0.01); assert_eq!(reading.battery, 100);
259 assert_eq!(reading.status, Status::Red);
260 assert_eq!(reading.interval, 65535);
261 assert_eq!(reading.age, 65535);
262 }
263
264 #[test]
267 fn test_parse_aranet4_reading() {
268 let data: [u8; 13] = [
270 0x20, 0x03, 0xC2, 0x01, 0x94, 0x27, 45, 85, 1, 0x2C, 0x01, 0x78, 0x00, ];
279
280 let reading = parse_aranet4_reading(&data).unwrap();
281 assert_eq!(reading.co2, 800);
282 assert!((reading.temperature - 22.5).abs() < 0.01);
283 assert!((reading.pressure - 1013.2).abs() < 0.1);
284 assert_eq!(reading.humidity, 45);
285 assert_eq!(reading.battery, 85);
286 assert_eq!(reading.status, Status::Green);
287 assert_eq!(reading.interval, 300);
288 assert_eq!(reading.age, 120);
289 }
290
291 #[test]
292 fn test_parse_aranet4_reading_high_co2() {
293 let data: [u8; 13] = [
295 0xD0, 0x07, 0x90, 0x01, 0x88, 0x27, 60, 75, 3, 0x3C, 0x00, 0x1E, 0x00, ];
304
305 let reading = parse_aranet4_reading(&data).unwrap();
306 assert_eq!(reading.co2, 2000);
307 assert_eq!(reading.status, Status::Red);
308 }
309
310 #[test]
311 fn test_parse_aranet4_reading_insufficient_bytes() {
312 let data: [u8; 10] = [0; 10];
313
314 let result = parse_aranet4_reading(&data);
315 assert!(result.is_err());
316
317 let err = result.unwrap_err();
318 assert!(err.to_string().contains("expected 13"));
320 assert!(err.to_string().contains("got 10"));
321 }
322
323 #[test]
326 fn test_parse_aranet_radon_reading() {
327 let data: [u8; 15] = [
329 0x00, 0x00, 0xC2, 0x01, 0x94, 0x27, 50, 80, 1, 0x2C, 0x01, 0x3C, 0x00, 0x64, 0x00, ];
339
340 let result = parse_aranet_radon_reading(&data).unwrap();
341 assert_eq!(result.reading.radon, Some(100));
342 assert!(result.reading.radiation_rate.is_none());
343 assert!((result.reading.temperature - 22.5).abs() < 0.01);
344 assert_eq!(result.reading.humidity, 50);
345 }
346
347 #[test]
348 fn test_parse_aranet_radon_reading_high_radon() {
349 let mut data: [u8; 15] = [0; 15];
350 data[13] = 0xF4;
352 data[14] = 0x01; let result = parse_aranet_radon_reading(&data).unwrap();
355 assert_eq!(result.reading.radon, Some(500));
356 }
357
358 #[test]
359 fn test_parse_aranet_radon_reading_insufficient_bytes() {
360 let data: [u8; 12] = [0; 12];
361
362 let result = parse_aranet_radon_reading(&data);
363 assert!(result.is_err());
364 assert!(
365 result
366 .unwrap_err()
367 .to_string()
368 .contains("requires 15 bytes")
369 );
370 }
371
372 #[test]
375 fn test_parse_aranet_radon_gatt() {
376 let mut data: [u8; 18] = [0; 18];
378 data[0] = 0x03;
380 data[1] = 0x00;
381 data[2] = 0x58;
383 data[3] = 0x02;
384 data[4] = 0x78;
386 data[5] = 0x00;
387 data[6] = 85;
389 data[7] = 0xC2;
391 data[8] = 0x01;
392 data[9] = 0x94;
394 data[10] = 0x27;
395 data[11] = 0xC2;
397 data[12] = 0x01;
398 data[13] = 0x64;
400 data[14] = 0x00;
401 data[15] = 0x00;
402 data[16] = 0x00;
403 data[17] = 1;
405
406 let reading = parse_aranet_radon_gatt(&data).unwrap();
407 assert_eq!(reading.battery, 85);
408 assert!((reading.temperature - 22.5).abs() < 0.01);
409 assert_eq!(reading.radon, Some(100)); assert_eq!(reading.co2, 0); assert_eq!(reading.status, Status::Green);
412 assert_eq!(reading.interval, 600);
413 assert_eq!(reading.age, 120);
414 }
415
416 #[test]
417 fn test_parse_aranet_radon_gatt_insufficient_bytes() {
418 let data: [u8; 15] = [0; 15];
419
420 let result = parse_aranet_radon_gatt(&data);
421 assert!(result.is_err());
422 assert!(result.unwrap_err().to_string().contains("expected 18"));
423 }
424
425 #[test]
426 fn test_parse_aranet_radon_gatt_high_radon() {
427 let mut data: [u8; 18] = [0; 18];
429 data[0] = 0x03; data[13] = 0xA0;
433 data[14] = 0x86;
434 data[15] = 0x01;
435 data[16] = 0x00; let reading = parse_aranet_radon_gatt(&data).unwrap();
438 assert_eq!(reading.radon, Some(100000)); }
440
441 #[test]
444 fn test_parse_reading_for_device_aranet4() {
445 let data: [u8; 13] = [
446 0x20, 0x03, 0xC2, 0x01, 0x94, 0x27, 45, 85, 1, 0x2C, 0x01, 0x78, 0x00, ];
453
454 let reading = parse_reading_for_device(&data, DeviceType::Aranet4).unwrap();
455 assert_eq!(reading.co2, 800);
456 }
457
458 #[test]
459 fn test_parse_reading_for_device_aranet2() {
460 let data: [u8; 12] = [
461 0x02, 0x00, 0x2C, 0x01, 0x78, 0x00, 90, 0xC2, 0x01, 0x26, 0x02, 0x04, ];
469
470 let reading = parse_reading_for_device(&data, DeviceType::Aranet2).unwrap();
471 assert_eq!(reading.co2, 0); assert!((reading.temperature - 22.5).abs() < 0.01);
473 }
474
475 #[test]
478 fn test_extended_reading_with_radon() {
479 let reading = CurrentReading {
480 co2: 0,
481 temperature: 22.5,
482 pressure: 1013.2,
483 humidity: 50,
484 battery: 80,
485 status: Status::Green,
486 interval: 300,
487 age: 60,
488 captured_at: None,
489 radon: Some(150),
490 radiation_rate: None,
491 radiation_total: None,
492 radon_avg_24h: None,
493 radon_avg_7d: None,
494 radon_avg_30d: None,
495 };
496
497 let extended = ExtendedReading {
498 reading,
499 radiation_duration: None,
500 };
501
502 assert_eq!(extended.reading.radon, Some(150));
503 assert!(extended.reading.radiation_rate.is_none());
504 assert!((extended.reading.temperature - 22.5).abs() < 0.01);
505 }
506
507 #[test]
508 fn test_extended_reading_with_radiation() {
509 let reading = CurrentReading {
510 co2: 0,
511 temperature: 20.0,
512 pressure: 1000.0,
513 humidity: 45,
514 battery: 90,
515 status: Status::Green,
516 interval: 60,
517 age: 30,
518 captured_at: None,
519 radon: None,
520 radiation_rate: Some(0.15),
521 radiation_total: Some(0.001),
522 radon_avg_24h: None,
523 radon_avg_7d: None,
524 radon_avg_30d: None,
525 };
526
527 let extended = ExtendedReading {
528 reading,
529 radiation_duration: Some(3600),
530 };
531
532 assert!(extended.reading.radon.is_none());
533 assert!((extended.reading.radiation_rate.unwrap() - 0.15).abs() < 0.001);
534 assert_eq!(extended.radiation_duration, Some(3600));
535 }
536
537 #[test]
538 fn test_extended_reading_debug() {
539 let reading = CurrentReading {
540 co2: 800,
541 temperature: 22.5,
542 pressure: 1013.2,
543 humidity: 50,
544 battery: 80,
545 status: Status::Green,
546 interval: 300,
547 age: 60,
548 captured_at: None,
549 radon: Some(100),
550 radiation_rate: None,
551 radiation_total: None,
552 radon_avg_24h: None,
553 radon_avg_7d: None,
554 radon_avg_30d: None,
555 };
556
557 let extended = ExtendedReading {
558 reading,
559 radiation_duration: None,
560 };
561
562 let debug_str = format!("{:?}", extended);
563 assert!(debug_str.contains("radon"));
564 assert!(debug_str.contains("100"));
565 }
566
567 #[test]
568 fn test_extended_reading_clone() {
569 let reading = CurrentReading {
570 co2: 800,
571 temperature: 22.5,
572 pressure: 1013.2,
573 humidity: 50,
574 battery: 80,
575 status: Status::Green,
576 interval: 300,
577 age: 60,
578 captured_at: None,
579 radon: Some(100),
580 radiation_rate: Some(0.1),
581 radiation_total: Some(0.001),
582 radon_avg_24h: None,
583 radon_avg_7d: None,
584 radon_avg_30d: None,
585 };
586
587 let extended = ExtendedReading {
588 reading,
589 radiation_duration: Some(3600),
590 };
591
592 let cloned = extended.clone();
593 assert_eq!(cloned.reading.radon, extended.reading.radon);
594 assert_eq!(
595 cloned.reading.radiation_rate,
596 extended.reading.radiation_rate
597 );
598 assert_eq!(cloned.reading.co2, extended.reading.co2);
599 assert_eq!(cloned.radiation_duration, extended.radiation_duration);
600 }
601
602 #[test]
603 fn test_parse_aranet_radiation_gatt() {
604 let data = [
606 0x00, 0x00, 0x3C, 0x00, 0x1E, 0x00, 0x5A, 0xE8, 0x03, 0x00, 0x00, 0x40, 0x42, 0x0F, 0x00, 0x00, 0x00, 0x00,
612 0x00, 0x10, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, ];
616
617 let result = parse_aranet_radiation_gatt(&data).unwrap();
618 assert_eq!(result.reading.interval, 60);
619 assert_eq!(result.reading.age, 30);
620 assert_eq!(result.reading.battery, 90);
621 assert!((result.reading.radiation_rate.unwrap() - 1.0).abs() < 0.001);
622 assert!((result.reading.radiation_total.unwrap() - 1.0).abs() < 0.001);
623 assert_eq!(result.radiation_duration, Some(3600));
624 assert_eq!(result.reading.status, Status::Green);
625 assert!(result.reading.radon.is_none());
626 }
627
628 #[test]
629 fn test_parse_aranet_radiation_gatt_insufficient_bytes() {
630 let data = [0x00; 20]; let result = parse_aranet_radiation_gatt(&data);
632 assert!(result.is_err());
633 let err = result.unwrap_err();
634 assert!(err.to_string().contains("expected 28"));
635 }
636
637 #[test]
638 fn test_parse_aranet_radiation_gatt_high_values() {
639 let data = [
641 0x00, 0x00, 0x2C, 0x01, 0x0A, 0x00, 0x64, 0x10, 0x27, 0x00, 0x00, 0x00, 0xE1, 0xF5, 0x05, 0x00, 0x00, 0x00,
647 0x00, 0x80, 0x51, 0x01, 0x00, 0x00, 0x00, 0x00,
649 0x00, 0x02, ];
652
653 let result = parse_aranet_radiation_gatt(&data).unwrap();
654 assert_eq!(result.reading.interval, 300);
655 assert!((result.reading.radiation_rate.unwrap() - 10.0).abs() < 0.001);
656 assert!((result.reading.radiation_total.unwrap() - 100.0).abs() < 0.001);
657 assert_eq!(result.radiation_duration, Some(86400));
658 assert_eq!(result.reading.status, Status::Yellow);
659 }
660}
661
662#[cfg(test)]
688mod proptests {
689 use super::*;
690 use proptest::prelude::*;
691
692 proptest! {
693 #[test]
695 fn parse_aranet4_never_panics(data: Vec<u8>) {
696 let _ = parse_aranet4_reading(&data);
697 }
698
699 #[test]
700 fn parse_aranet2_never_panics(data: Vec<u8>) {
701 let _ = parse_aranet2_reading(&data);
702 }
703
704 #[test]
705 fn parse_aranet_radon_never_panics(data: Vec<u8>) {
706 let _ = parse_aranet_radon_reading(&data);
707 }
708
709 #[test]
710 fn parse_aranet_radon_gatt_never_panics(data: Vec<u8>) {
711 let _ = parse_aranet_radon_gatt(&data);
712 }
713
714 #[test]
715 fn parse_aranet_radiation_gatt_never_panics(data: Vec<u8>) {
716 let _ = parse_aranet_radiation_gatt(&data);
717 }
718
719 #[test]
721 fn parse_reading_for_device_never_panics(
722 data: Vec<u8>,
723 device_type_byte in 0xF1u8..=0xF4u8,
724 ) {
725 if let Ok(device_type) = DeviceType::try_from(device_type_byte) {
726 let _ = parse_reading_for_device(&data, device_type);
727 }
728 }
729
730 #[test]
732 fn aranet4_valid_bytes_parse_correctly(
733 co2 in 0u16..10000u16,
734 temp_raw in 0u16..2000u16,
735 pressure_raw in 8000u16..12000u16,
736 humidity in 0u8..100u8,
737 battery in 0u8..100u8,
738 status_byte in 0u8..4u8,
739 interval in 60u16..3600u16,
740 age in 0u16..3600u16,
741 ) {
742 let mut data = [0u8; 13];
743 data[0..2].copy_from_slice(&co2.to_le_bytes());
744 data[2..4].copy_from_slice(&temp_raw.to_le_bytes());
745 data[4..6].copy_from_slice(&pressure_raw.to_le_bytes());
746 data[6] = humidity;
747 data[7] = battery;
748 data[8] = status_byte;
749 data[9..11].copy_from_slice(&interval.to_le_bytes());
750 data[11..13].copy_from_slice(&age.to_le_bytes());
751
752 let result = parse_aranet4_reading(&data);
753 prop_assert!(result.is_ok());
754
755 let reading = result.unwrap();
756 prop_assert_eq!(reading.co2, co2);
757 prop_assert_eq!(reading.humidity, humidity);
758 prop_assert_eq!(reading.battery, battery);
759 prop_assert_eq!(reading.interval, interval);
760 prop_assert_eq!(reading.age, age);
761 }
762
763 #[test]
765 fn aranet2_valid_bytes_parse_correctly(
766 temp_raw in 0u16..2000u16,
767 humidity_raw in 0u16..1000u16,
768 battery in 0u8..100u8,
769 status_flags in 0u8..16u8,
770 interval in 60u16..3600u16,
771 age in 0u16..3600u16,
772 ) {
773 let mut data = [0u8; 12];
774 data[0..2].copy_from_slice(&0x0002u16.to_le_bytes()); data[2..4].copy_from_slice(&interval.to_le_bytes());
776 data[4..6].copy_from_slice(&age.to_le_bytes());
777 data[6] = battery;
778 data[7..9].copy_from_slice(&temp_raw.to_le_bytes());
779 data[9..11].copy_from_slice(&humidity_raw.to_le_bytes());
780 data[11] = status_flags;
781
782 let result = parse_aranet2_reading(&data);
783 prop_assert!(result.is_ok());
784
785 let reading = result.unwrap();
786 prop_assert_eq!(reading.co2, 0); prop_assert_eq!(reading.humidity, (humidity_raw / 10) as u8);
788 prop_assert_eq!(reading.battery, battery);
789 prop_assert_eq!(reading.interval, interval);
790 prop_assert_eq!(reading.age, age);
791 }
792 }
793}