use bytes::Buf;
use serde::{Deserialize, Serialize};
use aranet_types::{DeviceType, Status};
use crate::error::{Error, Result};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AdvertisementData {
pub device_type: DeviceType,
pub co2: Option<u16>,
pub temperature: Option<f32>,
pub pressure: Option<f32>,
pub humidity: Option<u8>,
pub battery: u8,
pub status: Status,
pub interval: u16,
pub age: u16,
pub radon: Option<u32>,
pub radiation_dose_rate: Option<f32>,
pub counter: Option<u8>,
pub flags: u8,
}
pub fn parse_advertisement(data: &[u8]) -> Result<AdvertisementData> {
parse_advertisement_with_name(data, None)
}
pub fn parse_advertisement_with_name(data: &[u8], name: Option<&str>) -> Result<AdvertisementData> {
if data.is_empty() {
return Err(Error::InvalidData(
"Advertisement data is empty".to_string(),
));
}
let is_aranet4_by_name = name.map(|n| n.starts_with("Aranet4")).unwrap_or(false);
let is_aranet4_by_len = data.len() == 7 || data.len() == 22;
let (device_type, sensor_data) = if is_aranet4_by_name || is_aranet4_by_len {
(DeviceType::Aranet4, data)
} else {
let device_type = match data[0] {
0x01 => DeviceType::Aranet2,
0x02 => DeviceType::AranetRadiation,
0x03 => DeviceType::AranetRadon,
other => {
return Err(Error::InvalidData(format!(
"Unknown device type byte: 0x{:02X}. Expected 0x01 (Aranet2), \
0x02 (Radiation), or 0x03 (Radon). Data length: {} bytes.",
other,
data.len()
)));
}
};
(device_type, &data[1..])
};
if sensor_data.is_empty() {
return Err(Error::InvalidData(
"Advertisement data too short for basic info".to_string(),
));
}
let flags = sensor_data[0];
let integrations_enabled = (flags & (1 << 5)) != 0;
if !integrations_enabled {
return Err(Error::InvalidData(
"Smart Home integration is not enabled on this device. \
To enable: go to device Settings > Smart Home > Enable."
.to_string(),
));
}
match device_type {
DeviceType::Aranet4 => parse_aranet4_advertisement_v2(sensor_data),
DeviceType::Aranet2 => parse_aranet2_advertisement_v2(sensor_data),
DeviceType::AranetRadon => parse_aranet_radon_advertisement_v2(sensor_data),
DeviceType::AranetRadiation => parse_aranet_radiation_advertisement_v2(sensor_data),
_ => Err(Error::InvalidData(format!(
"Unsupported device type for advertisement parsing: {:?}",
device_type
))),
}
}
fn parse_aranet4_advertisement_v2(data: &[u8]) -> Result<AdvertisementData> {
if data.len() < 22 {
return Err(Error::InvalidData(format!(
"Aranet4 advertisement requires 22 bytes, got {}",
data.len()
)));
}
let flags = data[0];
let mut buf = &data[8..];
let co2 = buf.get_u16_le();
let temp_raw = buf.get_i16_le();
let pressure_raw = buf.get_u16_le();
let humidity = buf.get_u8();
let battery = buf.get_u8();
let status = Status::from(buf.get_u8());
let interval = buf.get_u16_le();
let age = buf.get_u16_le();
let counter = if !buf.is_empty() {
Some(buf.get_u8())
} else {
None
};
Ok(AdvertisementData {
device_type: DeviceType::Aranet4,
co2: Some(co2),
temperature: Some(temp_raw as f32 * 0.05),
pressure: Some(pressure_raw as f32 * 0.1),
humidity: Some(humidity),
battery,
status,
interval,
age,
radon: None,
radiation_dose_rate: None,
counter,
flags,
})
}
fn parse_aranet2_advertisement_v2(data: &[u8]) -> Result<AdvertisementData> {
if data.len() < 19 {
return Err(Error::InvalidData(format!(
"Aranet2 advertisement requires at least 19 bytes, got {}",
data.len()
)));
}
let flags = data[0];
let mut buf = &data[7..];
let temp_raw = buf.get_i16_le();
let _unused = buf.get_u16_le();
let humidity_raw = buf.get_u16_le();
let battery = buf.get_u8();
let status_raw = buf.get_u8();
let status = Status::from((status_raw >> 2) & 0x03);
let interval = buf.get_u16_le();
let age = buf.get_u16_le();
let counter = if !buf.is_empty() {
Some(buf.get_u8())
} else {
None
};
Ok(AdvertisementData {
device_type: DeviceType::Aranet2,
co2: None,
temperature: Some(temp_raw as f32 * 0.05),
pressure: None,
humidity: Some((humidity_raw as f32 * 0.1).clamp(0.0, 100.0) as u8),
battery,
status,
interval,
age,
radon: None,
radiation_dose_rate: None,
counter,
flags,
})
}
fn parse_aranet_radon_advertisement_v2(data: &[u8]) -> Result<AdvertisementData> {
if data.len() < 22 {
return Err(Error::InvalidData(format!(
"Aranet Radon advertisement requires at least 22 bytes, got {}",
data.len()
)));
}
let flags = data[0];
let mut buf = &data[7..];
let radon = buf.get_u16_le() as u32;
let temp_raw = buf.get_i16_le();
let pressure_raw = buf.get_u16_le();
let humidity_raw = buf.get_u16_le();
let _reserved = buf.get_u8(); let battery = buf.get_u8();
let status = Status::from(buf.get_u8());
let interval = buf.get_u16_le();
let age = buf.get_u16_le();
let counter = if !buf.is_empty() {
Some(buf.get_u8())
} else {
None
};
Ok(AdvertisementData {
device_type: DeviceType::AranetRadon,
co2: None,
temperature: Some(temp_raw as f32 * 0.05),
pressure: Some(pressure_raw as f32 * 0.1),
humidity: Some((humidity_raw as f32 * 0.1).clamp(0.0, 100.0) as u8),
battery,
status,
interval,
age,
radon: Some(radon),
radiation_dose_rate: None,
counter,
flags,
})
}
fn parse_aranet_radiation_advertisement_v2(data: &[u8]) -> Result<AdvertisementData> {
if data.len() < 21 {
return Err(Error::InvalidData(format!(
"Aranet Radiation advertisement requires at least 21 bytes, got {}",
data.len()
)));
}
let flags = data[0];
let mut buf = &data[5..];
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();
let status = Status::from(buf.get_u8());
let interval = buf.get_u16_le();
let age = buf.get_u16_le();
let counter = if !buf.is_empty() {
Some(buf.get_u8())
} else {
None
};
let dose_rate_usv = (radiation_rate_raw as f32 * 10.0) / 1000.0;
Ok(AdvertisementData {
device_type: DeviceType::AranetRadiation,
co2: None,
temperature: None,
pressure: None,
humidity: None,
battery,
status,
interval,
age,
radon: None,
radiation_dose_rate: Some(dose_rate_usv),
counter,
flags,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_aranet4_advertisement() {
let data: [u8; 22] = [
0x22, 0x13, 0x04, 0x01, 0x00, 0x0E, 0x0F, 0x01, 0x20, 0x03, 0xC2, 0x01, 0x94, 0x27, 45, 85, 1, 0x2C, 0x01, 0x78, 0x00, 5, ];
let result = parse_advertisement(&data).unwrap();
assert_eq!(result.device_type, DeviceType::Aranet4);
assert_eq!(result.co2, Some(800));
assert!((result.temperature.unwrap() - 22.5).abs() < 0.01);
assert!((result.pressure.unwrap() - 1013.2).abs() < 0.1);
assert_eq!(result.humidity, Some(45));
assert_eq!(result.battery, 85);
assert_eq!(result.status, Status::Green);
assert_eq!(result.interval, 300);
assert_eq!(result.age, 120);
}
#[test]
fn test_parse_aranet2_advertisement() {
let data: [u8; 20] = [
0x01, 0x20, 0x13, 0x04, 0x01, 0x00, 0x0E, 0x0F, 0xC2, 0x01, 0x00, 0x00, 0xC2, 0x01, 85, 0x04, 0x2C, 0x01, 0x3C, 0x00, ];
let result = parse_advertisement(&data).unwrap();
assert_eq!(result.device_type, DeviceType::Aranet2);
assert!(result.co2.is_none());
assert!((result.temperature.unwrap() - 22.5).abs() < 0.01);
assert_eq!(result.humidity, Some(45));
assert_eq!(result.battery, 85);
assert_eq!(result.status, Status::Green);
}
#[test]
fn test_parse_aranet_radon_advertisement() {
let data: [u8; 24] = [
0x03, 0x21, 0x00, 0x0C, 0x01, 0x00, 0x00, 0x00, 0x51, 0x00, 0xC2, 0x01, 0x94, 0x27, 0xC2, 0x01, 0x00, 85, 1, 0x2C, 0x01, 0x3C, 0x00, 5, ];
let result = parse_advertisement(&data).unwrap();
assert_eq!(result.device_type, DeviceType::AranetRadon);
assert!(result.co2.is_none());
assert!((result.temperature.unwrap() - 22.5).abs() < 0.01);
assert!((result.pressure.unwrap() - 1013.2).abs() < 0.1);
assert_eq!(result.humidity, Some(45));
assert_eq!(result.radon, Some(81));
assert_eq!(result.battery, 85);
assert_eq!(result.status, Status::Green);
}
#[test]
fn test_parse_empty_data() {
let result = parse_advertisement(&[]);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("empty"));
}
#[test]
fn test_parse_unknown_device_type() {
let data: [u8; 16] = [0xFF; 16];
let result = parse_advertisement(&data);
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("Unknown device type byte"),
"Expected unknown device type error, got: {}",
err_msg
);
}
#[test]
fn test_parse_aranet4_insufficient_bytes() {
let data: [u8; 10] = [0x22; 10];
let result = parse_advertisement(&data);
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("Unknown device type byte"),
"Expected unknown device type error, got: {}",
err_msg
);
}
#[test]
fn test_parse_aranet_radiation_advertisement() {
let data: [u8; 23] = [
0x02, 0x20, 0x13, 0x04, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x64, 0x00, 85, 1, 0x2C, 0x01, 0x3C, 0x00, 5, ];
let result = parse_advertisement(&data).unwrap();
assert_eq!(result.device_type, DeviceType::AranetRadiation);
assert!(result.co2.is_none());
assert!(result.temperature.is_none());
assert!(result.radon.is_none());
assert!((result.radiation_dose_rate.unwrap() - 1.0).abs() < 0.001);
assert_eq!(result.battery, 85);
assert_eq!(result.status, Status::Green);
assert_eq!(result.interval, 300);
assert_eq!(result.age, 60);
}
#[test]
fn test_parse_aranet_radiation_insufficient_bytes() {
let data: [u8; 10] = [0x02, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
let result = parse_advertisement(&data);
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("requires at least 21 bytes"),
"Expected insufficient bytes error, got: {}",
err_msg
);
}
#[test]
fn test_parse_smart_home_not_enabled() {
let data: [u8; 22] = [
0x00, 0x13, 0x04, 0x01, 0x00, 0x0E, 0x0F, 0x01, 0x20, 0x03, 0xC2, 0x01, 0x94, 0x27, 45, 85, 1, 0x2C, 0x01, 0x78, 0x00, 5, ];
let result = parse_advertisement(&data);
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("Smart Home integration is not enabled"),
"Expected Smart Home error, got: {}",
err_msg
);
}
}
#[cfg(test)]
mod proptests {
use super::*;
use proptest::prelude::*;
proptest! {
#[test]
fn parse_advertisement_never_panics(data: Vec<u8>) {
let _ = parse_advertisement(&data);
}
#[test]
fn parse_aranet4_advertisement_never_panics(data in proptest::collection::vec(any::<u8>(), 22)) {
let _ = parse_advertisement(&data);
}
#[test]
fn parse_aranet2_advertisement_never_panics(data in proptest::collection::vec(any::<u8>(), 19..=30)) {
let mut modified = data.clone();
if !modified.is_empty() {
modified[0] = 0x01; }
let _ = parse_advertisement(&modified);
}
#[test]
fn parse_aranet_radon_advertisement_never_panics(data in proptest::collection::vec(any::<u8>(), 23..=30)) {
let mut modified = data.clone();
if !modified.is_empty() {
modified[0] = 0x03; }
let _ = parse_advertisement(&modified);
}
#[test]
fn parse_aranet_radiation_advertisement_never_panics(data in proptest::collection::vec(any::<u8>(), 19..=30)) {
let mut modified = data.clone();
if !modified.is_empty() {
modified[0] = 0x02; }
let _ = parse_advertisement(&modified);
}
}
}