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_u16_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
197#[allow(dead_code)]
199fn parse_aranet4_advertisement(data: &[u8]) -> Result<AdvertisementData> {
200 if data.len() < 16 {
201 return Err(Error::InvalidData(format!(
202 "Aranet4 advertisement requires 16 bytes, got {}",
203 data.len()
204 )));
205 }
206
207 let mut buf = &data[1..]; let flags = buf.get_u8();
209 let co2 = buf.get_u16_le();
210 let temp_raw = buf.get_u16_le();
211 let pressure_raw = buf.get_u16_le();
212 let humidity = buf.get_u8();
213 let battery = buf.get_u8();
214 let status = Status::from(buf.get_u8());
215 let interval = buf.get_u16_le();
216 let age = buf.get_u16_le();
217 let counter = buf.get_u8();
218
219 Ok(AdvertisementData {
220 device_type: DeviceType::Aranet4,
221 co2: Some(co2),
222 temperature: Some(temp_raw as f32 / 20.0),
223 pressure: Some(pressure_raw as f32 / 10.0),
224 humidity: Some(humidity),
225 battery,
226 status,
227 interval,
228 age,
229 radon: None,
230 radiation_dose_rate: None,
231 counter: Some(counter),
232 flags,
233 })
234}
235
236fn parse_aranet2_advertisement_v2(data: &[u8]) -> Result<AdvertisementData> {
249 if data.len() < 19 {
250 return Err(Error::InvalidData(format!(
251 "Aranet2 advertisement requires at least 19 bytes, got {}",
252 data.len()
253 )));
254 }
255
256 let flags = data[0];
257 let mut buf = &data[7..];
259 let temp_raw = buf.get_u16_le();
260 let _unused = buf.get_u16_le();
261 let humidity_raw = buf.get_u16_le();
262 let battery = buf.get_u8();
263 let status_raw = buf.get_u8();
264 let status = Status::from(status_raw & 0x03);
266 let interval = buf.get_u16_le();
267 let age = buf.get_u16_le();
268 let counter = if !buf.is_empty() {
269 Some(buf.get_u8())
270 } else {
271 None
272 };
273
274 Ok(AdvertisementData {
275 device_type: DeviceType::Aranet2,
276 co2: None,
277 temperature: Some(temp_raw as f32 * 0.05),
278 pressure: None,
279 humidity: Some((humidity_raw as f32 * 0.1).min(255.0) as u8),
280 battery,
281 status,
282 interval,
283 age,
284 radon: None,
285 radiation_dose_rate: None,
286 counter,
287 flags,
288 })
289}
290
291fn parse_aranet_radon_advertisement_v2(data: &[u8]) -> Result<AdvertisementData> {
307 if data.len() < 22 {
308 return Err(Error::InvalidData(format!(
309 "Aranet Radon advertisement requires at least 22 bytes, got {}",
310 data.len()
311 )));
312 }
313
314 let flags = data[0];
315 let mut buf = &data[7..];
317 let radon = buf.get_u16_le() as u32;
318 let temp_raw = buf.get_u16_le();
319 let pressure_raw = buf.get_u16_le();
320 let humidity_raw = buf.get_u16_le();
321 let _reserved = buf.get_u8(); let battery = buf.get_u8();
323 let status = Status::from(buf.get_u8());
324 let interval = buf.get_u16_le();
325 let age = buf.get_u16_le();
326 let counter = if !buf.is_empty() {
327 Some(buf.get_u8())
328 } else {
329 None
330 };
331
332 Ok(AdvertisementData {
333 device_type: DeviceType::AranetRadon,
334 co2: None,
335 temperature: Some(temp_raw as f32 * 0.05),
336 pressure: Some(pressure_raw as f32 * 0.1),
337 humidity: Some((humidity_raw as f32 * 0.1).min(255.0) as u8),
338 battery,
339 status,
340 interval,
341 age,
342 radon: Some(radon),
343 radiation_dose_rate: None,
344 counter,
345 flags,
346 })
347}
348
349fn parse_aranet_radiation_advertisement_v2(data: &[u8]) -> Result<AdvertisementData> {
362 if data.len() < 21 {
364 return Err(Error::InvalidData(format!(
365 "Aranet Radiation advertisement requires at least 21 bytes, got {}",
366 data.len()
367 )));
368 }
369
370 let flags = data[0];
371 let mut buf = &data[5..];
373 let _radiation_total = buf.get_u32_le(); let _radiation_duration = buf.get_u32_le(); let radiation_rate_raw = buf.get_u16_le(); let battery = buf.get_u8();
377 let status = Status::from(buf.get_u8());
378 let interval = buf.get_u16_le();
379 let age = buf.get_u16_le();
380 let counter = if !buf.is_empty() {
381 Some(buf.get_u8())
382 } else {
383 None
384 };
385
386 let dose_rate_usv = (radiation_rate_raw as f32 * 10.0) / 1000.0;
388
389 Ok(AdvertisementData {
390 device_type: DeviceType::AranetRadiation,
391 co2: None,
392 temperature: None,
393 pressure: None,
394 humidity: None,
395 battery,
396 status,
397 interval,
398 age,
399 radon: None,
400 radiation_dose_rate: Some(dose_rate_usv),
401 counter,
402 flags,
403 })
404}
405
406#[allow(dead_code)]
408fn parse_aranet2_advertisement(data: &[u8]) -> Result<AdvertisementData> {
409 if data.len() < 12 {
410 return Err(Error::InvalidData(format!(
411 "Aranet2 advertisement requires at least 12 bytes, got {}",
412 data.len()
413 )));
414 }
415
416 let mut buf = &data[1..];
417 let flags = buf.get_u8();
418 let temp_raw = buf.get_u16_le();
419 let humidity_raw = buf.get_u16_le();
420 let battery = buf.get_u8();
421 let status = Status::from(buf.get_u8());
422 let interval = buf.get_u16_le();
423 let age = buf.get_u16_le();
424
425 Ok(AdvertisementData {
426 device_type: DeviceType::Aranet2,
427 co2: None,
428 temperature: Some(temp_raw as f32 / 20.0),
429 pressure: None,
430 humidity: Some((humidity_raw / 10).min(255) as u8),
431 battery,
432 status,
433 interval,
434 age,
435 radon: None,
436 radiation_dose_rate: None,
437 counter: None,
438 flags,
439 })
440}
441
442#[allow(dead_code)]
444fn parse_aranet_radon_advertisement(data: &[u8]) -> Result<AdvertisementData> {
445 if data.len() < 18 {
446 return Err(Error::InvalidData(format!(
447 "Aranet Radon advertisement requires at least 18 bytes, got {}",
448 data.len()
449 )));
450 }
451
452 let mut buf = &data[1..];
453 let flags = buf.get_u8();
454 let temp_raw = buf.get_u16_le();
455 let pressure_raw = buf.get_u16_le();
456 let humidity_raw = buf.get_u16_le();
457 let battery = buf.get_u8();
458 let status = Status::from(buf.get_u8());
459 let interval = buf.get_u16_le();
460 let age = buf.get_u16_le();
461 let radon = buf.get_u32_le();
462
463 Ok(AdvertisementData {
464 device_type: DeviceType::AranetRadon,
465 co2: None,
466 temperature: Some(temp_raw as f32 / 20.0),
467 pressure: Some(pressure_raw as f32 / 10.0),
468 humidity: Some((humidity_raw / 10).min(255) as u8),
469 battery,
470 status,
471 interval,
472 age,
473 radon: Some(radon),
474 radiation_dose_rate: None,
475 counter: None,
476 flags,
477 })
478}
479
480#[allow(dead_code)]
482fn parse_aranet_radiation_advertisement(data: &[u8]) -> Result<AdvertisementData> {
483 if data.len() < 16 {
484 return Err(Error::InvalidData(format!(
485 "Aranet Radiation advertisement requires at least 16 bytes, got {}",
486 data.len()
487 )));
488 }
489
490 let mut buf = &data[1..];
491 let flags = buf.get_u8();
492 let battery = buf.get_u8();
493 let status = Status::from(buf.get_u8());
494 let interval = buf.get_u16_le();
495 let age = buf.get_u16_le();
496 let dose_rate_nsv = buf.get_u32_le();
498 let dose_rate_usv = dose_rate_nsv as f32 / 1000.0;
499
500 Ok(AdvertisementData {
501 device_type: DeviceType::AranetRadiation,
502 co2: None,
503 temperature: None,
504 pressure: None,
505 humidity: None,
506 battery,
507 status,
508 interval,
509 age,
510 radon: None,
511 radiation_dose_rate: Some(dose_rate_usv),
512 counter: None,
513 flags,
514 })
515}
516
517#[cfg(test)]
518mod tests {
519 use super::*;
520
521 #[test]
522 fn test_parse_aranet4_advertisement() {
523 let data: [u8; 22] = [
526 0x22, 0x13, 0x04, 0x01, 0x00, 0x0E, 0x0F, 0x01, 0x20, 0x03, 0xC2, 0x01, 0x94, 0x27, 45, 85, 1, 0x2C, 0x01, 0x78, 0x00, 5, ];
538
539 let result = parse_advertisement(&data).unwrap();
540 assert_eq!(result.device_type, DeviceType::Aranet4);
541 assert_eq!(result.co2, Some(800));
542 assert!((result.temperature.unwrap() - 22.5).abs() < 0.01);
543 assert!((result.pressure.unwrap() - 1013.2).abs() < 0.1);
544 assert_eq!(result.humidity, Some(45));
545 assert_eq!(result.battery, 85);
546 assert_eq!(result.status, Status::Green);
547 assert_eq!(result.interval, 300);
548 assert_eq!(result.age, 120);
549 }
550
551 #[test]
552 fn test_parse_aranet2_advertisement() {
553 let data: [u8; 20] = [
556 0x01, 0x20, 0x13, 0x04, 0x01, 0x00, 0x0E, 0x0F, 0xC2, 0x01, 0x00, 0x00, 0xC2, 0x01, 85, 1, 0x2C, 0x01, 0x3C, 0x00, ];
567
568 let result = parse_advertisement(&data).unwrap();
569 assert_eq!(result.device_type, DeviceType::Aranet2);
570 assert!(result.co2.is_none());
571 assert!((result.temperature.unwrap() - 22.5).abs() < 0.01);
572 assert_eq!(result.humidity, Some(45));
573 assert_eq!(result.battery, 85);
574 }
575
576 #[test]
577 fn test_parse_aranet_radon_advertisement() {
578 let data: [u8; 24] = [
582 0x03, 0x21, 0x00, 0x0C, 0x01, 0x00, 0x00, 0x00, 0x51, 0x00, 0xC2, 0x01, 0x94, 0x27, 0xC2, 0x01, 0x00, 85, 1, 0x2C, 0x01, 0x3C, 0x00, 5, ];
596
597 let result = parse_advertisement(&data).unwrap();
598 assert_eq!(result.device_type, DeviceType::AranetRadon);
599 assert!(result.co2.is_none());
600 assert!((result.temperature.unwrap() - 22.5).abs() < 0.01);
601 assert!((result.pressure.unwrap() - 1013.2).abs() < 0.1);
602 assert_eq!(result.humidity, Some(45));
603 assert_eq!(result.radon, Some(81));
604 assert_eq!(result.battery, 85);
605 assert_eq!(result.status, Status::Green);
606 }
607
608 #[test]
609 fn test_parse_empty_data() {
610 let result = parse_advertisement(&[]);
611 assert!(result.is_err());
612 assert!(result.unwrap_err().to_string().contains("empty"));
613 }
614
615 #[test]
616 fn test_parse_unknown_device_type() {
617 let data: [u8; 16] = [0xFF; 16];
620 let result = parse_advertisement(&data);
621 assert!(result.is_err());
622 let err_msg = result.unwrap_err().to_string();
623 assert!(
624 err_msg.contains("Unknown device type byte"),
625 "Expected unknown device type error, got: {}",
626 err_msg
627 );
628 }
629
630 #[test]
631 fn test_parse_aranet4_insufficient_bytes() {
632 let data: [u8; 10] = [0x22; 10];
636 let result = parse_advertisement(&data);
637 assert!(result.is_err());
638 let err_msg = result.unwrap_err().to_string();
639 assert!(
640 err_msg.contains("Unknown device type byte"),
641 "Expected unknown device type error, got: {}",
642 err_msg
643 );
644 }
645
646 #[test]
647 fn test_parse_aranet_radiation_advertisement() {
648 let data: [u8; 23] = [
652 0x02, 0x20, 0x13, 0x04, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x64, 0x00, 85, 1, 0x2C, 0x01, 0x3C, 0x00, 5, ];
664
665 let result = parse_advertisement(&data).unwrap();
666 assert_eq!(result.device_type, DeviceType::AranetRadiation);
667 assert!(result.co2.is_none());
668 assert!(result.temperature.is_none());
669 assert!(result.radon.is_none());
670 assert!((result.radiation_dose_rate.unwrap() - 1.0).abs() < 0.001);
671 assert_eq!(result.battery, 85);
672 assert_eq!(result.status, Status::Green);
673 assert_eq!(result.interval, 300);
674 assert_eq!(result.age, 60);
675 }
676
677 #[test]
678 fn test_parse_aranet_radiation_insufficient_bytes() {
679 let data: [u8; 10] = [0x02, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
681 let result = parse_advertisement(&data);
682 assert!(result.is_err());
683 let err_msg = result.unwrap_err().to_string();
684 assert!(
685 err_msg.contains("requires at least 21 bytes"),
686 "Expected insufficient bytes error, got: {}",
687 err_msg
688 );
689 }
690
691 #[test]
692 fn test_parse_smart_home_not_enabled() {
693 let data: [u8; 22] = [
695 0x00, 0x13, 0x04, 0x01, 0x00, 0x0E, 0x0F, 0x01, 0x20, 0x03, 0xC2, 0x01, 0x94, 0x27, 45, 85, 1, 0x2C, 0x01, 0x78, 0x00, 5, ];
705
706 let result = parse_advertisement(&data);
707 assert!(result.is_err());
708 let err_msg = result.unwrap_err().to_string();
709 assert!(
710 err_msg.contains("Smart Home integration is not enabled"),
711 "Expected Smart Home error, got: {}",
712 err_msg
713 );
714 }
715}
716
717#[cfg(test)]
737mod proptests {
738 use super::*;
739 use proptest::prelude::*;
740
741 proptest! {
742 #[test]
745 fn parse_advertisement_never_panics(data: Vec<u8>) {
746 let _ = parse_advertisement(&data);
747 }
748
749 #[test]
751 fn parse_aranet4_advertisement_never_panics(data in proptest::collection::vec(any::<u8>(), 22)) {
752 let _ = parse_advertisement(&data);
753 }
754
755 #[test]
757 fn parse_aranet2_advertisement_never_panics(data in proptest::collection::vec(any::<u8>(), 19..=30)) {
758 let mut modified = data.clone();
759 if !modified.is_empty() {
760 modified[0] = 0x01; }
762 let _ = parse_advertisement(&modified);
763 }
764
765 #[test]
767 fn parse_aranet_radon_advertisement_never_panics(data in proptest::collection::vec(any::<u8>(), 23..=30)) {
768 let mut modified = data.clone();
769 if !modified.is_empty() {
770 modified[0] = 0x03; }
772 let _ = parse_advertisement(&modified);
773 }
774
775 #[test]
777 fn parse_aranet_radiation_advertisement_never_panics(data in proptest::collection::vec(any::<u8>(), 19..=30)) {
778 let mut modified = data.clone();
779 if !modified.is_empty() {
780 modified[0] = 0x02; }
782 let _ = parse_advertisement(&modified);
783 }
784 }
785}