aranet_types/
lib.rs

1//! Platform-agnostic types for Aranet environmental sensors.
2//!
3//! This crate provides shared types that can be used by both native
4//! (aranet-core) and WebAssembly (aranet-wasm) implementations.
5//!
6//! # Features
7//!
8//! - Core data types for sensor readings
9//! - Device information structures
10//! - UUID constants for BLE characteristics
11//! - Error types for data parsing
12//!
13//! # Example
14//!
15//! ```
16//! use aranet_types::{CurrentReading, Status, DeviceType};
17//!
18//! // Types can be used for parsing and serialization
19//! ```
20
21pub mod error;
22pub mod types;
23pub mod uuid;
24
25pub use error::{ParseError, ParseResult};
26pub use types::{
27    CurrentReading, CurrentReadingBuilder, DeviceInfo, DeviceInfoBuilder, DeviceType,
28    HistoryRecord, HistoryRecordBuilder, MIN_CURRENT_READING_BYTES, Status,
29};
30
31// Re-export uuid module with a clearer name to avoid confusion with the `uuid` crate.
32// The `uuids` alias is kept for backwards compatibility.
33pub use uuid as ble;
34#[doc(hidden)]
35pub use uuid as uuids;
36
37#[cfg(test)]
38mod tests {
39    use super::*;
40
41    // --- CurrentReading parsing tests ---
42
43    #[test]
44    fn test_parse_current_reading_from_valid_bytes() {
45        // Construct test bytes:
46        // CO2: 800 (0x0320 LE -> [0x20, 0x03])
47        // Temperature: 450 raw (22.5°C = 450/20) -> [0xC2, 0x01]
48        // Pressure: 10132 raw (1013.2 hPa = 10132/10) -> [0x94, 0x27]
49        // Humidity: 45
50        // Battery: 85
51        // Status: 1 (Green)
52        // Interval: 300 -> [0x2C, 0x01]
53        // Age: 120 -> [0x78, 0x00]
54        let bytes: [u8; 13] = [
55            0x20, 0x03, // CO2 = 800
56            0xC2, 0x01, // temp_raw = 450
57            0x94, 0x27, // pressure_raw = 10132
58            45,   // humidity
59            85,   // battery
60            1,    // status = Green
61            0x2C, 0x01, // interval = 300
62            0x78, 0x00, // age = 120
63        ];
64
65        let reading = CurrentReading::from_bytes(&bytes).unwrap();
66
67        assert_eq!(reading.co2, 800);
68        assert!((reading.temperature - 22.5).abs() < 0.01);
69        assert!((reading.pressure - 1013.2).abs() < 0.1);
70        assert_eq!(reading.humidity, 45);
71        assert_eq!(reading.battery, 85);
72        assert_eq!(reading.status, Status::Green);
73        assert_eq!(reading.interval, 300);
74        assert_eq!(reading.age, 120);
75    }
76
77    #[test]
78    fn test_parse_current_reading_from_insufficient_bytes() {
79        let bytes: [u8; 10] = [0; 10]; // Only 10 bytes, need 13
80
81        let result = CurrentReading::from_bytes(&bytes);
82
83        assert!(result.is_err());
84        let err = result.unwrap_err();
85        assert_eq!(
86            err,
87            ParseError::InsufficientBytes {
88                expected: 13,
89                actual: 10
90            }
91        );
92        assert!(err.to_string().contains("expected 13"));
93        assert!(err.to_string().contains("got 10"));
94    }
95
96    #[test]
97    fn test_parse_current_reading_zero_bytes() {
98        let bytes: [u8; 0] = [];
99
100        let result = CurrentReading::from_bytes(&bytes);
101        assert!(result.is_err());
102    }
103
104    #[test]
105    fn test_parse_current_reading_all_zeros() {
106        let bytes: [u8; 13] = [0; 13];
107
108        let reading = CurrentReading::from_bytes(&bytes).unwrap();
109        assert_eq!(reading.co2, 0);
110        assert!((reading.temperature - 0.0).abs() < 0.01);
111        assert!((reading.pressure - 0.0).abs() < 0.1);
112        assert_eq!(reading.humidity, 0);
113        assert_eq!(reading.battery, 0);
114        assert_eq!(reading.status, Status::Error);
115        assert_eq!(reading.interval, 0);
116        assert_eq!(reading.age, 0);
117    }
118
119    #[test]
120    fn test_parse_current_reading_max_values() {
121        let bytes: [u8; 13] = [
122            0xFF, 0xFF, // CO2 = 65535
123            0xFF, 0xFF, // temp_raw = 65535
124            0xFF, 0xFF, // pressure_raw = 65535
125            0xFF, // humidity = 255
126            0xFF, // battery = 255
127            3,    // status = Red
128            0xFF, 0xFF, // interval = 65535
129            0xFF, 0xFF, // age = 65535
130        ];
131
132        let reading = CurrentReading::from_bytes(&bytes).unwrap();
133        assert_eq!(reading.co2, 65535);
134        assert!((reading.temperature - 3276.75).abs() < 0.01); // 65535/20
135        assert!((reading.pressure - 6553.5).abs() < 0.1); // 65535/10
136        assert_eq!(reading.humidity, 255);
137        assert_eq!(reading.battery, 255);
138        assert_eq!(reading.interval, 65535);
139        assert_eq!(reading.age, 65535);
140    }
141
142    #[test]
143    fn test_parse_current_reading_high_co2_red_status() {
144        // 2000 ppm CO2 = Red status
145        let bytes: [u8; 13] = [
146            0xD0, 0x07, // CO2 = 2000
147            0xC2, 0x01, // temp
148            0x94, 0x27, // pressure
149            50, 80, 3, // Red status
150            0x2C, 0x01, 0x78, 0x00,
151        ];
152
153        let reading = CurrentReading::from_bytes(&bytes).unwrap();
154        assert_eq!(reading.co2, 2000);
155        assert_eq!(reading.status, Status::Red);
156    }
157
158    #[test]
159    fn test_parse_current_reading_moderate_co2_yellow_status() {
160        // 1200 ppm CO2 = Yellow status
161        let bytes: [u8; 13] = [
162            0xB0, 0x04, // CO2 = 1200
163            0xC2, 0x01, 0x94, 0x27, 50, 80, 2, // Yellow status
164            0x2C, 0x01, 0x78, 0x00,
165        ];
166
167        let reading = CurrentReading::from_bytes(&bytes).unwrap();
168        assert_eq!(reading.co2, 1200);
169        assert_eq!(reading.status, Status::Yellow);
170    }
171
172    #[test]
173    fn test_parse_current_reading_extra_bytes_ignored() {
174        // More than 13 bytes should work (extra bytes ignored)
175        let bytes: [u8; 16] = [
176            0x20, 0x03, 0xC2, 0x01, 0x94, 0x27, 45, 85, 1, 0x2C, 0x01, 0x78, 0x00, 0xAA, 0xBB, 0xCC,
177        ];
178
179        let reading = CurrentReading::from_bytes(&bytes).unwrap();
180        assert_eq!(reading.co2, 800);
181    }
182
183    // --- Status enum tests ---
184
185    #[test]
186    fn test_status_from_u8() {
187        assert_eq!(Status::from(0), Status::Error);
188        assert_eq!(Status::from(1), Status::Green);
189        assert_eq!(Status::from(2), Status::Yellow);
190        assert_eq!(Status::from(3), Status::Red);
191        // Unknown values should map to Error
192        assert_eq!(Status::from(4), Status::Error);
193        assert_eq!(Status::from(255), Status::Error);
194    }
195
196    #[test]
197    fn test_status_repr_values() {
198        assert_eq!(Status::Error as u8, 0);
199        assert_eq!(Status::Green as u8, 1);
200        assert_eq!(Status::Yellow as u8, 2);
201        assert_eq!(Status::Red as u8, 3);
202    }
203
204    #[test]
205    fn test_status_debug() {
206        assert_eq!(format!("{:?}", Status::Green), "Green");
207        assert_eq!(format!("{:?}", Status::Yellow), "Yellow");
208        assert_eq!(format!("{:?}", Status::Red), "Red");
209        assert_eq!(format!("{:?}", Status::Error), "Error");
210    }
211
212    #[test]
213    fn test_status_clone() {
214        let status = Status::Green;
215        // Status implements Copy, so we can just copy it
216        let cloned = status;
217        assert_eq!(status, cloned);
218    }
219
220    #[test]
221    fn test_status_copy() {
222        let status = Status::Red;
223        let copied = status; // Copy
224        assert_eq!(status, copied); // Original still valid
225    }
226
227    // --- DeviceType enum tests ---
228
229    #[test]
230    fn test_device_type_values() {
231        assert_eq!(DeviceType::Aranet4 as u8, 0xF1);
232        assert_eq!(DeviceType::Aranet2 as u8, 0xF2);
233        assert_eq!(DeviceType::AranetRadon as u8, 0xF3);
234        assert_eq!(DeviceType::AranetRadiation as u8, 0xF4);
235    }
236
237    #[test]
238    fn test_device_type_debug() {
239        assert_eq!(format!("{:?}", DeviceType::Aranet4), "Aranet4");
240        assert_eq!(format!("{:?}", DeviceType::Aranet2), "Aranet2");
241        assert_eq!(format!("{:?}", DeviceType::AranetRadon), "AranetRadon");
242        assert_eq!(
243            format!("{:?}", DeviceType::AranetRadiation),
244            "AranetRadiation"
245        );
246    }
247
248    #[test]
249    fn test_device_type_clone() {
250        let device_type = DeviceType::Aranet4;
251        // DeviceType implements Copy, so we can just copy it
252        let cloned = device_type;
253        assert_eq!(device_type, cloned);
254    }
255
256    #[test]
257    fn test_device_type_try_from_u8() {
258        assert_eq!(DeviceType::try_from(0xF1), Ok(DeviceType::Aranet4));
259        assert_eq!(DeviceType::try_from(0xF2), Ok(DeviceType::Aranet2));
260        assert_eq!(DeviceType::try_from(0xF3), Ok(DeviceType::AranetRadon));
261        assert_eq!(DeviceType::try_from(0xF4), Ok(DeviceType::AranetRadiation));
262    }
263
264    #[test]
265    fn test_device_type_try_from_u8_invalid() {
266        let result = DeviceType::try_from(0x00);
267        assert!(result.is_err());
268        assert_eq!(result.unwrap_err(), ParseError::UnknownDeviceType(0x00));
269
270        let result = DeviceType::try_from(0xFF);
271        assert!(result.is_err());
272        assert_eq!(result.unwrap_err(), ParseError::UnknownDeviceType(0xFF));
273    }
274
275    #[test]
276    fn test_device_type_display() {
277        assert_eq!(format!("{}", DeviceType::Aranet4), "Aranet4");
278        assert_eq!(format!("{}", DeviceType::Aranet2), "Aranet2");
279        assert_eq!(format!("{}", DeviceType::AranetRadon), "Aranet Radon");
280        assert_eq!(
281            format!("{}", DeviceType::AranetRadiation),
282            "Aranet Radiation"
283        );
284    }
285
286    #[test]
287    fn test_device_type_hash() {
288        use std::collections::HashSet;
289        let mut set = HashSet::new();
290        set.insert(DeviceType::Aranet4);
291        set.insert(DeviceType::Aranet2);
292        set.insert(DeviceType::Aranet4); // duplicate
293        assert_eq!(set.len(), 2);
294        assert!(set.contains(&DeviceType::Aranet4));
295        assert!(set.contains(&DeviceType::Aranet2));
296    }
297
298    #[test]
299    fn test_status_display() {
300        assert_eq!(format!("{}", Status::Error), "Error");
301        assert_eq!(format!("{}", Status::Green), "Good");
302        assert_eq!(format!("{}", Status::Yellow), "Moderate");
303        assert_eq!(format!("{}", Status::Red), "High");
304    }
305
306    #[test]
307    fn test_status_hash() {
308        use std::collections::HashSet;
309        let mut set = HashSet::new();
310        set.insert(Status::Green);
311        set.insert(Status::Yellow);
312        set.insert(Status::Green); // duplicate
313        assert_eq!(set.len(), 2);
314        assert!(set.contains(&Status::Green));
315        assert!(set.contains(&Status::Yellow));
316    }
317
318    // --- DeviceInfo tests ---
319
320    #[test]
321    fn test_device_info_creation() {
322        let info = types::DeviceInfo {
323            name: "Aranet4 12345".to_string(),
324            model: "Aranet4".to_string(),
325            serial: "12345".to_string(),
326            firmware: "v1.2.0".to_string(),
327            hardware: "1.0".to_string(),
328            software: "1.2.0".to_string(),
329            manufacturer: "SAF Tehnika".to_string(),
330        };
331
332        assert_eq!(info.name, "Aranet4 12345");
333        assert_eq!(info.serial, "12345");
334        assert_eq!(info.manufacturer, "SAF Tehnika");
335    }
336
337    #[test]
338    fn test_device_info_clone() {
339        let info = types::DeviceInfo {
340            name: "Test".to_string(),
341            model: "Model".to_string(),
342            serial: "123".to_string(),
343            firmware: "1.0".to_string(),
344            hardware: "1.0".to_string(),
345            software: "1.0".to_string(),
346            manufacturer: "Mfg".to_string(),
347        };
348
349        let cloned = info.clone();
350        assert_eq!(cloned.name, info.name);
351        assert_eq!(cloned.serial, info.serial);
352    }
353
354    #[test]
355    fn test_device_info_debug() {
356        let info = types::DeviceInfo {
357            name: "Aranet4".to_string(),
358            model: "".to_string(),
359            serial: "".to_string(),
360            firmware: "".to_string(),
361            hardware: "".to_string(),
362            software: "".to_string(),
363            manufacturer: "".to_string(),
364        };
365
366        let debug_str = format!("{:?}", info);
367        assert!(debug_str.contains("Aranet4"));
368    }
369
370    #[test]
371    fn test_device_info_default() {
372        let info = types::DeviceInfo::default();
373        assert_eq!(info.name, "");
374        assert_eq!(info.model, "");
375        assert_eq!(info.serial, "");
376        assert_eq!(info.firmware, "");
377        assert_eq!(info.hardware, "");
378        assert_eq!(info.software, "");
379        assert_eq!(info.manufacturer, "");
380    }
381
382    #[test]
383    fn test_device_info_equality() {
384        let info1 = types::DeviceInfo {
385            name: "Test".to_string(),
386            model: "Model".to_string(),
387            serial: "123".to_string(),
388            firmware: "1.0".to_string(),
389            hardware: "1.0".to_string(),
390            software: "1.0".to_string(),
391            manufacturer: "Mfg".to_string(),
392        };
393        let info2 = info1.clone();
394        let info3 = types::DeviceInfo {
395            name: "Different".to_string(),
396            ..info1.clone()
397        };
398        assert_eq!(info1, info2);
399        assert_ne!(info1, info3);
400    }
401
402    // --- HistoryRecord tests ---
403
404    #[test]
405    fn test_history_record_creation() {
406        use time::OffsetDateTime;
407
408        let record = types::HistoryRecord {
409            timestamp: OffsetDateTime::UNIX_EPOCH,
410            co2: 800,
411            temperature: 22.5,
412            pressure: 1013.2,
413            humidity: 45,
414            radon: None,
415            radiation_rate: None,
416            radiation_total: None,
417        };
418
419        assert_eq!(record.co2, 800);
420        assert!((record.temperature - 22.5).abs() < 0.01);
421        assert!((record.pressure - 1013.2).abs() < 0.1);
422        assert_eq!(record.humidity, 45);
423        assert!(record.radon.is_none());
424        assert!(record.radiation_rate.is_none());
425        assert!(record.radiation_total.is_none());
426    }
427
428    #[test]
429    fn test_history_record_clone() {
430        use time::OffsetDateTime;
431
432        let record = types::HistoryRecord {
433            timestamp: OffsetDateTime::UNIX_EPOCH,
434            co2: 500,
435            temperature: 20.0,
436            pressure: 1000.0,
437            humidity: 50,
438            radon: Some(100),
439            radiation_rate: Some(0.15),
440            radiation_total: Some(1.5),
441        };
442
443        let cloned = record.clone();
444        assert_eq!(cloned.co2, record.co2);
445        assert_eq!(cloned.humidity, record.humidity);
446        assert_eq!(cloned.radon, Some(100));
447        assert_eq!(cloned.radiation_rate, Some(0.15));
448        assert_eq!(cloned.radiation_total, Some(1.5));
449    }
450
451    #[test]
452    fn test_history_record_equality() {
453        use time::OffsetDateTime;
454
455        let record1 = types::HistoryRecord {
456            timestamp: OffsetDateTime::UNIX_EPOCH,
457            co2: 800,
458            temperature: 22.5,
459            pressure: 1013.2,
460            humidity: 45,
461            radon: None,
462            radiation_rate: None,
463            radiation_total: None,
464        };
465        let record2 = record1.clone();
466        assert_eq!(record1, record2);
467    }
468
469    #[test]
470    fn test_current_reading_equality() {
471        let reading1 = CurrentReading {
472            co2: 800,
473            temperature: 22.5,
474            pressure: 1013.2,
475            humidity: 45,
476            battery: 85,
477            status: Status::Green,
478            interval: 300,
479            age: 120,
480            captured_at: None,
481            radon: None,
482            radiation_rate: None,
483            radiation_total: None,
484            radon_avg_24h: None,
485            radon_avg_7d: None,
486            radon_avg_30d: None,
487        };
488        // CurrentReading implements Copy, so we can just copy it
489        let reading2 = reading1;
490        assert_eq!(reading1, reading2);
491    }
492
493    #[test]
494    fn test_min_current_reading_bytes_const() {
495        assert_eq!(MIN_CURRENT_READING_BYTES, 13);
496        // Ensure buffer of exact size works
497        let bytes = [0u8; MIN_CURRENT_READING_BYTES];
498        assert!(CurrentReading::from_bytes(&bytes).is_ok());
499        // Ensure buffer one byte short fails
500        let short_bytes = [0u8; MIN_CURRENT_READING_BYTES - 1];
501        assert!(CurrentReading::from_bytes(&short_bytes).is_err());
502    }
503
504    // --- ParseError tests ---
505
506    #[test]
507    fn test_parse_error_display() {
508        let err = ParseError::invalid_value("test message");
509        assert_eq!(err.to_string(), "Invalid value: test message");
510    }
511
512    #[test]
513    fn test_parse_error_insufficient_bytes() {
514        let err = ParseError::InsufficientBytes {
515            expected: 13,
516            actual: 5,
517        };
518        assert_eq!(err.to_string(), "Insufficient bytes: expected 13, got 5");
519    }
520
521    #[test]
522    fn test_parse_error_unknown_device_type() {
523        let err = ParseError::UnknownDeviceType(0xAB);
524        assert_eq!(err.to_string(), "Unknown device type: 0xAB");
525    }
526
527    #[test]
528    fn test_parse_error_invalid_value() {
529        let err = ParseError::InvalidValue("bad value".to_string());
530        assert_eq!(err.to_string(), "Invalid value: bad value");
531    }
532
533    #[test]
534    fn test_parse_error_debug() {
535        let err = ParseError::invalid_value("debug test");
536        let debug_str = format!("{:?}", err);
537        assert!(debug_str.contains("InvalidValue"));
538        assert!(debug_str.contains("debug test"));
539    }
540
541    #[test]
542    fn test_parse_error_equality() {
543        let err1 = ParseError::InsufficientBytes {
544            expected: 10,
545            actual: 5,
546        };
547        let err2 = ParseError::InsufficientBytes {
548            expected: 10,
549            actual: 5,
550        };
551        let err3 = ParseError::InsufficientBytes {
552            expected: 10,
553            actual: 6,
554        };
555        assert_eq!(err1, err2);
556        assert_ne!(err1, err3);
557    }
558
559    // --- Serialization tests ---
560
561    #[test]
562    fn test_current_reading_serialization() {
563        let reading = CurrentReading {
564            co2: 800,
565            temperature: 22.5,
566            pressure: 1013.2,
567            humidity: 45,
568            battery: 85,
569            status: Status::Green,
570            interval: 300,
571            age: 120,
572            captured_at: None,
573            radon: None,
574            radiation_rate: None,
575            radiation_total: None,
576            radon_avg_24h: None,
577            radon_avg_7d: None,
578            radon_avg_30d: None,
579        };
580
581        let json = serde_json::to_string(&reading).unwrap();
582        assert!(json.contains("\"co2\":800"));
583        assert!(json.contains("\"humidity\":45"));
584    }
585
586    #[test]
587    fn test_current_reading_deserialization() {
588        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}"#;
589
590        let reading: CurrentReading = serde_json::from_str(json).unwrap();
591        assert_eq!(reading.co2, 800);
592        assert_eq!(reading.status, Status::Green);
593    }
594
595    #[test]
596    fn test_status_serialization() {
597        assert_eq!(serde_json::to_string(&Status::Green).unwrap(), "\"Green\"");
598        assert_eq!(
599            serde_json::to_string(&Status::Yellow).unwrap(),
600            "\"Yellow\""
601        );
602        assert_eq!(serde_json::to_string(&Status::Red).unwrap(), "\"Red\"");
603        assert_eq!(serde_json::to_string(&Status::Error).unwrap(), "\"Error\"");
604    }
605
606    #[test]
607    fn test_device_type_serialization() {
608        assert_eq!(
609            serde_json::to_string(&DeviceType::Aranet4).unwrap(),
610            "\"Aranet4\""
611        );
612        assert_eq!(
613            serde_json::to_string(&DeviceType::AranetRadon).unwrap(),
614            "\"AranetRadon\""
615        );
616    }
617
618    #[test]
619    fn test_device_info_serialization_roundtrip() {
620        let info = types::DeviceInfo {
621            name: "Test Device".to_string(),
622            model: "Model X".to_string(),
623            serial: "SN12345".to_string(),
624            firmware: "1.2.3".to_string(),
625            hardware: "2.0".to_string(),
626            software: "3.0".to_string(),
627            manufacturer: "Acme Corp".to_string(),
628        };
629
630        let json = serde_json::to_string(&info).unwrap();
631        let deserialized: types::DeviceInfo = serde_json::from_str(&json).unwrap();
632
633        assert_eq!(deserialized.name, info.name);
634        assert_eq!(deserialized.serial, info.serial);
635        assert_eq!(deserialized.manufacturer, info.manufacturer);
636    }
637
638    // --- New feature tests ---
639
640    #[test]
641    fn test_status_ordering() {
642        // Status should be ordered by severity
643        assert!(Status::Error < Status::Green);
644        assert!(Status::Green < Status::Yellow);
645        assert!(Status::Yellow < Status::Red);
646
647        // Test comparison operators
648        assert!(Status::Red > Status::Yellow);
649        assert!(Status::Yellow >= Status::Yellow);
650        assert!(Status::Green <= Status::Yellow);
651    }
652
653    #[test]
654    fn test_device_type_readings_characteristic() {
655        use crate::ble;
656
657        // Aranet4 uses the original characteristic
658        assert_eq!(
659            DeviceType::Aranet4.readings_characteristic(),
660            ble::CURRENT_READINGS_DETAIL
661        );
662
663        // Other devices use the alternate characteristic
664        assert_eq!(
665            DeviceType::Aranet2.readings_characteristic(),
666            ble::CURRENT_READINGS_DETAIL_ALT
667        );
668        assert_eq!(
669            DeviceType::AranetRadon.readings_characteristic(),
670            ble::CURRENT_READINGS_DETAIL_ALT
671        );
672        assert_eq!(
673            DeviceType::AranetRadiation.readings_characteristic(),
674            ble::CURRENT_READINGS_DETAIL_ALT
675        );
676    }
677
678    #[test]
679    fn test_device_type_from_name_word_boundary() {
680        // Should match at word boundaries
681        assert_eq!(
682            DeviceType::from_name("Aranet4 12345"),
683            Some(DeviceType::Aranet4)
684        );
685        assert_eq!(
686            DeviceType::from_name("My Aranet4"),
687            Some(DeviceType::Aranet4)
688        );
689
690        // Should match case-insensitively
691        assert_eq!(DeviceType::from_name("ARANET4"), Some(DeviceType::Aranet4));
692        assert_eq!(DeviceType::from_name("aranet2"), Some(DeviceType::Aranet2));
693
694        // Should match AranetRn+ naming convention (real device name format)
695        assert_eq!(
696            DeviceType::from_name("AranetRn+ 306B8"),
697            Some(DeviceType::AranetRadon)
698        );
699        assert_eq!(
700            DeviceType::from_name("aranetrn+ 12345"),
701            Some(DeviceType::AranetRadon)
702        );
703    }
704
705    #[test]
706    fn test_byte_size_constants() {
707        assert_eq!(MIN_CURRENT_READING_BYTES, 13);
708        assert_eq!(types::MIN_ARANET2_READING_BYTES, 7);
709        assert_eq!(types::MIN_RADON_READING_BYTES, 15);
710        assert_eq!(types::MIN_RADON_GATT_READING_BYTES, 18);
711        assert_eq!(types::MIN_RADIATION_READING_BYTES, 28);
712    }
713
714    #[test]
715    fn test_from_bytes_aranet2() {
716        // 7 bytes: temp(2), humidity(1), battery(1), status(1), interval(2)
717        let data = [
718            0x90, 0x01, // temp = 400 -> 20.0°C
719            0x32, // humidity = 50
720            0x55, // battery = 85
721            0x01, // status = Green
722            0x2C, 0x01, // interval = 300
723        ];
724
725        let reading = CurrentReading::from_bytes_aranet2(&data).unwrap();
726        assert_eq!(reading.co2, 0); // Aranet2 has no CO2
727        assert!((reading.temperature - 20.0).abs() < 0.1);
728        assert_eq!(reading.humidity, 50);
729        assert_eq!(reading.battery, 85);
730        assert_eq!(reading.status, Status::Green);
731        assert_eq!(reading.interval, 300);
732        assert_eq!(reading.pressure, 0.0); // Aranet2 has no pressure
733    }
734
735    #[test]
736    fn test_from_bytes_aranet2_insufficient() {
737        let data = [0u8; 6]; // Too short
738        let result = CurrentReading::from_bytes_aranet2(&data);
739        assert!(result.is_err());
740    }
741
742    #[test]
743    fn test_from_bytes_for_device() {
744        // Test dispatch to correct parser
745        let aranet4_data = [0u8; 13];
746        let result = CurrentReading::from_bytes_for_device(&aranet4_data, DeviceType::Aranet4);
747        assert!(result.is_ok());
748
749        let aranet2_data = [0u8; 7];
750        let result = CurrentReading::from_bytes_for_device(&aranet2_data, DeviceType::Aranet2);
751        assert!(result.is_ok());
752    }
753
754    #[test]
755    fn test_builder_with_captured_at() {
756        use time::OffsetDateTime;
757
758        let now = OffsetDateTime::now_utc();
759        let reading = CurrentReading::builder()
760            .co2(800)
761            .temperature(22.5)
762            .captured_at(now)
763            .build();
764
765        assert_eq!(reading.co2, 800);
766        assert_eq!(reading.captured_at, Some(now));
767    }
768
769    #[test]
770    fn test_builder_try_build_valid() {
771        let result = CurrentReading::builder()
772            .co2(800)
773            .temperature(22.5)
774            .pressure(1013.0)
775            .humidity(50)
776            .battery(85)
777            .try_build();
778
779        assert!(result.is_ok());
780    }
781
782    #[test]
783    fn test_builder_try_build_invalid_humidity() {
784        let result = CurrentReading::builder()
785            .humidity(150) // Invalid: > 100
786            .try_build();
787
788        assert!(result.is_err());
789        let err = result.unwrap_err();
790        assert!(err.to_string().contains("humidity"));
791    }
792
793    #[test]
794    fn test_builder_try_build_invalid_battery() {
795        let result = CurrentReading::builder()
796            .battery(120) // Invalid: > 100
797            .try_build();
798
799        assert!(result.is_err());
800        let err = result.unwrap_err();
801        assert!(err.to_string().contains("battery"));
802    }
803
804    #[test]
805    fn test_builder_try_build_invalid_temperature() {
806        let result = CurrentReading::builder()
807            .temperature(-50.0) // Invalid: < -40
808            .try_build();
809
810        assert!(result.is_err());
811        let err = result.unwrap_err();
812        assert!(err.to_string().contains("temperature"));
813    }
814
815    #[test]
816    fn test_builder_try_build_invalid_pressure() {
817        let result = CurrentReading::builder()
818            .temperature(22.0) // Valid temperature
819            .pressure(500.0) // Invalid: < 800
820            .try_build();
821
822        assert!(result.is_err());
823        let err = result.unwrap_err();
824        assert!(err.to_string().contains("pressure"));
825    }
826
827    #[test]
828    fn test_with_captured_at() {
829        use time::OffsetDateTime;
830
831        let reading = CurrentReading::builder().age(60).build();
832
833        let now = OffsetDateTime::now_utc();
834        let reading_with_time = reading.with_captured_at(now);
835
836        assert!(reading_with_time.captured_at.is_some());
837        // The captured_at should be approximately now - 60 seconds
838        let captured = reading_with_time.captured_at.unwrap();
839        let expected = now - time::Duration::seconds(60);
840        assert!((captured - expected).whole_seconds().abs() < 2);
841    }
842
843    #[test]
844    fn test_parse_error_invalid_value_helper() {
845        let err = ParseError::invalid_value("test error");
846        assert_eq!(err.to_string(), "Invalid value: test error");
847    }
848}