use rust_decimal::Decimal;
use time::OffsetDateTime;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct PowerQualityInterval {
pub from: OffsetDateTime,
pub to: OffsetDateTime,
pub voltage_l1_v: Option<Decimal>,
pub voltage_l2_v: Option<Decimal>,
pub voltage_l3_v: Option<Decimal>,
pub current_l1_a: Option<Decimal>,
pub current_l2_a: Option<Decimal>,
pub current_l3_a: Option<Decimal>,
pub frequency_hz: Option<Decimal>,
pub power_factor: Option<Decimal>,
pub thd_voltage_pct: Option<Decimal>,
pub thd_current_pct: Option<Decimal>,
}
impl PowerQualityInterval {
#[must_use]
pub fn voltage_out_of_range(&self, nominal_v: Decimal, threshold_pct: Decimal) -> bool {
let check = |v: Option<Decimal>| {
v.map(|voltage| {
let deviation = ((voltage - nominal_v) / nominal_v * Decimal::from(100)).abs();
deviation > threshold_pct
})
.unwrap_or(false)
};
check(self.voltage_l1_v) || check(self.voltage_l2_v) || check(self.voltage_l3_v)
}
#[must_use]
pub fn frequency_out_of_range(&self, threshold_hz: Decimal) -> bool {
self.frequency_hz
.map(|f| (f - Decimal::from(50u32)).abs() > threshold_hz)
.unwrap_or(false)
}
#[must_use]
pub fn power_factor_below_threshold(&self, min_pf: Decimal) -> bool {
self.power_factor.map(|pf| pf < min_pf).unwrap_or(false)
}
#[must_use]
pub fn voltage_thd_exceeded(&self, max_thd_pct: Decimal) -> bool {
self.thd_voltage_pct
.map(|thd| thd > max_thd_pct)
.unwrap_or(false)
}
#[must_use]
pub fn has_quality_issue(&self) -> bool {
self.voltage_out_of_range(Decimal::from(230u32), Decimal::from(10u32))
|| self.frequency_out_of_range(rust_decimal::Decimal::new(5, 1)) || self.power_factor_below_threshold(rust_decimal::Decimal::new(9, 1)) || self.voltage_thd_exceeded(Decimal::from(8u32))
}
}
impl Default for PowerQualityInterval {
fn default() -> Self {
use time::OffsetDateTime;
Self {
from: OffsetDateTime::UNIX_EPOCH,
to: OffsetDateTime::UNIX_EPOCH,
voltage_l1_v: None,
voltage_l2_v: None,
voltage_l3_v: None,
current_l1_a: None,
current_l2_a: None,
current_l3_a: None,
frequency_hz: None,
power_factor: None,
thd_voltage_pct: None,
thd_current_pct: None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use rust_decimal::dec;
use time::macros::datetime;
fn pq(v_l1: Option<Decimal>, freq: Option<Decimal>) -> PowerQualityInterval {
PowerQualityInterval {
from: datetime!(2026-01-01 0:00 UTC),
to: datetime!(2026-01-01 0:15 UTC),
voltage_l1_v: v_l1,
frequency_hz: freq,
..Default::default()
}
}
#[test]
fn nominal_voltage_in_range() {
let iv = pq(Some(dec!(230)), None);
assert!(!iv.voltage_out_of_range(dec!(230), dec!(10)));
}
#[test]
fn overvoltage_detected() {
let iv = pq(Some(dec!(254)), None); assert!(iv.voltage_out_of_range(dec!(230), dec!(10)));
}
#[test]
fn nominal_frequency_in_range() {
let iv = pq(None, Some(dec!(50.0)));
assert!(!iv.frequency_out_of_range(dec!(0.5)));
}
#[test]
fn frequency_deviation_detected() {
let iv = pq(None, Some(dec!(49.0)));
assert!(iv.frequency_out_of_range(dec!(0.5)));
}
#[test]
fn no_measurement_returns_false() {
let iv = pq(None, None);
assert!(!iv.voltage_out_of_range(dec!(230), dec!(10)));
assert!(!iv.frequency_out_of_range(dec!(0.5)));
}
}