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 DemandInterval {
pub from: OffsetDateTime,
pub to: OffsetDateTime,
pub demand_kw: Decimal,
pub obis_code: Option<crate::obis::ObisCode>,
}
impl DemandInterval {
#[must_use]
pub fn energy_to_demand_kw(energy_kwh: Decimal, interval_secs: u32) -> Decimal {
if interval_secs == 0 {
return Decimal::ZERO;
}
let hours = Decimal::from(interval_secs) / Decimal::from(3600u32);
energy_kwh / hours
}
}
#[derive(Debug, Clone, Default)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct DemandWindow {
intervals: Vec<DemandInterval>,
}
impl DemandWindow {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn push(&mut self, interval: DemandInterval) {
self.intervals.push(interval);
}
#[must_use]
pub fn peak_kw(&self) -> Option<Decimal> {
self.intervals
.iter()
.map(|i| i.demand_kw)
.reduce(Decimal::max)
}
#[must_use]
pub fn peak_interval(&self) -> Option<&DemandInterval> {
self.intervals.iter().max_by_key(|i| i.demand_kw)
}
#[must_use]
pub fn len(&self) -> usize {
self.intervals.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.intervals.is_empty()
}
#[must_use]
pub fn average_kw(&self) -> Option<Decimal> {
if self.intervals.is_empty() {
return None;
}
let sum: Decimal = self.intervals.iter().map(|i| i.demand_kw).sum();
Some(sum / Decimal::from(self.intervals.len()))
}
#[must_use]
pub fn from_intervals(
intervals: &[crate::interval::MeterInterval],
interval_secs: u32,
) -> Self {
let mut window = Self::new();
for iv in intervals {
let demand_kw = DemandInterval::energy_to_demand_kw(iv.value_kwh, interval_secs);
window.push(DemandInterval {
from: iv.from,
to: iv.to,
demand_kw,
obis_code: iv.parsed_obis_code(),
});
}
window
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::interval::{MeterInterval, QualityFlag};
use rust_decimal_macros::dec;
use time::macros::datetime;
#[test]
fn energy_to_demand_kw_15min() {
assert_eq!(DemandInterval::energy_to_demand_kw(dec!(15), 900), dec!(60));
}
#[test]
fn energy_to_demand_kw_hourly() {
assert_eq!(
DemandInterval::energy_to_demand_kw(dec!(10), 3600),
dec!(10)
);
}
#[test]
fn peak_kw_returns_maximum() {
let mut window = DemandWindow::new();
let base = datetime!(2026-01-01 0:00 UTC);
for (i, kw) in [dec!(30), dec!(48), dec!(35), dec!(22)].iter().enumerate() {
let from = base + time::Duration::minutes(i as i64 * 15);
window.push(DemandInterval {
from,
to: from + time::Duration::minutes(15),
demand_kw: *kw,
obis_code: None,
});
}
assert_eq!(window.peak_kw(), Some(dec!(48)));
assert_eq!(window.len(), 4);
}
#[test]
fn from_intervals_15min_conversion() {
let base = datetime!(2026-01-01 0:00 UTC);
let intervals = vec![MeterInterval {
from: base,
to: base + time::Duration::minutes(15),
value_kwh: dec!(12), quality: QualityFlag::Measured,
obis_code: None,
}];
let window = DemandWindow::from_intervals(&intervals, 900);
assert_eq!(window.peak_kw(), Some(dec!(48)));
}
#[test]
fn empty_window_returns_none() {
let window = DemandWindow::new();
assert!(window.peak_kw().is_none());
assert!(window.average_kw().is_none());
assert!(window.is_empty());
}
}