use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum PqEventType {
#[default]
None,
Dip, Swell, Interruption, }
impl PqEventType {
pub fn as_str(&self) -> &'static str {
match self {
PqEventType::None => "None",
PqEventType::Dip => "Dip",
PqEventType::Swell => "Swell",
PqEventType::Interruption => "Interruption",
}
}
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
pub struct PqEventRecord {
pub event_type: PqEventType,
pub phase_index: u8,
pub start_timestamp_ns: u64,
pub duration_ms: f32,
pub extremum_v: f32, pub reference_v: f32, pub is_active: bool,
}
#[derive(Debug, Clone, Copy)]
pub struct PqEventConfig {
pub nominal_voltage: f32, pub dip_threshold_pct: f32, pub swell_threshold_pct: f32, pub interrupt_threshold_pct: f32, pub hysteresis_pct: f32, }
impl Default for PqEventConfig {
fn default() -> Self {
Self {
nominal_voltage: 230.0,
dip_threshold_pct: 90.0,
swell_threshold_pct: 110.0,
interrupt_threshold_pct: 10.0,
hysteresis_pct: 1.0,
}
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct PowerQualityEventDetector {
pub active_event: PqEventRecord,
pub last_completed_event: PqEventRecord,
pub event_count: u32,
pub dip_count: u32,
pub swell_count: u32,
pub interruption_count: u32,
}
impl PowerQualityEventDetector {
pub fn process_half_cycle(
&mut self,
phase_index: u8,
urms_half: f32,
now_ns: u64,
config: &PqEventConfig,
) -> Option<PqEventRecord> {
let u_din = config.nominal_voltage;
if u_din <= 0.0 || urms_half <= 0.0 {
return None;
}
let dip_thresh = u_din * (config.dip_threshold_pct / 100.0);
let swell_thresh = u_din * (config.swell_threshold_pct / 100.0);
let int_thresh = u_din * (config.interrupt_threshold_pct / 100.0);
let hyst = u_din * (config.hysteresis_pct / 100.0);
let current_type = if urms_half < int_thresh {
PqEventType::Interruption
} else if urms_half < dip_thresh {
PqEventType::Dip
} else if urms_half > swell_thresh {
PqEventType::Swell
} else {
PqEventType::None
};
if self.active_event.is_active {
let event_type = self.active_event.event_type;
let ended = match event_type {
PqEventType::Interruption => urms_half >= int_thresh + hyst,
PqEventType::Dip => urms_half >= dip_thresh + hyst,
PqEventType::Swell => urms_half <= swell_thresh - hyst,
PqEventType::None => true,
};
if ended {
self.active_event.is_active = false;
if now_ns >= self.active_event.start_timestamp_ns {
self.active_event.duration_ms =
(now_ns - self.active_event.start_timestamp_ns) as f32 / 1_000_000.0;
}
self.last_completed_event = self.active_event;
self.active_event = PqEventRecord::default();
return Some(self.last_completed_event);
} else {
match event_type {
PqEventType::Dip | PqEventType::Interruption => {
self.active_event.extremum_v = self.active_event.extremum_v.min(urms_half);
}
PqEventType::Swell => {
self.active_event.extremum_v = self.active_event.extremum_v.max(urms_half);
}
_ => {}
}
if now_ns >= self.active_event.start_timestamp_ns {
self.active_event.duration_ms =
(now_ns - self.active_event.start_timestamp_ns) as f32 / 1_000_000.0;
}
}
} else {
if current_type != PqEventType::None {
self.active_event = PqEventRecord {
event_type: current_type,
phase_index,
start_timestamp_ns: now_ns,
duration_ms: 0.0,
extremum_v: urms_half,
reference_v: u_din,
is_active: true,
};
self.event_count += 1;
match current_type {
PqEventType::Dip => self.dip_count += 1,
PqEventType::Swell => self.swell_count += 1,
PqEventType::Interruption => self.interruption_count += 1,
_ => {}
}
}
}
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_dip_detection() {
let mut detector = PowerQualityEventDetector::default();
let config = PqEventConfig::default();
assert!(detector
.process_half_cycle(0, 230.0, 1_000_000_000, &config)
.is_none());
assert!(!detector.active_event.is_active);
assert!(detector
.process_half_cycle(0, 180.0, 1_010_000_000, &config)
.is_none());
assert!(detector.active_event.is_active);
assert_eq!(detector.active_event.event_type, PqEventType::Dip);
assert_eq!(detector.dip_count, 1);
let event = detector.process_half_cycle(0, 235.0, 1_060_000_000, &config);
assert!(event.is_some());
let completed = event.unwrap();
assert_eq!(completed.event_type, PqEventType::Dip);
assert_eq!(completed.duration_ms, 50.0);
assert_eq!(completed.extremum_v, 180.0);
}
#[test]
fn test_swell_detection() {
let mut detector = PowerQualityEventDetector::default();
let config = PqEventConfig::default();
assert!(detector
.process_half_cycle(0, 230.0, 1_000_000_000, &config)
.is_none());
assert!(detector
.process_half_cycle(0, 270.0, 1_010_000_000, &config)
.is_none());
assert!(detector.active_event.is_active);
assert_eq!(detector.active_event.event_type, PqEventType::Swell);
assert_eq!(detector.swell_count, 1);
let event = detector.process_half_cycle(0, 220.0, 1_110_000_000, &config);
assert!(event.is_some());
let completed = event.unwrap();
assert_eq!(completed.event_type, PqEventType::Swell);
assert_eq!(completed.duration_ms, 100.0);
assert_eq!(completed.extremum_v, 270.0);
}
}