1use bytes::Buf;
13use serde::{Deserialize, Serialize};
14
15use aranet_types::{DeviceType, Status};
16
17use crate::error::{Error, Result};
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct AdvertisementData {
22 pub device_type: DeviceType,
24 pub co2: Option<u16>,
26 pub temperature: Option<f32>,
28 pub pressure: Option<f32>,
30 pub humidity: Option<u8>,
32 pub battery: u8,
34 pub status: Status,
36 pub interval: u16,
38 pub age: u16,
40 pub radon: Option<u32>,
42 pub radiation_dose_rate: Option<f32>,
44 pub counter: Option<u8>,
46 pub flags: u8,
48}
49
50pub fn parse_advertisement(data: &[u8]) -> Result<AdvertisementData> {
62 parse_advertisement_with_name(data, None)
63}
64
65pub fn parse_advertisement_with_name(data: &[u8], name: Option<&str>) -> Result<AdvertisementData> {
70 if data.is_empty() {
71 return Err(Error::InvalidData(
72 "Advertisement data is empty".to_string(),
73 ));
74 }
75
76 let is_aranet4_by_name = name.map(|n| n.starts_with("Aranet4")).unwrap_or(false);
88 let is_aranet4_by_len = data.len() == 7 || data.len() == 22;
89
90 let (device_type, sensor_data) = if is_aranet4_by_name || is_aranet4_by_len {
91 (DeviceType::Aranet4, data)
93 } else {
94 let device_type = match data[0] {
96 0x01 => DeviceType::Aranet2,
97 0x02 => DeviceType::AranetRadiation,
98 0x03 => DeviceType::AranetRadon,
99 other => {
100 return Err(Error::InvalidData(format!(
101 "Unknown device type byte: 0x{:02X}. Expected 0x01 (Aranet2), \
102 0x02 (Radiation), or 0x03 (Radon). Data length: {} bytes.",
103 other,
104 data.len()
105 )));
106 }
107 };
108 (device_type, &data[1..])
109 };
110
111 if sensor_data.is_empty() {
113 return Err(Error::InvalidData(
114 "Advertisement data too short for basic info".to_string(),
115 ));
116 }
117
118 let flags = sensor_data[0];
119 let integrations_enabled = (flags & (1 << 5)) != 0;
120
121 if !integrations_enabled {
122 return Err(Error::InvalidData(
123 "Smart Home integration is not enabled on this device. \
124 To enable: go to device Settings > Smart Home > Enable."
125 .to_string(),
126 ));
127 }
128
129 match device_type {
130 DeviceType::Aranet4 => parse_aranet4_advertisement_v2(sensor_data),
131 DeviceType::Aranet2 => parse_aranet2_advertisement_v2(sensor_data),
132 DeviceType::AranetRadon => parse_aranet_radon_advertisement_v2(sensor_data),
133 DeviceType::AranetRadiation => parse_aranet_radiation_advertisement_v2(sensor_data),
134 _ => Err(Error::InvalidData(format!(
135 "Unsupported device type for advertisement parsing: {:?}",
136 device_type
137 ))),
138 }
139}
140
141fn parse_aranet4_advertisement_v2(data: &[u8]) -> Result<AdvertisementData> {
155 if data.len() < 22 {
157 return Err(Error::InvalidData(format!(
158 "Aranet4 advertisement requires 22 bytes, got {}",
159 data.len()
160 )));
161 }
162
163 let flags = data[0];
164 let mut buf = &data[8..];
166 let co2 = buf.get_u16_le();
167 let temp_raw = buf.get_i16_le();
168 let pressure_raw = buf.get_u16_le();
169 let humidity = buf.get_u8();
170 let battery = buf.get_u8();
171 let status = Status::from(buf.get_u8());
172 let interval = buf.get_u16_le();
173 let age = buf.get_u16_le();
174 let counter = if !buf.is_empty() {
175 Some(buf.get_u8())
176 } else {
177 None
178 };
179
180 Ok(AdvertisementData {
181 device_type: DeviceType::Aranet4,
182 co2: Some(co2),
183 temperature: Some(temp_raw as f32 * 0.05),
184 pressure: Some(pressure_raw as f32 * 0.1),
185 humidity: Some(humidity),
186 battery,
187 status,
188 interval,
189 age,
190 radon: None,
191 radiation_dose_rate: None,
192 counter,
193 flags,
194 })
195}
196
197fn parse_aranet2_advertisement_v2(data: &[u8]) -> Result<AdvertisementData> {
214 if data.len() < 22 {
215 return Err(Error::InvalidData(format!(
216 "Aranet2 advertisement requires at least 22 bytes, got {}",
217 data.len()
218 )));
219 }
220
221 let flags = data[0];
222 let mut buf = &data[9..];
223 let temp_raw = buf.get_i16_le();
224 let _unused = buf.get_u16_le();
225 let humidity_raw = buf.get_u16_le();
226 let _unused = buf.get_u8();
227 let battery = buf.get_u8();
228 let status_raw = buf.get_u8();
229 let status = Status::from((status_raw >> 2) & 0x03);
231 let interval = buf.get_u16_le();
232 let age = buf.get_u16_le();
233 let counter = if !buf.is_empty() {
234 Some(buf.get_u8())
235 } else {
236 None
237 };
238
239 Ok(AdvertisementData {
240 device_type: DeviceType::Aranet2,
241 co2: None,
242 temperature: Some(temp_raw as f32 * 0.05),
243 pressure: None,
244 humidity: Some((humidity_raw as f32 * 0.1).clamp(0.0, 100.0) as u8),
245 battery,
246 status,
247 interval,
248 age,
249 radon: None,
250 radiation_dose_rate: None,
251 counter,
252 flags,
253 })
254}
255
256fn parse_aranet_radon_advertisement_v2(data: &[u8]) -> Result<AdvertisementData> {
272 if data.len() < 22 {
273 return Err(Error::InvalidData(format!(
274 "Aranet Radon advertisement requires at least 22 bytes, got {}",
275 data.len()
276 )));
277 }
278
279 let flags = data[0];
280 let mut buf = &data[7..];
282 let radon = buf.get_u16_le() as u32;
283 let temp_raw = buf.get_i16_le();
284 let pressure_raw = buf.get_u16_le();
285 let humidity_raw = buf.get_u16_le();
286 let _reserved = buf.get_u8(); let battery = buf.get_u8();
288 let status = Status::from(buf.get_u8());
289 let interval = buf.get_u16_le();
290 let age = buf.get_u16_le();
291 let counter = if !buf.is_empty() {
292 Some(buf.get_u8())
293 } else {
294 None
295 };
296
297 Ok(AdvertisementData {
298 device_type: DeviceType::AranetRadon,
299 co2: None,
300 temperature: Some(temp_raw as f32 * 0.05),
301 pressure: Some(pressure_raw as f32 * 0.1),
302 humidity: Some((humidity_raw as f32 * 0.1).clamp(0.0, 100.0) as u8),
303 battery,
304 status,
305 interval,
306 age,
307 radon: Some(radon),
308 radiation_dose_rate: None,
309 counter,
310 flags,
311 })
312}
313
314fn parse_aranet_radiation_advertisement_v2(data: &[u8]) -> Result<AdvertisementData> {
330 if data.len() < 22 {
331 return Err(Error::InvalidData(format!(
332 "Aranet Radiation advertisement requires at least 22 bytes, got {}",
333 data.len()
334 )));
335 }
336
337 let flags = data[0];
338 let mut buf = &data[5..];
339 let _radiation_total = buf.get_u32_le(); let _radiation_duration = buf.get_u32_le(); let radiation_rate_nsv = buf.get_u16_le(); let _unknown = buf.get_u8();
343 let battery = buf.get_u8();
344 let status = Status::from(buf.get_u8());
345 let interval = buf.get_u16_le();
346 let age = buf.get_u16_le();
347 let counter = if !buf.is_empty() {
348 Some(buf.get_u8())
349 } else {
350 None
351 };
352
353 let dose_rate_usv = radiation_rate_nsv as f32 / 1000.0;
355
356 Ok(AdvertisementData {
357 device_type: DeviceType::AranetRadiation,
358 co2: None,
359 temperature: None,
360 pressure: None,
361 humidity: None,
362 battery,
363 status,
364 interval,
365 age,
366 radon: None,
367 radiation_dose_rate: Some(dose_rate_usv),
368 counter,
369 flags,
370 })
371}
372
373#[cfg(test)]
374mod tests {
375 use super::*;
376
377 #[test]
378 fn test_parse_aranet4_advertisement() {
379 let data: [u8; 22] = [
382 0x22, 0x13, 0x04, 0x01, 0x00, 0x0E, 0x0F, 0x01, 0x20, 0x03, 0xC2, 0x01, 0x94, 0x27, 45, 85, 1, 0x2C, 0x01, 0x78, 0x00, 5, ];
394
395 let result = parse_advertisement(&data).unwrap();
396 assert_eq!(result.device_type, DeviceType::Aranet4);
397 assert_eq!(result.co2, Some(800));
398 assert!((result.temperature.unwrap() - 22.5).abs() < 0.01);
399 assert!((result.pressure.unwrap() - 1013.2).abs() < 0.1);
400 assert_eq!(result.humidity, Some(45));
401 assert_eq!(result.battery, 85);
402 assert_eq!(result.status, Status::Green);
403 assert_eq!(result.interval, 300);
404 assert_eq!(result.age, 120);
405 }
406
407 #[test]
408 fn test_parse_aranet2_advertisement() {
409 let data: [u8; 24] = [
412 0x01, 0x20, 0x13, 0x04, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC2, 0x01, 0x00, 0x00, 0xC2, 0x01, 0x00, 85, 0x04, 0x2C, 0x01, 0x3C, 0x00, 0x07, ];
425
426 let result = parse_advertisement(&data).unwrap();
427 assert_eq!(result.device_type, DeviceType::Aranet2);
428 assert!(result.co2.is_none());
429 assert!((result.temperature.unwrap() - 22.5).abs() < 0.01);
430 assert_eq!(result.humidity, Some(45));
431 assert_eq!(result.battery, 85);
432 assert_eq!(result.status, Status::Green);
433 }
434
435 const ARANET2_CAPTURE: [u8; 24] = [
439 0x01, 0x21, 0x04, 0x04, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x01, 0x00, 0x00, 0x0c,
440 0x02, 0x00, 0x4f, 0x00, 0x3c, 0x00, 0x01, 0x00, 0x80,
441 ];
442
443 #[test]
444 fn test_parse_aranet2_real_capture() {
445 let result = parse_advertisement(&ARANET2_CAPTURE).unwrap();
446 assert_eq!(result.device_type, DeviceType::Aranet2);
447 assert!(
448 (result.temperature.unwrap() - 24.8).abs() < 0.01,
449 "temperature was {:?}",
450 result.temperature
451 );
452 assert_eq!(result.humidity, Some(52));
453 assert_eq!(result.battery, 79);
454 assert_eq!(result.interval, 60);
455 assert_eq!(result.age, 1);
456 assert_eq!(result.counter, Some(0x80));
457 }
458
459 #[test]
460 fn test_parse_aranet_radon_advertisement() {
461 let data: [u8; 24] = [
465 0x03, 0x21, 0x00, 0x0C, 0x01, 0x00, 0x00, 0x00, 0x51, 0x00, 0xC2, 0x01, 0x94, 0x27, 0xC2, 0x01, 0x00, 85, 1, 0x2C, 0x01, 0x3C, 0x00, 5, ];
479
480 let result = parse_advertisement(&data).unwrap();
481 assert_eq!(result.device_type, DeviceType::AranetRadon);
482 assert!(result.co2.is_none());
483 assert!((result.temperature.unwrap() - 22.5).abs() < 0.01);
484 assert!((result.pressure.unwrap() - 1013.2).abs() < 0.1);
485 assert_eq!(result.humidity, Some(45));
486 assert_eq!(result.radon, Some(81));
487 assert_eq!(result.battery, 85);
488 assert_eq!(result.status, Status::Green);
489 }
490
491 #[test]
492 fn test_parse_empty_data() {
493 let result = parse_advertisement(&[]);
494 assert!(result.is_err());
495 assert!(result.unwrap_err().to_string().contains("empty"));
496 }
497
498 #[test]
499 fn test_parse_unknown_device_type() {
500 let data: [u8; 16] = [0xFF; 16];
503 let result = parse_advertisement(&data);
504 assert!(result.is_err());
505 let err_msg = result.unwrap_err().to_string();
506 assert!(
507 err_msg.contains("Unknown device type byte"),
508 "Expected unknown device type error, got: {}",
509 err_msg
510 );
511 }
512
513 #[test]
514 fn test_parse_aranet4_insufficient_bytes() {
515 let data: [u8; 10] = [0x22; 10];
519 let result = parse_advertisement(&data);
520 assert!(result.is_err());
521 let err_msg = result.unwrap_err().to_string();
522 assert!(
523 err_msg.contains("Unknown device type byte"),
524 "Expected unknown device type error, got: {}",
525 err_msg
526 );
527 }
528
529 const RADIATION_CAPTURE: [u8; 24] = [
532 0x02, 0x21, 0x26, 0x04, 0x01, 0x00, 0x60, 0x2d, 0x00, 0x00, 0x08, 0x98, 0x05, 0x00, 0x6e,
533 0x00, 0x00, 0x64, 0x00, 0x2c, 0x01, 0xfd, 0x00, 0xc7,
534 ];
535
536 #[test]
537 fn test_parse_aranet_radiation_real_capture() {
538 let result = parse_advertisement(&RADIATION_CAPTURE).unwrap();
539 assert_eq!(result.device_type, DeviceType::AranetRadiation);
540 let rate = result.radiation_dose_rate.unwrap();
541 assert!((rate - 0.11).abs() < 0.001, "dose rate was {rate} µSv/h");
542 assert_eq!(result.battery, 100);
543 assert_eq!(result.interval, 300);
544 assert_eq!(result.age, 253);
545 assert_eq!(result.counter, Some(0xc7));
546 }
547
548 #[test]
549 fn test_parse_aranet_radiation_advertisement() {
550 let data: [u8; 24] = [
554 0x02, 0x20, 0x13, 0x04, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE8, 0x03, 0x00, 85, 1, 0x2C, 0x01, 0x3C, 0x00, 5, ];
567
568 let result = parse_advertisement(&data).unwrap();
569 assert_eq!(result.device_type, DeviceType::AranetRadiation);
570 assert!(result.co2.is_none());
571 assert!(result.temperature.is_none());
572 assert!(result.radon.is_none());
573 assert!((result.radiation_dose_rate.unwrap() - 1.0).abs() < 0.001);
574 assert_eq!(result.battery, 85);
575 assert_eq!(result.status, Status::Green);
576 assert_eq!(result.interval, 300);
577 assert_eq!(result.age, 60);
578 }
579
580 #[test]
581 fn test_parse_aranet_radiation_insufficient_bytes() {
582 let data: [u8; 10] = [0x02, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
584 let result = parse_advertisement(&data);
585 assert!(result.is_err());
586 let err_msg = result.unwrap_err().to_string();
587 assert!(
588 err_msg.contains("requires at least 22 bytes"),
589 "Expected insufficient bytes error, got: {}",
590 err_msg
591 );
592 }
593
594 #[test]
595 fn test_parse_smart_home_not_enabled() {
596 let data: [u8; 22] = [
598 0x00, 0x13, 0x04, 0x01, 0x00, 0x0E, 0x0F, 0x01, 0x20, 0x03, 0xC2, 0x01, 0x94, 0x27, 45, 85, 1, 0x2C, 0x01, 0x78, 0x00, 5, ];
608
609 let result = parse_advertisement(&data);
610 assert!(result.is_err());
611 let err_msg = result.unwrap_err().to_string();
612 assert!(
613 err_msg.contains("Smart Home integration is not enabled"),
614 "Expected Smart Home error, got: {}",
615 err_msg
616 );
617 }
618}
619
620#[cfg(test)]
640mod proptests {
641 use super::*;
642 use proptest::prelude::*;
643
644 proptest! {
645 #[test]
648 fn parse_advertisement_never_panics(data: Vec<u8>) {
649 let _ = parse_advertisement(&data);
650 }
651
652 #[test]
654 fn parse_aranet4_advertisement_never_panics(data in proptest::collection::vec(any::<u8>(), 22)) {
655 let _ = parse_advertisement(&data);
656 }
657
658 #[test]
660 fn parse_aranet2_advertisement_never_panics(data in proptest::collection::vec(any::<u8>(), 19..=30)) {
661 let mut modified = data.clone();
662 if !modified.is_empty() {
663 modified[0] = 0x01; }
665 let _ = parse_advertisement(&modified);
666 }
667
668 #[test]
670 fn parse_aranet_radon_advertisement_never_panics(data in proptest::collection::vec(any::<u8>(), 23..=30)) {
671 let mut modified = data.clone();
672 if !modified.is_empty() {
673 modified[0] = 0x03; }
675 let _ = parse_advertisement(&modified);
676 }
677
678 #[test]
680 fn parse_aranet_radiation_advertisement_never_panics(data in proptest::collection::vec(any::<u8>(), 19..=30)) {
681 let mut modified = data.clone();
682 if !modified.is_empty() {
683 modified[0] = 0x02; }
685 let _ = parse_advertisement(&modified);
686 }
687 }
688}