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> {
210 if data.len() < 19 {
211 return Err(Error::InvalidData(format!(
212 "Aranet2 advertisement requires at least 19 bytes, got {}",
213 data.len()
214 )));
215 }
216
217 let flags = data[0];
218 let mut buf = &data[7..];
220 let temp_raw = buf.get_i16_le();
221 let _unused = buf.get_u16_le();
222 let humidity_raw = buf.get_u16_le();
223 let battery = buf.get_u8();
224 let status_raw = buf.get_u8();
225 let status = Status::from((status_raw >> 2) & 0x03);
227 let interval = buf.get_u16_le();
228 let age = buf.get_u16_le();
229 let counter = if !buf.is_empty() {
230 Some(buf.get_u8())
231 } else {
232 None
233 };
234
235 Ok(AdvertisementData {
236 device_type: DeviceType::Aranet2,
237 co2: None,
238 temperature: Some(temp_raw as f32 * 0.05),
239 pressure: None,
240 humidity: Some((humidity_raw as f32 * 0.1).clamp(0.0, 100.0) as u8),
241 battery,
242 status,
243 interval,
244 age,
245 radon: None,
246 radiation_dose_rate: None,
247 counter,
248 flags,
249 })
250}
251
252fn parse_aranet_radon_advertisement_v2(data: &[u8]) -> Result<AdvertisementData> {
268 if data.len() < 22 {
269 return Err(Error::InvalidData(format!(
270 "Aranet Radon advertisement requires at least 22 bytes, got {}",
271 data.len()
272 )));
273 }
274
275 let flags = data[0];
276 let mut buf = &data[7..];
278 let radon = buf.get_u16_le() as u32;
279 let temp_raw = buf.get_i16_le();
280 let pressure_raw = buf.get_u16_le();
281 let humidity_raw = buf.get_u16_le();
282 let _reserved = buf.get_u8(); let battery = buf.get_u8();
284 let status = Status::from(buf.get_u8());
285 let interval = buf.get_u16_le();
286 let age = buf.get_u16_le();
287 let counter = if !buf.is_empty() {
288 Some(buf.get_u8())
289 } else {
290 None
291 };
292
293 Ok(AdvertisementData {
294 device_type: DeviceType::AranetRadon,
295 co2: None,
296 temperature: Some(temp_raw as f32 * 0.05),
297 pressure: Some(pressure_raw as f32 * 0.1),
298 humidity: Some((humidity_raw as f32 * 0.1).clamp(0.0, 100.0) as u8),
299 battery,
300 status,
301 interval,
302 age,
303 radon: Some(radon),
304 radiation_dose_rate: None,
305 counter,
306 flags,
307 })
308}
309
310fn parse_aranet_radiation_advertisement_v2(data: &[u8]) -> Result<AdvertisementData> {
323 if data.len() < 21 {
325 return Err(Error::InvalidData(format!(
326 "Aranet Radiation advertisement requires at least 21 bytes, got {}",
327 data.len()
328 )));
329 }
330
331 let flags = data[0];
332 let mut buf = &data[5..];
334 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();
338 let status = Status::from(buf.get_u8());
339 let interval = buf.get_u16_le();
340 let age = buf.get_u16_le();
341 let counter = if !buf.is_empty() {
342 Some(buf.get_u8())
343 } else {
344 None
345 };
346
347 let dose_rate_usv = (radiation_rate_raw as f32 * 10.0) / 1000.0;
349
350 Ok(AdvertisementData {
351 device_type: DeviceType::AranetRadiation,
352 co2: None,
353 temperature: None,
354 pressure: None,
355 humidity: None,
356 battery,
357 status,
358 interval,
359 age,
360 radon: None,
361 radiation_dose_rate: Some(dose_rate_usv),
362 counter,
363 flags,
364 })
365}
366
367#[cfg(test)]
368mod tests {
369 use super::*;
370
371 #[test]
372 fn test_parse_aranet4_advertisement() {
373 let data: [u8; 22] = [
376 0x22, 0x13, 0x04, 0x01, 0x00, 0x0E, 0x0F, 0x01, 0x20, 0x03, 0xC2, 0x01, 0x94, 0x27, 45, 85, 1, 0x2C, 0x01, 0x78, 0x00, 5, ];
388
389 let result = parse_advertisement(&data).unwrap();
390 assert_eq!(result.device_type, DeviceType::Aranet4);
391 assert_eq!(result.co2, Some(800));
392 assert!((result.temperature.unwrap() - 22.5).abs() < 0.01);
393 assert!((result.pressure.unwrap() - 1013.2).abs() < 0.1);
394 assert_eq!(result.humidity, Some(45));
395 assert_eq!(result.battery, 85);
396 assert_eq!(result.status, Status::Green);
397 assert_eq!(result.interval, 300);
398 assert_eq!(result.age, 120);
399 }
400
401 #[test]
402 fn test_parse_aranet2_advertisement() {
403 let data: [u8; 20] = [
406 0x01, 0x20, 0x13, 0x04, 0x01, 0x00, 0x0E, 0x0F, 0xC2, 0x01, 0x00, 0x00, 0xC2, 0x01, 85, 0x04, 0x2C, 0x01, 0x3C, 0x00, ];
417
418 let result = parse_advertisement(&data).unwrap();
419 assert_eq!(result.device_type, DeviceType::Aranet2);
420 assert!(result.co2.is_none());
421 assert!((result.temperature.unwrap() - 22.5).abs() < 0.01);
422 assert_eq!(result.humidity, Some(45));
423 assert_eq!(result.battery, 85);
424 assert_eq!(result.status, Status::Green);
425 }
426
427 #[test]
428 fn test_parse_aranet_radon_advertisement() {
429 let data: [u8; 24] = [
433 0x03, 0x21, 0x00, 0x0C, 0x01, 0x00, 0x00, 0x00, 0x51, 0x00, 0xC2, 0x01, 0x94, 0x27, 0xC2, 0x01, 0x00, 85, 1, 0x2C, 0x01, 0x3C, 0x00, 5, ];
447
448 let result = parse_advertisement(&data).unwrap();
449 assert_eq!(result.device_type, DeviceType::AranetRadon);
450 assert!(result.co2.is_none());
451 assert!((result.temperature.unwrap() - 22.5).abs() < 0.01);
452 assert!((result.pressure.unwrap() - 1013.2).abs() < 0.1);
453 assert_eq!(result.humidity, Some(45));
454 assert_eq!(result.radon, Some(81));
455 assert_eq!(result.battery, 85);
456 assert_eq!(result.status, Status::Green);
457 }
458
459 #[test]
460 fn test_parse_empty_data() {
461 let result = parse_advertisement(&[]);
462 assert!(result.is_err());
463 assert!(result.unwrap_err().to_string().contains("empty"));
464 }
465
466 #[test]
467 fn test_parse_unknown_device_type() {
468 let data: [u8; 16] = [0xFF; 16];
471 let result = parse_advertisement(&data);
472 assert!(result.is_err());
473 let err_msg = result.unwrap_err().to_string();
474 assert!(
475 err_msg.contains("Unknown device type byte"),
476 "Expected unknown device type error, got: {}",
477 err_msg
478 );
479 }
480
481 #[test]
482 fn test_parse_aranet4_insufficient_bytes() {
483 let data: [u8; 10] = [0x22; 10];
487 let result = parse_advertisement(&data);
488 assert!(result.is_err());
489 let err_msg = result.unwrap_err().to_string();
490 assert!(
491 err_msg.contains("Unknown device type byte"),
492 "Expected unknown device type error, got: {}",
493 err_msg
494 );
495 }
496
497 #[test]
498 fn test_parse_aranet_radiation_advertisement() {
499 let data: [u8; 23] = [
503 0x02, 0x20, 0x13, 0x04, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x64, 0x00, 85, 1, 0x2C, 0x01, 0x3C, 0x00, 5, ];
515
516 let result = parse_advertisement(&data).unwrap();
517 assert_eq!(result.device_type, DeviceType::AranetRadiation);
518 assert!(result.co2.is_none());
519 assert!(result.temperature.is_none());
520 assert!(result.radon.is_none());
521 assert!((result.radiation_dose_rate.unwrap() - 1.0).abs() < 0.001);
522 assert_eq!(result.battery, 85);
523 assert_eq!(result.status, Status::Green);
524 assert_eq!(result.interval, 300);
525 assert_eq!(result.age, 60);
526 }
527
528 #[test]
529 fn test_parse_aranet_radiation_insufficient_bytes() {
530 let data: [u8; 10] = [0x02, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
532 let result = parse_advertisement(&data);
533 assert!(result.is_err());
534 let err_msg = result.unwrap_err().to_string();
535 assert!(
536 err_msg.contains("requires at least 21 bytes"),
537 "Expected insufficient bytes error, got: {}",
538 err_msg
539 );
540 }
541
542 #[test]
543 fn test_parse_smart_home_not_enabled() {
544 let data: [u8; 22] = [
546 0x00, 0x13, 0x04, 0x01, 0x00, 0x0E, 0x0F, 0x01, 0x20, 0x03, 0xC2, 0x01, 0x94, 0x27, 45, 85, 1, 0x2C, 0x01, 0x78, 0x00, 5, ];
556
557 let result = parse_advertisement(&data);
558 assert!(result.is_err());
559 let err_msg = result.unwrap_err().to_string();
560 assert!(
561 err_msg.contains("Smart Home integration is not enabled"),
562 "Expected Smart Home error, got: {}",
563 err_msg
564 );
565 }
566}
567
568#[cfg(test)]
588mod proptests {
589 use super::*;
590 use proptest::prelude::*;
591
592 proptest! {
593 #[test]
596 fn parse_advertisement_never_panics(data: Vec<u8>) {
597 let _ = parse_advertisement(&data);
598 }
599
600 #[test]
602 fn parse_aranet4_advertisement_never_panics(data in proptest::collection::vec(any::<u8>(), 22)) {
603 let _ = parse_advertisement(&data);
604 }
605
606 #[test]
608 fn parse_aranet2_advertisement_never_panics(data in proptest::collection::vec(any::<u8>(), 19..=30)) {
609 let mut modified = data.clone();
610 if !modified.is_empty() {
611 modified[0] = 0x01; }
613 let _ = parse_advertisement(&modified);
614 }
615
616 #[test]
618 fn parse_aranet_radon_advertisement_never_panics(data in proptest::collection::vec(any::<u8>(), 23..=30)) {
619 let mut modified = data.clone();
620 if !modified.is_empty() {
621 modified[0] = 0x03; }
623 let _ = parse_advertisement(&modified);
624 }
625
626 #[test]
628 fn parse_aranet_radiation_advertisement_never_panics(data in proptest::collection::vec(any::<u8>(), 19..=30)) {
629 let mut modified = data.clone();
630 if !modified.is_empty() {
631 modified[0] = 0x02; }
633 let _ = parse_advertisement(&modified);
634 }
635 }
636}