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() < 19 {
363 return Err(Error::InvalidData(format!(
364 "Aranet Radiation advertisement requires at least 19 bytes, got {}",
365 data.len()
366 )));
367 }
368
369 let flags = data[0];
370 let mut buf = &data[5..];
372 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();
376 let status = Status::from(buf.get_u8());
377 let interval = buf.get_u16_le();
378 let age = buf.get_u16_le();
379 let counter = if !buf.is_empty() {
380 Some(buf.get_u8())
381 } else {
382 None
383 };
384
385 let dose_rate_usv = (radiation_rate_raw as f32 * 10.0) / 1000.0;
387
388 Ok(AdvertisementData {
389 device_type: DeviceType::AranetRadiation,
390 co2: None,
391 temperature: None,
392 pressure: None,
393 humidity: None,
394 battery,
395 status,
396 interval,
397 age,
398 radon: None,
399 radiation_dose_rate: Some(dose_rate_usv),
400 counter,
401 flags,
402 })
403}
404
405#[allow(dead_code)]
407fn parse_aranet2_advertisement(data: &[u8]) -> Result<AdvertisementData> {
408 if data.len() < 12 {
409 return Err(Error::InvalidData(format!(
410 "Aranet2 advertisement requires at least 12 bytes, got {}",
411 data.len()
412 )));
413 }
414
415 let mut buf = &data[1..];
416 let flags = buf.get_u8();
417 let temp_raw = buf.get_u16_le();
418 let humidity_raw = buf.get_u16_le();
419 let battery = buf.get_u8();
420 let status = Status::from(buf.get_u8());
421 let interval = buf.get_u16_le();
422 let age = buf.get_u16_le();
423
424 Ok(AdvertisementData {
425 device_type: DeviceType::Aranet2,
426 co2: None,
427 temperature: Some(temp_raw as f32 / 20.0),
428 pressure: None,
429 humidity: Some((humidity_raw / 10).min(255) as u8),
430 battery,
431 status,
432 interval,
433 age,
434 radon: None,
435 radiation_dose_rate: None,
436 counter: None,
437 flags,
438 })
439}
440
441#[allow(dead_code)]
443fn parse_aranet_radon_advertisement(data: &[u8]) -> Result<AdvertisementData> {
444 if data.len() < 18 {
445 return Err(Error::InvalidData(format!(
446 "Aranet Radon advertisement requires at least 18 bytes, got {}",
447 data.len()
448 )));
449 }
450
451 let mut buf = &data[1..];
452 let flags = buf.get_u8();
453 let temp_raw = buf.get_u16_le();
454 let pressure_raw = buf.get_u16_le();
455 let humidity_raw = buf.get_u16_le();
456 let battery = buf.get_u8();
457 let status = Status::from(buf.get_u8());
458 let interval = buf.get_u16_le();
459 let age = buf.get_u16_le();
460 let radon = buf.get_u32_le();
461
462 Ok(AdvertisementData {
463 device_type: DeviceType::AranetRadon,
464 co2: None,
465 temperature: Some(temp_raw as f32 / 20.0),
466 pressure: Some(pressure_raw as f32 / 10.0),
467 humidity: Some((humidity_raw / 10).min(255) as u8),
468 battery,
469 status,
470 interval,
471 age,
472 radon: Some(radon),
473 radiation_dose_rate: None,
474 counter: None,
475 flags,
476 })
477}
478
479#[allow(dead_code)]
481fn parse_aranet_radiation_advertisement(data: &[u8]) -> Result<AdvertisementData> {
482 if data.len() < 16 {
483 return Err(Error::InvalidData(format!(
484 "Aranet Radiation advertisement requires at least 16 bytes, got {}",
485 data.len()
486 )));
487 }
488
489 let mut buf = &data[1..];
490 let flags = buf.get_u8();
491 let battery = buf.get_u8();
492 let status = Status::from(buf.get_u8());
493 let interval = buf.get_u16_le();
494 let age = buf.get_u16_le();
495 let dose_rate_nsv = buf.get_u32_le();
497 let dose_rate_usv = dose_rate_nsv as f32 / 1000.0;
498
499 Ok(AdvertisementData {
500 device_type: DeviceType::AranetRadiation,
501 co2: None,
502 temperature: None,
503 pressure: None,
504 humidity: None,
505 battery,
506 status,
507 interval,
508 age,
509 radon: None,
510 radiation_dose_rate: Some(dose_rate_usv),
511 counter: None,
512 flags,
513 })
514}
515
516#[cfg(test)]
517mod tests {
518 use super::*;
519
520 #[test]
521 fn test_parse_aranet4_advertisement() {
522 let data: [u8; 22] = [
525 0x22, 0x13, 0x04, 0x01, 0x00, 0x0E, 0x0F, 0x01, 0x20, 0x03, 0xC2, 0x01, 0x94, 0x27, 45, 85, 1, 0x2C, 0x01, 0x78, 0x00, 5, ];
537
538 let result = parse_advertisement(&data).unwrap();
539 assert_eq!(result.device_type, DeviceType::Aranet4);
540 assert_eq!(result.co2, Some(800));
541 assert!((result.temperature.unwrap() - 22.5).abs() < 0.01);
542 assert!((result.pressure.unwrap() - 1013.2).abs() < 0.1);
543 assert_eq!(result.humidity, Some(45));
544 assert_eq!(result.battery, 85);
545 assert_eq!(result.status, Status::Green);
546 assert_eq!(result.interval, 300);
547 assert_eq!(result.age, 120);
548 }
549
550 #[test]
551 fn test_parse_aranet2_advertisement() {
552 let data: [u8; 20] = [
555 0x01, 0x20, 0x13, 0x04, 0x01, 0x00, 0x0E, 0x0F, 0xC2, 0x01, 0x00, 0x00, 0xC2, 0x01, 85, 1, 0x2C, 0x01, 0x3C, 0x00, ];
566
567 let result = parse_advertisement(&data).unwrap();
568 assert_eq!(result.device_type, DeviceType::Aranet2);
569 assert!(result.co2.is_none());
570 assert!((result.temperature.unwrap() - 22.5).abs() < 0.01);
571 assert_eq!(result.humidity, Some(45));
572 assert_eq!(result.battery, 85);
573 }
574
575 #[test]
576 fn test_parse_aranet_radon_advertisement() {
577 let data: [u8; 24] = [
581 0x03, 0x21, 0x00, 0x0C, 0x01, 0x00, 0x00, 0x00, 0x51, 0x00, 0xC2, 0x01, 0x94, 0x27, 0xC2, 0x01, 0x00, 85, 1, 0x2C, 0x01, 0x3C, 0x00, 5, ];
595
596 let result = parse_advertisement(&data).unwrap();
597 assert_eq!(result.device_type, DeviceType::AranetRadon);
598 assert!(result.co2.is_none());
599 assert!((result.temperature.unwrap() - 22.5).abs() < 0.01);
600 assert!((result.pressure.unwrap() - 1013.2).abs() < 0.1);
601 assert_eq!(result.humidity, Some(45));
602 assert_eq!(result.radon, Some(81));
603 assert_eq!(result.battery, 85);
604 assert_eq!(result.status, Status::Green);
605 }
606
607 #[test]
608 fn test_parse_empty_data() {
609 let result = parse_advertisement(&[]);
610 assert!(result.is_err());
611 assert!(result.unwrap_err().to_string().contains("empty"));
612 }
613
614 #[test]
615 fn test_parse_unknown_device_type() {
616 let data: [u8; 16] = [0xFF; 16];
619 let result = parse_advertisement(&data);
620 assert!(result.is_err());
621 let err_msg = result.unwrap_err().to_string();
622 assert!(
623 err_msg.contains("Unknown device type byte"),
624 "Expected unknown device type error, got: {}",
625 err_msg
626 );
627 }
628
629 #[test]
630 fn test_parse_aranet4_insufficient_bytes() {
631 let data: [u8; 10] = [0x22; 10];
635 let result = parse_advertisement(&data);
636 assert!(result.is_err());
637 let err_msg = result.unwrap_err().to_string();
638 assert!(
639 err_msg.contains("Unknown device type byte"),
640 "Expected unknown device type error, got: {}",
641 err_msg
642 );
643 }
644
645 #[test]
646 fn test_parse_aranet_radiation_advertisement() {
647 let data: [u8; 23] = [
651 0x02, 0x20, 0x13, 0x04, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x64, 0x00, 85, 1, 0x2C, 0x01, 0x3C, 0x00, 5, ];
663
664 let result = parse_advertisement(&data).unwrap();
665 assert_eq!(result.device_type, DeviceType::AranetRadiation);
666 assert!(result.co2.is_none());
667 assert!(result.temperature.is_none());
668 assert!(result.radon.is_none());
669 assert!((result.radiation_dose_rate.unwrap() - 1.0).abs() < 0.001);
670 assert_eq!(result.battery, 85);
671 assert_eq!(result.status, Status::Green);
672 assert_eq!(result.interval, 300);
673 assert_eq!(result.age, 60);
674 }
675
676 #[test]
677 fn test_parse_aranet_radiation_insufficient_bytes() {
678 let data: [u8; 10] = [0x02, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
680 let result = parse_advertisement(&data);
681 assert!(result.is_err());
682 let err_msg = result.unwrap_err().to_string();
683 assert!(
684 err_msg.contains("requires at least 19 bytes"),
685 "Expected insufficient bytes error, got: {}",
686 err_msg
687 );
688 }
689
690 #[test]
691 fn test_parse_smart_home_not_enabled() {
692 let data: [u8; 22] = [
694 0x00, 0x13, 0x04, 0x01, 0x00, 0x0E, 0x0F, 0x01, 0x20, 0x03, 0xC2, 0x01, 0x94, 0x27, 45, 85, 1, 0x2C, 0x01, 0x78, 0x00, 5, ];
704
705 let result = parse_advertisement(&data);
706 assert!(result.is_err());
707 let err_msg = result.unwrap_err().to_string();
708 assert!(
709 err_msg.contains("Smart Home integration is not enabled"),
710 "Expected Smart Home error, got: {}",
711 err_msg
712 );
713 }
714}