use std::time::Duration;
use crate::types::{
ColorTemperature, Dimmer, FadeDuration, HsbColor, PowerState, Scheme, TasmotaDateTime,
WakeupDuration,
};
use super::StateChange;
#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct SystemInfo {
#[serde(
serialize_with = "serialize_duration_as_secs",
deserialize_with = "deserialize_duration_from_secs"
)]
uptime: Option<Duration>,
wifi_rssi: Option<i8>,
heap: Option<u32>,
}
#[allow(clippy::ref_option)]
fn serialize_duration_as_secs<S>(
duration: &Option<Duration>,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match duration {
Some(d) => serializer.serialize_some(&d.as_secs()),
None => serializer.serialize_none(),
}
}
fn deserialize_duration_from_secs<'de, D>(deserializer: D) -> Result<Option<Duration>, D::Error>
where
D: serde::Deserializer<'de>,
{
let opt: Option<u64> = serde::Deserialize::deserialize(deserializer)?;
Ok(opt.map(Duration::from_secs))
}
impl SystemInfo {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_uptime(mut self, duration: Duration) -> Self {
self.uptime = Some(duration);
self
}
#[must_use]
pub fn with_wifi_rssi(mut self, rssi: i8) -> Self {
self.wifi_rssi = Some(rssi);
self
}
#[must_use]
pub fn with_heap(mut self, heap_kb: u32) -> Self {
self.heap = Some(heap_kb);
self
}
#[must_use]
pub fn uptime(&self) -> Option<Duration> {
self.uptime
}
#[must_use]
pub fn wifi_rssi(&self) -> Option<i8> {
self.wifi_rssi
}
#[must_use]
pub fn heap(&self) -> Option<u32> {
self.heap
}
pub fn merge(&mut self, other: &SystemInfo) {
if other.uptime.is_some() {
self.uptime = other.uptime;
}
if other.wifi_rssi.is_some() {
self.wifi_rssi = other.wifi_rssi;
}
if other.heap.is_some() {
self.heap = other.heap;
}
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.uptime.is_none() && self.wifi_rssi.is_none() && self.heap.is_none()
}
}
#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct DeviceState {
power: [Option<PowerState>; 8],
dimmer: Option<Dimmer>,
hsb_color: Option<HsbColor>,
color_temperature: Option<ColorTemperature>,
scheme: Option<Scheme>,
wakeup_duration: Option<WakeupDuration>,
fade_enabled: Option<bool>,
fade_duration: Option<FadeDuration>,
power_consumption: Option<f32>,
voltage: Option<f32>,
current: Option<f32>,
apparent_power: Option<f32>,
reactive_power: Option<f32>,
power_factor: Option<f32>,
energy_today: Option<f32>,
energy_yesterday: Option<f32>,
energy_total: Option<f32>,
total_start_time: Option<TasmotaDateTime>,
frequency: Option<f32>,
system_info: Option<SystemInfo>,
}
impl DeviceState {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn power(&self, index: u8) -> Option<PowerState> {
if index == 0 || index > 8 {
return None;
}
self.power[usize::from(index - 1)]
}
pub fn set_power(&mut self, index: u8, state: PowerState) {
if index > 0 && index <= 8 {
self.power[usize::from(index - 1)] = Some(state);
}
}
pub fn clear_power(&mut self, index: u8) {
if index > 0 && index <= 8 {
self.power[usize::from(index - 1)] = None;
}
}
#[must_use]
pub fn all_power_states(&self) -> Vec<(u8, PowerState)> {
self.power
.iter()
.enumerate()
.filter_map(|(i, state)| {
state.map(|s| {
#[allow(clippy::cast_possible_truncation)]
let index = (i + 1) as u8;
(index, s)
})
})
.collect()
}
#[must_use]
pub fn is_any_on(&self) -> bool {
self.power.iter().any(|s| matches!(s, Some(PowerState::On)))
}
#[must_use]
pub fn dimmer(&self) -> Option<Dimmer> {
self.dimmer
}
pub fn set_dimmer(&mut self, value: Dimmer) {
self.dimmer = Some(value);
}
pub fn clear_dimmer(&mut self) {
self.dimmer = None;
}
#[must_use]
pub fn hsb_color(&self) -> Option<HsbColor> {
self.hsb_color
}
pub fn set_hsb_color(&mut self, color: HsbColor) {
self.hsb_color = Some(color);
}
pub fn clear_hsb_color(&mut self) {
self.hsb_color = None;
}
#[must_use]
pub fn color_temperature(&self) -> Option<ColorTemperature> {
self.color_temperature
}
pub fn set_color_temperature(&mut self, ct: ColorTemperature) {
self.color_temperature = Some(ct);
}
pub fn clear_color_temperature(&mut self) {
self.color_temperature = None;
}
#[must_use]
pub fn scheme(&self) -> Option<Scheme> {
self.scheme
}
pub fn set_scheme(&mut self, scheme: Scheme) {
self.scheme = Some(scheme);
}
pub fn clear_scheme(&mut self) {
self.scheme = None;
}
#[must_use]
pub fn wakeup_duration(&self) -> Option<WakeupDuration> {
self.wakeup_duration
}
pub fn set_wakeup_duration(&mut self, duration: WakeupDuration) {
self.wakeup_duration = Some(duration);
}
pub fn clear_wakeup_duration(&mut self) {
self.wakeup_duration = None;
}
#[must_use]
pub fn fade_enabled(&self) -> Option<bool> {
self.fade_enabled
}
pub fn set_fade_enabled(&mut self, enabled: bool) {
self.fade_enabled = Some(enabled);
}
pub fn clear_fade_enabled(&mut self) {
self.fade_enabled = None;
}
#[must_use]
pub fn fade_duration(&self) -> Option<FadeDuration> {
self.fade_duration
}
pub fn set_fade_duration(&mut self, duration: FadeDuration) {
self.fade_duration = Some(duration);
}
pub fn clear_fade_duration(&mut self) {
self.fade_duration = None;
}
#[must_use]
pub fn power_consumption(&self) -> Option<f32> {
self.power_consumption
}
pub fn set_power_consumption(&mut self, watts: f32) {
self.power_consumption = Some(watts);
}
#[must_use]
pub fn voltage(&self) -> Option<f32> {
self.voltage
}
pub fn set_voltage(&mut self, volts: f32) {
self.voltage = Some(volts);
}
#[must_use]
pub fn current(&self) -> Option<f32> {
self.current
}
pub fn set_current(&mut self, amps: f32) {
self.current = Some(amps);
}
#[must_use]
pub fn energy_total(&self) -> Option<f32> {
self.energy_total
}
pub fn set_energy_total(&mut self, kwh: f32) {
self.energy_total = Some(kwh);
}
#[must_use]
pub fn apparent_power(&self) -> Option<f32> {
self.apparent_power
}
pub fn set_apparent_power(&mut self, va: f32) {
self.apparent_power = Some(va);
}
#[must_use]
pub fn reactive_power(&self) -> Option<f32> {
self.reactive_power
}
pub fn set_reactive_power(&mut self, var: f32) {
self.reactive_power = Some(var);
}
#[must_use]
pub fn power_factor(&self) -> Option<f32> {
self.power_factor
}
pub fn set_power_factor(&mut self, factor: f32) {
self.power_factor = Some(factor);
}
#[must_use]
pub fn energy_today(&self) -> Option<f32> {
self.energy_today
}
pub fn set_energy_today(&mut self, kwh: f32) {
self.energy_today = Some(kwh);
}
#[must_use]
pub fn energy_yesterday(&self) -> Option<f32> {
self.energy_yesterday
}
pub fn set_energy_yesterday(&mut self, kwh: f32) {
self.energy_yesterday = Some(kwh);
}
#[must_use]
pub fn total_start_time(&self) -> Option<&TasmotaDateTime> {
self.total_start_time.as_ref()
}
pub fn set_total_start_time(&mut self, time: TasmotaDateTime) {
self.total_start_time = Some(time);
}
#[must_use]
pub fn frequency(&self) -> Option<f32> {
self.frequency
}
pub fn set_frequency(&mut self, hz: f32) {
self.frequency = Some(hz);
}
#[must_use]
pub fn system_info(&self) -> Option<&SystemInfo> {
self.system_info.as_ref()
}
pub fn set_system_info(&mut self, info: SystemInfo) {
self.system_info = Some(info);
}
pub fn update_system_info(&mut self, info: &SystemInfo) {
if let Some(existing) = &mut self.system_info {
existing.merge(info);
} else {
self.system_info = Some(info.clone());
}
}
#[must_use]
pub fn uptime(&self) -> Option<Duration> {
self.system_info.as_ref().and_then(SystemInfo::uptime)
}
#[allow(clippy::too_many_lines)]
pub fn apply(&mut self, change: &StateChange) -> bool {
match change {
StateChange::Power { index, state } => {
let current = self.power(*index);
if current == Some(*state) {
false
} else {
self.set_power(*index, *state);
true
}
}
StateChange::Dimmer(value) => {
if self.dimmer == Some(*value) {
false
} else {
self.dimmer = Some(*value);
true
}
}
StateChange::HsbColor(color) => {
if self.hsb_color == Some(*color) {
false
} else {
self.hsb_color = Some(*color);
true
}
}
StateChange::ColorTemperature(ct) => {
if self.color_temperature == Some(*ct) {
false
} else {
self.color_temperature = Some(*ct);
true
}
}
StateChange::Scheme(scheme) => {
if self.scheme == Some(*scheme) {
false
} else {
self.scheme = Some(*scheme);
true
}
}
StateChange::WakeupDuration(duration) => {
if self.wakeup_duration == Some(*duration) {
false
} else {
self.wakeup_duration = Some(*duration);
true
}
}
StateChange::FadeEnabled(enabled) => {
if self.fade_enabled == Some(*enabled) {
false
} else {
self.fade_enabled = Some(*enabled);
true
}
}
StateChange::FadeDuration(duration) => {
if self.fade_duration == Some(*duration) {
false
} else {
self.fade_duration = Some(*duration);
true
}
}
StateChange::Energy {
power,
voltage,
current,
apparent_power,
reactive_power,
power_factor,
energy_today,
energy_yesterday,
energy_total,
total_start_time,
frequency,
} => {
let mut changed = false;
macro_rules! update_if_some {
($field:ident, $value:expr) => {
if let Some(v) = $value {
if self.$field != Some(*v) {
self.$field = Some(*v);
changed = true;
}
}
};
}
update_if_some!(power_consumption, power);
update_if_some!(voltage, voltage);
update_if_some!(current, current);
update_if_some!(apparent_power, apparent_power);
update_if_some!(reactive_power, reactive_power);
update_if_some!(power_factor, power_factor);
update_if_some!(energy_today, energy_today);
update_if_some!(energy_yesterday, energy_yesterday);
update_if_some!(energy_total, energy_total);
update_if_some!(frequency, frequency);
if let Some(time) = total_start_time
&& self.total_start_time.as_ref() != Some(time)
{
self.total_start_time = Some(time.clone());
changed = true;
}
changed
}
StateChange::Batch(changes) => {
let mut any_changed = false;
for c in changes {
if self.apply(c) {
any_changed = true;
}
}
any_changed
}
}
}
pub fn clear(&mut self) {
*self = Self::new();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_state_is_empty() {
let state = DeviceState::new();
assert!(state.power(1).is_none());
assert!(state.dimmer().is_none());
assert!(state.hsb_color().is_none());
assert!(state.color_temperature().is_none());
assert!(state.power_consumption().is_none());
}
#[test]
fn power_state_management() {
let mut state = DeviceState::new();
state.set_power(1, PowerState::On);
assert_eq!(state.power(1), Some(PowerState::On));
assert!(state.power(2).is_none());
state.set_power(2, PowerState::Off);
assert_eq!(state.power(2), Some(PowerState::Off));
state.clear_power(1);
assert!(state.power(1).is_none());
}
#[test]
fn power_index_bounds() {
let mut state = DeviceState::new();
state.set_power(0, PowerState::On);
assert!(state.power(0).is_none());
state.set_power(9, PowerState::On);
assert!(state.power(9).is_none());
state.set_power(8, PowerState::On);
assert_eq!(state.power(8), Some(PowerState::On));
}
#[test]
fn all_power_states() {
let mut state = DeviceState::new();
state.set_power(1, PowerState::On);
state.set_power(3, PowerState::Off);
state.set_power(5, PowerState::On);
let states = state.all_power_states();
assert_eq!(states.len(), 3);
assert!(states.contains(&(1, PowerState::On)));
assert!(states.contains(&(3, PowerState::Off)));
assert!(states.contains(&(5, PowerState::On)));
}
#[test]
fn is_any_on() {
let mut state = DeviceState::new();
assert!(!state.is_any_on());
state.set_power(1, PowerState::Off);
assert!(!state.is_any_on());
state.set_power(2, PowerState::On);
assert!(state.is_any_on());
}
#[test]
fn apply_power_change() {
let mut state = DeviceState::new();
let change = StateChange::Power {
index: 1,
state: PowerState::On,
};
assert!(state.apply(&change));
assert_eq!(state.power(1), Some(PowerState::On));
assert!(!state.apply(&change));
}
#[test]
fn apply_dimmer_change() {
let mut state = DeviceState::new();
let dimmer = Dimmer::new(75).unwrap();
let change = StateChange::Dimmer(dimmer);
assert!(state.apply(&change));
assert_eq!(state.dimmer(), Some(dimmer));
}
#[test]
fn apply_batch_changes() {
let mut state = DeviceState::new();
let changes = StateChange::Batch(vec![
StateChange::Power {
index: 1,
state: PowerState::On,
},
StateChange::Dimmer(Dimmer::new(50).unwrap()),
]);
assert!(state.apply(&changes));
assert_eq!(state.power(1), Some(PowerState::On));
assert_eq!(state.dimmer(), Some(Dimmer::new(50).unwrap()));
}
#[test]
fn clear_resets_state() {
let mut state = DeviceState::new();
state.set_power(1, PowerState::On);
state.set_dimmer(Dimmer::new(75).unwrap());
state.clear();
assert!(state.power(1).is_none());
assert!(state.dimmer().is_none());
}
#[test]
fn apply_batch_with_hsb_color() {
use crate::types::HsbColor;
let mut state = DeviceState::new();
let hsb = HsbColor::new(360, 100, 100).unwrap();
let changes = StateChange::Batch(vec![
StateChange::Power {
index: 1,
state: PowerState::Off,
},
StateChange::Dimmer(Dimmer::new(100).unwrap()),
StateChange::HsbColor(hsb),
]);
assert!(state.apply(&changes));
assert_eq!(state.power(1), Some(PowerState::Off));
assert_eq!(state.dimmer(), Some(Dimmer::new(100).unwrap()));
let applied_hsb = state.hsb_color().expect("HsbColor should be set");
assert_eq!(applied_hsb.hue(), 360);
assert_eq!(applied_hsb.saturation(), 100);
assert_eq!(applied_hsb.brightness(), 100);
}
#[test]
fn apply_state_from_tasmota_telemetry() {
use crate::telemetry::TelemetryState;
let json = r#"{
"Time":"2025-12-24T14:24:03",
"Uptime":"1T23:46:58",
"UptimeSec":172018,
"Heap":25,
"SleepMode":"Dynamic",
"Sleep":50,
"LoadAvg":19,
"MqttCount":1,
"POWER":"OFF",
"Dimmer":100,
"Color":"FF00000000",
"HSBColor":"360,100,100",
"White":0,
"CT":153,
"Channel":[100,0,0,0,0],
"Scheme":0,
"Fade":"ON",
"Speed":2,
"LedTable":"ON",
"Wifi":{"AP":1}
}"#;
let telemetry: TelemetryState = serde_json::from_str(json).unwrap();
let changes = telemetry.to_state_changes();
let mut state = DeviceState::new();
for change in changes {
state.apply(&change);
}
assert_eq!(state.power(1), Some(PowerState::Off));
assert_eq!(state.dimmer(), Some(Dimmer::new(100).unwrap()));
let hsb = state
.hsb_color()
.expect("HSBColor should be set from telemetry");
assert_eq!(hsb.hue(), 360);
assert_eq!(hsb.saturation(), 100);
assert_eq!(hsb.brightness(), 100);
assert!(state.color_temperature().is_some());
assert_eq!(state.color_temperature().unwrap().value(), 153);
assert_eq!(state.fade_enabled(), Some(true));
assert_eq!(state.fade_duration().map(|s| s.value()), Some(2));
}
#[test]
fn fade_getters_setters() {
let mut state = DeviceState::new();
assert!(state.fade_enabled().is_none());
assert!(state.fade_duration().is_none());
state.set_fade_enabled(true);
assert_eq!(state.fade_enabled(), Some(true));
state.set_fade_enabled(false);
assert_eq!(state.fade_enabled(), Some(false));
let duration = FadeDuration::from_raw(15).unwrap();
state.set_fade_duration(duration);
assert_eq!(state.fade_duration(), Some(duration));
state.clear_fade_enabled();
state.clear_fade_duration();
assert!(state.fade_enabled().is_none());
assert!(state.fade_duration().is_none());
}
#[test]
fn apply_fade_changes() {
let mut state = DeviceState::new();
let change = StateChange::FadeEnabled(true);
assert!(state.apply(&change));
assert_eq!(state.fade_enabled(), Some(true));
assert!(!state.apply(&change));
let duration = FadeDuration::from_raw(20).unwrap();
let change = StateChange::FadeDuration(duration);
assert!(state.apply(&change));
assert_eq!(state.fade_duration(), Some(duration));
}
#[test]
fn system_info_new_is_empty() {
let info = SystemInfo::new();
assert!(info.is_empty());
assert!(info.uptime().is_none());
assert!(info.wifi_rssi().is_none());
assert!(info.heap().is_none());
}
#[test]
fn system_info_builder_pattern() {
let info = SystemInfo::new()
.with_uptime(Duration::from_secs(172800))
.with_wifi_rssi(-55)
.with_heap(25000);
assert!(!info.is_empty());
assert_eq!(info.uptime(), Some(Duration::from_secs(172800)));
assert_eq!(info.wifi_rssi(), Some(-55));
assert_eq!(info.heap(), Some(25000));
}
#[test]
fn system_info_merge_preserves_existing() {
let mut info = SystemInfo::new()
.with_uptime(Duration::from_secs(100))
.with_wifi_rssi(-50);
let update = SystemInfo::new().with_heap(30000);
info.merge(&update);
assert_eq!(info.uptime(), Some(Duration::from_secs(100)));
assert_eq!(info.wifi_rssi(), Some(-50));
assert_eq!(info.heap(), Some(30000));
}
#[test]
fn system_info_merge_updates_values() {
let mut info = SystemInfo::new()
.with_uptime(Duration::from_secs(100))
.with_wifi_rssi(-50);
let update = SystemInfo::new()
.with_uptime(Duration::from_secs(200))
.with_heap(30000);
info.merge(&update);
assert_eq!(info.uptime(), Some(Duration::from_secs(200)));
assert_eq!(info.wifi_rssi(), Some(-50)); assert_eq!(info.heap(), Some(30000));
}
#[test]
fn device_state_system_info_getters_setters() {
let mut state = DeviceState::new();
assert!(state.system_info().is_none());
assert!(state.uptime().is_none());
let info = SystemInfo::new().with_uptime(Duration::from_secs(172800));
state.set_system_info(info);
assert!(state.system_info().is_some());
assert_eq!(state.uptime(), Some(Duration::from_secs(172800)));
}
#[test]
fn device_state_update_system_info() {
let mut state = DeviceState::new();
let info1 = SystemInfo::new().with_uptime(Duration::from_secs(100));
state.update_system_info(&info1);
assert_eq!(state.uptime(), Some(Duration::from_secs(100)));
let info2 = SystemInfo::new().with_wifi_rssi(-55);
state.update_system_info(&info2);
let sys_info = state.system_info().unwrap();
assert_eq!(sys_info.uptime(), Some(Duration::from_secs(100))); assert_eq!(sys_info.wifi_rssi(), Some(-55)); }
#[test]
fn device_state_clear_clears_system_info() {
let mut state = DeviceState::new();
state.set_system_info(SystemInfo::new().with_uptime(Duration::from_secs(172800)));
state.clear();
assert!(state.system_info().is_none());
}
#[test]
fn system_info_serialization() {
let info = SystemInfo::new()
.with_uptime(Duration::from_secs(172800))
.with_wifi_rssi(-55)
.with_heap(25000);
let json = serde_json::to_string(&info).unwrap();
let deserialized: SystemInfo = serde_json::from_str(&json).unwrap();
assert_eq!(info, deserialized);
}
#[test]
fn device_state_with_system_info_serialization() {
let mut state = DeviceState::new();
state.set_power(1, PowerState::On);
state.set_system_info(
SystemInfo::new()
.with_uptime(Duration::from_secs(172800))
.with_wifi_rssi(-55),
);
let json = serde_json::to_string(&state).unwrap();
let deserialized: DeviceState = serde_json::from_str(&json).unwrap();
assert_eq!(state, deserialized);
assert_eq!(deserialized.uptime(), Some(Duration::from_secs(172800)));
}
}