use serde::Deserialize;
use crate::error::ParseError;
use crate::state::StateChange;
use crate::types::TasmotaDateTime;
#[derive(Debug, Clone, Default, Deserialize)]
pub struct SensorData {
#[serde(rename = "Time", default)]
time: Option<String>,
#[serde(rename = "ENERGY", default)]
energy: Option<EnergyReading>,
#[serde(rename = "Temperature", default)]
temperature: Option<f32>,
#[serde(rename = "Humidity", default)]
humidity: Option<f32>,
#[serde(rename = "Pressure", default)]
pressure: Option<f32>,
#[serde(rename = "DS18B20", default)]
ds18b20: Option<TemperatureSensor>,
#[serde(rename = "DHT11", default)]
dht11: Option<DhtSensor>,
#[serde(rename = "AM2301", default)]
am2301: Option<DhtSensor>,
#[serde(rename = "BME280", default)]
bme280: Option<Bme280Sensor>,
}
#[derive(Debug, Clone, Default, Deserialize)]
pub struct EnergyReading {
#[serde(rename = "TotalStartTime", default)]
pub total_start_time: Option<String>,
#[serde(rename = "Today", default)]
pub today: Option<f32>,
#[serde(rename = "Yesterday", default)]
pub yesterday: Option<f32>,
#[serde(rename = "Total", default)]
pub total: Option<f32>,
#[serde(rename = "Power", default)]
pub power: Option<f32>,
#[serde(rename = "ApparentPower", default)]
pub apparent_power: Option<f32>,
#[serde(rename = "ReactivePower", default)]
pub reactive_power: Option<f32>,
#[serde(rename = "Factor", default)]
pub factor: Option<f32>,
#[serde(rename = "Voltage", default)]
pub voltage: Option<f32>,
#[serde(rename = "Current", default)]
pub current: Option<f32>,
#[serde(rename = "Frequency", default)]
pub frequency: Option<f32>,
}
#[derive(Debug, Clone, Default, Deserialize)]
pub struct TemperatureSensor {
#[serde(rename = "Temperature", default)]
pub temperature: Option<f32>,
#[serde(rename = "Id", default)]
id: Option<String>,
}
impl TemperatureSensor {
#[must_use]
pub fn temperature(&self) -> Option<f32> {
self.temperature
}
#[must_use]
pub fn id(&self) -> Option<&str> {
self.id.as_deref()
}
}
#[derive(Debug, Clone, Default, Deserialize)]
pub struct DhtSensor {
#[serde(rename = "Temperature", default)]
temperature: Option<f32>,
#[serde(rename = "Humidity", default)]
humidity: Option<f32>,
#[serde(rename = "DewPoint", default)]
dew_point: Option<f32>,
}
impl DhtSensor {
#[must_use]
pub fn temperature(&self) -> Option<f32> {
self.temperature
}
#[must_use]
pub fn humidity(&self) -> Option<f32> {
self.humidity
}
#[must_use]
pub fn dew_point(&self) -> Option<f32> {
self.dew_point
}
}
#[derive(Debug, Clone, Default, Deserialize)]
pub struct Bme280Sensor {
#[serde(rename = "Temperature", default)]
temperature: Option<f32>,
#[serde(rename = "Humidity", default)]
humidity: Option<f32>,
#[serde(rename = "DewPoint", default)]
dew_point: Option<f32>,
#[serde(rename = "Pressure", default)]
pressure: Option<f32>,
}
impl Bme280Sensor {
#[must_use]
pub fn temperature(&self) -> Option<f32> {
self.temperature
}
#[must_use]
pub fn humidity(&self) -> Option<f32> {
self.humidity
}
#[must_use]
pub fn dew_point(&self) -> Option<f32> {
self.dew_point
}
#[must_use]
pub fn pressure(&self) -> Option<f32> {
self.pressure
}
}
impl SensorData {
#[must_use]
pub fn time(&self) -> Option<&str> {
self.time.as_deref()
}
#[must_use]
pub fn energy(&self) -> Option<&EnergyReading> {
self.energy.as_ref()
}
#[must_use]
pub fn temperature(&self) -> Option<f32> {
self.temperature
.or_else(|| {
self.ds18b20
.as_ref()
.and_then(TemperatureSensor::temperature)
})
.or_else(|| self.dht11.as_ref().and_then(DhtSensor::temperature))
.or_else(|| self.am2301.as_ref().and_then(DhtSensor::temperature))
.or_else(|| self.bme280.as_ref().and_then(Bme280Sensor::temperature))
}
#[must_use]
pub fn humidity(&self) -> Option<f32> {
self.humidity
.or_else(|| self.dht11.as_ref().and_then(DhtSensor::humidity))
.or_else(|| self.am2301.as_ref().and_then(DhtSensor::humidity))
.or_else(|| self.bme280.as_ref().and_then(Bme280Sensor::humidity))
}
#[must_use]
pub fn pressure(&self) -> Option<f32> {
self.pressure
.or_else(|| self.bme280.as_ref().and_then(Bme280Sensor::pressure))
}
#[must_use]
pub fn ds18b20(&self) -> Option<&TemperatureSensor> {
self.ds18b20.as_ref()
}
#[must_use]
pub fn dht11(&self) -> Option<&DhtSensor> {
self.dht11.as_ref()
}
#[must_use]
pub fn am2301(&self) -> Option<&DhtSensor> {
self.am2301.as_ref()
}
#[must_use]
pub fn bme280(&self) -> Option<&Bme280Sensor> {
self.bme280.as_ref()
}
#[must_use]
pub fn to_state_changes(&self) -> Vec<StateChange> {
let mut changes = Vec::new();
if let Some(energy) = &self.energy {
if energy.has_power_data() || energy.has_consumption_data() {
let total_start_time = energy
.total_start_time
.as_deref()
.and_then(TasmotaDateTime::parse);
changes.push(StateChange::Energy {
power: energy.power,
voltage: energy.voltage,
current: energy.current,
apparent_power: energy.apparent_power,
reactive_power: energy.reactive_power,
power_factor: energy.factor,
energy_today: energy.today,
energy_yesterday: energy.yesterday,
energy_total: energy.total,
total_start_time,
frequency: energy.frequency,
});
}
}
changes
}
}
impl EnergyReading {
#[must_use]
pub fn has_power_data(&self) -> bool {
self.power.is_some() || self.voltage.is_some() || self.current.is_some()
}
#[must_use]
pub fn has_consumption_data(&self) -> bool {
self.today.is_some() || self.yesterday.is_some() || self.total.is_some()
}
}
pub(crate) fn parse_sensor(payload: &str) -> Result<SensorData, ParseError> {
serde_json::from_str(payload).map_err(ParseError::Json)
}
#[derive(Debug, Clone, Default, Deserialize)]
pub struct StatusSnsResponse {
#[serde(rename = "StatusSNS")]
pub status_sns: Option<SensorData>,
}
impl StatusSnsResponse {
#[must_use]
pub fn sensor_data(&self) -> Option<&SensorData> {
self.status_sns.as_ref()
}
#[must_use]
pub fn to_state_changes(&self) -> Vec<StateChange> {
self.status_sns
.as_ref()
.map_or_else(Vec::new, SensorData::to_state_changes)
}
}
#[cfg(test)]
mod tests {
use approx::assert_abs_diff_eq;
use chrono::Datelike;
use chrono::Timelike;
use super::*;
#[test]
fn parse_energy_basic() {
let json = r#"{"Time":"2024-01-01T12:00:00","ENERGY":{"Power":150}}"#;
let data: SensorData = serde_json::from_str(json).unwrap();
let energy = data.energy().unwrap();
assert_eq!(energy.power, Some(150.0));
}
#[test]
fn parse_energy_full() {
let json = r#"{
"Time": "2024-01-01T12:00:00",
"ENERGY": {
"Today": 1.5,
"Yesterday": 2.3,
"Total": 1234.5,
"Power": 150,
"ApparentPower": 160,
"ReactivePower": 20,
"Factor": 0.95,
"Voltage": 230,
"Current": 0.65,
"Frequency": 50.0
}
}"#;
let data: SensorData = serde_json::from_str(json).unwrap();
let energy = data.energy().unwrap();
assert_eq!(energy.today, Some(1.5));
assert_eq!(energy.yesterday, Some(2.3));
assert_eq!(energy.total, Some(1234.5));
assert_eq!(energy.power, Some(150.0));
assert_eq!(energy.apparent_power, Some(160.0));
assert_eq!(energy.reactive_power, Some(20.0));
assert_eq!(energy.factor, Some(0.95));
assert_eq!(energy.voltage, Some(230.0));
assert_eq!(energy.current, Some(0.65));
assert_eq!(energy.frequency, Some(50.0));
}
#[test]
fn parse_temperature_direct() {
let json = r#"{"Time":"2024-01-01T12:00:00","Temperature":23.5}"#;
let data: SensorData = serde_json::from_str(json).unwrap();
assert_eq!(data.temperature(), Some(23.5));
}
#[test]
fn parse_ds18b20() {
let json = r#"{"Time":"2024-01-01T12:00:00","DS18B20":{"Temperature":22.5,"Id":"28-0123456789ab"}}"#;
let data: SensorData = serde_json::from_str(json).unwrap();
assert_eq!(data.temperature(), Some(22.5));
assert_eq!(
data.ds18b20().and_then(TemperatureSensor::id),
Some("28-0123456789ab")
);
}
#[test]
fn parse_dht11() {
let json = r#"{"Time":"2024-01-01T12:00:00","DHT11":{"Temperature":24.0,"Humidity":55.0}}"#;
let data: SensorData = serde_json::from_str(json).unwrap();
assert_eq!(data.temperature(), Some(24.0));
assert_eq!(data.humidity(), Some(55.0));
}
#[test]
fn parse_bme280() {
let json = r#"{
"Time": "2024-01-01T12:00:00",
"BME280": {
"Temperature": 21.5,
"Humidity": 60.0,
"DewPoint": 13.2,
"Pressure": 1013.25
}
}"#;
let data: SensorData = serde_json::from_str(json).unwrap();
assert_eq!(data.temperature(), Some(21.5));
assert_eq!(data.humidity(), Some(60.0));
assert_eq!(data.pressure(), Some(1013.25));
let bme = data.bme280().unwrap();
assert_eq!(bme.dew_point(), Some(13.2));
}
#[test]
fn energy_has_power_data() {
let energy = EnergyReading {
power: Some(100.0),
..Default::default()
};
assert!(energy.has_power_data());
let empty = EnergyReading::default();
assert!(!empty.has_power_data());
}
#[test]
fn energy_has_consumption_data() {
let energy = EnergyReading {
total: Some(1234.5),
..Default::default()
};
assert!(energy.has_consumption_data());
let empty = EnergyReading::default();
assert!(!empty.has_consumption_data());
}
#[test]
fn to_state_changes_with_energy() {
let json = r#"{"ENERGY":{"Power":150,"Voltage":230,"Current":0.65}}"#;
let data: SensorData = serde_json::from_str(json).unwrap();
let changes = data.to_state_changes();
assert_eq!(changes.len(), 1);
if let StateChange::Energy {
power,
voltage,
current,
..
} = &changes[0]
{
assert_abs_diff_eq!(power.unwrap(), 150.0, epsilon = 1e-6);
assert_abs_diff_eq!(voltage.unwrap(), 230.0, epsilon = 1e-6);
assert_abs_diff_eq!(current.unwrap(), 0.65, epsilon = 0.001);
} else {
panic!("Expected StateChange::Energy");
}
}
#[test]
fn to_state_changes_empty() {
let json = r#"{"Time":"2024-01-01T12:00:00"}"#;
let data: SensorData = serde_json::from_str(json).unwrap();
let changes = data.to_state_changes();
assert!(changes.is_empty());
}
#[test]
fn parse_sensor_function() {
let json = r#"{"Time":"2024-01-01T12:00:00","ENERGY":{"Power":100}}"#;
let result = parse_sensor(json);
assert!(result.is_ok());
let data = result.unwrap();
assert_eq!(data.energy().unwrap().power, Some(100.0));
}
#[test]
fn parse_sensor_invalid_json() {
let result = parse_sensor("not json");
assert!(result.is_err());
}
#[test]
fn parse_status_sns_response() {
let json = r#"{
"StatusSNS": {
"Time": "2024-01-01T12:00:00",
"ENERGY": {
"Power": 182,
"Voltage": 224,
"Current": 0.706,
"Total": 1104.315
}
}
}"#;
let response: StatusSnsResponse = serde_json::from_str(json).unwrap();
let sensor = response.sensor_data().unwrap();
let energy = sensor.energy().unwrap();
assert_eq!(energy.power, Some(182.0));
assert_eq!(energy.voltage, Some(224.0));
assert_abs_diff_eq!(energy.current.unwrap(), 0.706, epsilon = 0.001);
assert_abs_diff_eq!(energy.total.unwrap(), 1104.315, epsilon = 0.01);
}
#[test]
fn status_sns_to_state_changes() {
let json = r#"{"StatusSNS":{"ENERGY":{"Power":150,"Voltage":230,"Current":0.65}}}"#;
let response: StatusSnsResponse = serde_json::from_str(json).unwrap();
let changes = response.to_state_changes();
assert_eq!(changes.len(), 1);
if let StateChange::Energy {
power,
voltage,
current,
..
} = &changes[0]
{
assert_abs_diff_eq!(power.unwrap(), 150.0, epsilon = 1e-6);
assert_abs_diff_eq!(voltage.unwrap(), 230.0, epsilon = 1e-6);
assert_abs_diff_eq!(current.unwrap(), 0.65, epsilon = 0.001);
} else {
panic!("Expected StateChange::Energy");
}
}
#[test]
fn to_state_changes_with_full_energy() {
let json = r#"{
"ENERGY": {
"Power": 182,
"Voltage": 224,
"Current": 0.706,
"ApparentPower": 195,
"ReactivePower": 50,
"Factor": 0.93,
"Today": 1.5,
"Yesterday": 2.3,
"Total": 1104.315
}
}"#;
let data: SensorData = serde_json::from_str(json).unwrap();
let changes = data.to_state_changes();
assert_eq!(changes.len(), 1);
if let StateChange::Energy {
power,
voltage,
current,
apparent_power,
reactive_power,
power_factor,
energy_today,
energy_yesterday,
energy_total,
total_start_time,
frequency,
} = &changes[0]
{
assert_abs_diff_eq!(power.unwrap(), 182.0, epsilon = 1e-6);
assert_abs_diff_eq!(voltage.unwrap(), 224.0, epsilon = 1e-6);
assert_abs_diff_eq!(current.unwrap(), 0.706, epsilon = 0.001);
assert_abs_diff_eq!(apparent_power.unwrap(), 195.0, epsilon = 1e-6);
assert_abs_diff_eq!(reactive_power.unwrap(), 50.0, epsilon = 1e-6);
assert_abs_diff_eq!(power_factor.unwrap(), 0.93, epsilon = 0.01);
assert_abs_diff_eq!(energy_today.unwrap(), 1.5, epsilon = 0.01);
assert_abs_diff_eq!(energy_yesterday.unwrap(), 2.3, epsilon = 0.01);
assert_abs_diff_eq!(energy_total.unwrap(), 1104.315, epsilon = 0.01);
assert!(total_start_time.is_none()); assert!(frequency.is_none()); } else {
panic!("Expected StateChange::Energy");
}
}
#[test]
fn to_state_changes_with_total_start_time() {
let json = r#"{
"ENERGY": {
"TotalStartTime": "2024-01-15T10:30:00",
"Power": 100,
"Voltage": 230,
"Current": 0.5,
"Total": 500.0
}
}"#;
let data: SensorData = serde_json::from_str(json).unwrap();
let changes = data.to_state_changes();
assert_eq!(changes.len(), 1);
if let StateChange::Energy {
total_start_time, ..
} = &changes[0]
{
let dt = total_start_time.as_ref().expect("Should have datetime");
assert_eq!(dt.naive().year(), 2024);
assert_eq!(dt.naive().month(), 1);
assert_eq!(dt.naive().day(), 15);
assert_eq!(dt.naive().hour(), 10);
assert_eq!(dt.naive().minute(), 30);
assert!(!dt.has_timezone()); } else {
panic!("Expected StateChange::Energy");
}
}
#[test]
fn to_state_changes_with_total_start_time_with_tz() {
let json = r#"{
"ENERGY": {
"TotalStartTime": "2024-01-15T10:30:00+01:00",
"Power": 100,
"Voltage": 230,
"Current": 0.5,
"Total": 500.0
}
}"#;
let data: SensorData = serde_json::from_str(json).unwrap();
let changes = data.to_state_changes();
assert_eq!(changes.len(), 1);
if let StateChange::Energy {
total_start_time, ..
} = &changes[0]
{
let dt = total_start_time.as_ref().expect("Should have datetime");
assert!(dt.has_timezone());
let tz_dt = dt.to_datetime().expect("Should have timezone");
assert_eq!(tz_dt.offset().local_minus_utc(), 3600); } else {
panic!("Expected StateChange::Energy");
}
}
}