use crate::types::TasmotaDateTime;
use serde::Deserialize;
#[derive(Debug, Clone, Deserialize)]
pub struct EnergyResponse {
#[serde(rename = "StatusSNS")]
pub status_sns: Option<SensorStatus>,
#[serde(rename = "ENERGY")]
pub direct_energy: Option<EnergyData>,
}
impl EnergyResponse {
#[must_use]
pub fn energy(&self) -> Option<&EnergyData> {
self.status_sns
.as_ref()
.and_then(|s| s.energy.as_ref())
.or(self.direct_energy.as_ref())
}
#[must_use]
pub fn power(&self) -> Option<f32> {
self.energy().map(|e| e.power)
}
#[must_use]
pub fn voltage(&self) -> Option<f32> {
self.energy().map(|e| e.voltage)
}
#[must_use]
pub fn current(&self) -> Option<f32> {
self.energy().map(|e| e.current)
}
#[must_use]
pub fn total_energy(&self) -> Option<f32> {
self.energy().map(|e| e.total)
}
#[must_use]
pub fn today_energy(&self) -> Option<f32> {
self.energy().map(|e| e.today)
}
#[must_use]
pub fn yesterday_energy(&self) -> Option<f32> {
self.energy().map(|e| e.yesterday)
}
#[must_use]
pub fn frequency(&self) -> Option<f32> {
self.energy().and_then(|e| e.frequency)
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct SensorStatus {
#[serde(default)]
pub time: String,
#[serde(rename = "ENERGY")]
pub energy: Option<EnergyData>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct EnergyData {
#[serde(default)]
pub total_start_time: Option<TasmotaDateTime>,
#[serde(default)]
pub total: f32,
#[serde(default)]
pub yesterday: f32,
#[serde(default)]
pub today: f32,
#[serde(default)]
pub power: f32,
#[serde(default)]
pub apparent_power: f32,
#[serde(default)]
pub reactive_power: f32,
#[serde(default)]
pub factor: f32,
#[serde(default)]
pub voltage: f32,
#[serde(default)]
pub current: f32,
#[serde(default)]
pub frequency: Option<f32>,
}
impl EnergyData {
#[must_use]
pub fn power_factor_percent(&self) -> f32 {
self.factor * 100.0
}
#[must_use]
pub fn is_consuming(&self) -> bool {
self.power > 0.0
}
#[must_use]
pub fn estimated_daily_cost(&self, price_per_kwh: f32) -> f32 {
let kwh_per_day = self.power * 24.0 / 1000.0;
kwh_per_day * price_per_kwh
}
}
#[cfg(test)]
mod tests {
use approx::assert_abs_diff_eq;
use super::*;
#[test]
fn parse_energy_response_0() {
let json = r#"{
"StatusSNS": {
"Time": "2024-01-01T12:00:00",
"ENERGY": {
"TotalStartTime": "2023-01-01T00:00:00",
"Total": 123.45678,
"Yesterday": 1.23456,
"Today": 0.56789,
"Power": 45.001,
"ApparentPower": 50.000,
"ReactivePower": 10.000,
"Factor": 0.9,
"Voltage": 229.987,
"Current": 0.196
}
}
}"#;
let response: EnergyResponse = serde_json::from_str(json).unwrap();
let energy = response.energy().unwrap();
assert_abs_diff_eq!(energy.power, 45.001, epsilon = 0.0001);
assert_abs_diff_eq!(energy.voltage, 229.987, epsilon = 0.001);
assert_abs_diff_eq!(energy.current, 0.196, epsilon = 0.001);
assert_abs_diff_eq!(energy.total, 123.456, epsilon = 0.001);
assert_abs_diff_eq!(energy.factor, 0.9, epsilon = 0.01);
}
#[test]
fn parse_energy_response_1() {
let json = r#"{
"StatusSNS": {
"Time": "2024-01-01T12:00:00",
"ENERGY": {
"TotalStartTime": "2023-01-01T00:00:00",
"Total": 123.456,
"Yesterday": 1.234,
"Today": 0.567,
"Power": 45,
"ApparentPower": 50,
"ReactivePower": 10,
"Factor": 0.9,
"Voltage": 230,
"Current": 0.196
}
}
}"#;
let response: EnergyResponse = serde_json::from_str(json).unwrap();
let energy = response.energy().unwrap();
assert_abs_diff_eq!(energy.power, 45.0, epsilon = f32::EPSILON);
assert_abs_diff_eq!(energy.voltage, 230.0, epsilon = f32::EPSILON);
assert_abs_diff_eq!(energy.current, 0.196, epsilon = 0.001);
assert_abs_diff_eq!(energy.total, 123.456, epsilon = 0.01);
assert_abs_diff_eq!(energy.factor, 0.9, epsilon = 0.01);
}
#[test]
fn energy_helper_methods_0() {
let energy = EnergyData {
total_start_time: None,
total: 100.0,
yesterday: 2.0,
today: 1.0,
power: 100.0,
apparent_power: 110.0,
reactive_power: 20.0,
factor: 0.9,
voltage: 230.0,
current: 0.435,
frequency: None,
};
assert!(energy.is_consuming());
assert_abs_diff_eq!(energy.power_factor_percent(), 90.0, epsilon = 0.01);
let cost = energy.estimated_daily_cost(0.15);
assert_abs_diff_eq!(cost, 0.36, epsilon = 0.01);
}
#[test]
fn energy_helper_methods_1() {
let energy = EnergyData {
total_start_time: None,
total: 100.0,
yesterday: 2.0,
today: 1.0,
power: 100.001,
apparent_power: 110.002,
reactive_power: 20.003,
factor: 0.9,
voltage: 230.004,
current: 0.435,
frequency: None,
};
assert!(energy.is_consuming());
assert!((energy.power_factor_percent() - 90.0).abs() < f32::EPSILON);
let cost = energy.estimated_daily_cost(0.15);
assert!((cost - 0.36).abs() < 0.01);
}
#[test]
fn parse_direct_energy_0() {
let json = r#"{
"ENERGY": {
"Total": 50.0,
"Yesterday": 1.0,
"Today": 0.5,
"Power": 25,
"Voltage": 120,
"Current": 0.208
}
}"#;
let response: EnergyResponse = serde_json::from_str(json).unwrap();
assert_eq!(response.power(), Some(25.0));
assert_eq!(response.voltage(), Some(120.0));
}
#[test]
fn parse_direct_energy_1() {
let json = r#"{
"ENERGY": {
"Total": 50.012,
"Yesterday": 1.123,
"Today": 0.567,
"Power": 25.987,
"Voltage": 120.555,
"Current": 0.20868
}
}"#;
let response: EnergyResponse = serde_json::from_str(json).unwrap();
assert_abs_diff_eq!(response.power().unwrap(), 25.987, epsilon = 0.001);
assert_abs_diff_eq!(response.voltage().unwrap(), 120.555, epsilon = 0.001);
}
#[test]
fn response_helper_methods_0() {
let json = r#"{
"StatusSNS": {
"ENERGY": {
"Total": 100.0,
"Yesterday": 2.0,
"Today": 1.5,
"Power": 50,
"Voltage": 230,
"Current": 0.217
}
}
}"#;
let response: EnergyResponse = serde_json::from_str(json).unwrap();
assert_eq!(response.power(), Some(50.0));
assert_eq!(response.voltage(), Some(230.0));
assert_abs_diff_eq!(response.current().unwrap(), 0.217, epsilon = 0.001);
assert_abs_diff_eq!(response.total_energy().unwrap(), 100.0, epsilon = 0.01);
assert_abs_diff_eq!(response.today_energy().unwrap(), 1.5, epsilon = 0.01);
assert_abs_diff_eq!(response.yesterday_energy().unwrap(), 2.0, epsilon = 0.01);
}
#[test]
fn response_helper_methods_1() {
let json = r#"{
"StatusSNS": {
"ENERGY": {
"Total": 100.012,
"Yesterday": 2.123,
"Today": 1.234,
"Power": 50.345,
"Voltage": 230.456,
"Current": 0.21789
}
}
}"#;
let response: EnergyResponse = serde_json::from_str(json).unwrap();
assert_abs_diff_eq!(response.power().unwrap(), 50.345, epsilon = 0.001);
assert_abs_diff_eq!(response.voltage().unwrap(), 230.456, epsilon = 0.001);
assert_abs_diff_eq!(response.current().unwrap(), 0.21789, epsilon = 0.001);
assert_abs_diff_eq!(response.total_energy().unwrap(), 100.012, epsilon = 0.001);
assert_abs_diff_eq!(response.today_energy().unwrap(), 1.234, epsilon = 0.001);
assert_abs_diff_eq!(response.yesterday_energy().unwrap(), 2.123, epsilon = 0.001);
}
#[test]
fn parse_energy_with_frequency() {
let json = r#"{
"StatusSNS": {
"Time": "2024-01-01T12:00:00",
"ENERGY": {
"Total": 10.0,
"Yesterday": 1.0,
"Today": 0.5,
"Power": 60,
"Voltage": 230,
"Current": 0.26,
"Frequency": 50.1
}
}
}"#;
let response: EnergyResponse = serde_json::from_str(json).unwrap();
let energy = response.energy().unwrap();
assert_abs_diff_eq!(energy.frequency.unwrap(), 50.1, epsilon = 0.01);
}
#[test]
fn parse_energy_without_frequency_yields_none() {
let json = r#"{
"StatusSNS": {
"Time": "2024-01-01T12:00:00",
"ENERGY": {
"Total": 10.0,
"Yesterday": 1.0,
"Today": 0.5,
"Power": 60,
"Voltage": 12,
"Current": 5.0
}
}
}"#;
let response: EnergyResponse = serde_json::from_str(json).unwrap();
let energy = response.energy().unwrap();
assert!(energy.frequency.is_none());
}
}