use rust_decimal::Decimal;
use time::OffsetDateTime;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "SCREAMING_SNAKE_CASE"))]
pub enum Sparte {
#[default]
Strom,
Gas,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "SCREAMING_SNAKE_CASE"))]
pub enum QualityFlag {
Measured,
Estimated,
Substituted,
Calculated,
Corrected,
Preliminary,
Faulty,
#[default]
Unknown,
}
impl QualityFlag {
#[must_use]
pub fn is_billable(&self) -> bool {
matches!(
self,
QualityFlag::Measured
| QualityFlag::Estimated
| QualityFlag::Substituted
| QualityFlag::Calculated
| QualityFlag::Corrected
| QualityFlag::Preliminary
)
}
#[must_use]
pub fn is_provisional(&self) -> bool {
matches!(self, QualityFlag::Preliminary | QualityFlag::Estimated)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct MeterInterval {
pub from: OffsetDateTime,
pub to: OffsetDateTime,
pub value_kwh: Decimal,
pub quality: QualityFlag,
pub obis_code: Option<String>,
}
impl MeterInterval {
#[must_use]
pub fn duration_secs(&self) -> i64 {
(self.to - self.from).whole_seconds()
}
#[must_use]
pub fn duration_minutes(&self) -> i64 {
(self.to - self.from).whole_minutes()
}
#[must_use]
pub fn demand_kw(&self) -> Option<Decimal> {
let h = Decimal::from(self.duration_secs()) / Decimal::from(3600u32);
if h.is_zero() {
None
} else {
Some(self.value_kwh / h)
}
}
#[must_use]
pub fn parsed_obis_code(&self) -> Option<crate::obis::ObisCode> {
self.obis_code.as_deref()?.parse().ok()
}
#[must_use]
pub fn is_import_energy(&self) -> Option<bool> {
self.parsed_obis_code().map(|c| c.is_import())
}
#[must_use]
pub fn is_export_energy(&self) -> Option<bool> {
self.parsed_obis_code().map(|c| c.is_export())
}
#[must_use]
pub fn tariff_register(&self) -> Option<u8> {
self.parsed_obis_code()?.tariff_register()
}
}
#[cfg(test)]
mod tests {
use super::*;
use rust_decimal_macros::dec;
use time::macros::datetime;
#[test]
fn demand_kw_15min_interval() {
let iv = MeterInterval {
from: datetime!(2026-01-01 0:00 UTC),
to: datetime!(2026-01-01 0:15 UTC),
value_kwh: dec!(2.5),
quality: QualityFlag::Measured,
obis_code: None,
};
assert_eq!(iv.demand_kw(), Some(dec!(10)));
}
#[test]
fn demand_kw_hourly() {
let iv = MeterInterval {
from: datetime!(2026-01-01 0:00 UTC),
to: datetime!(2026-01-01 1:00 UTC),
value_kwh: dec!(5.0),
quality: QualityFlag::Measured,
obis_code: None,
};
assert_eq!(iv.demand_kw(), Some(dec!(5)));
}
#[test]
fn quality_flag_billable() {
assert!(QualityFlag::Measured.is_billable());
assert!(QualityFlag::Substituted.is_billable());
assert!(
QualityFlag::Estimated.is_billable(),
"Estimated must be billable per §17 MessZV"
);
assert!(QualityFlag::Corrected.is_billable());
assert!(QualityFlag::Preliminary.is_billable());
assert!(!QualityFlag::Faulty.is_billable());
assert!(!QualityFlag::Unknown.is_billable());
}
#[test]
fn quality_flag_provisional() {
assert!(QualityFlag::Estimated.is_provisional());
assert!(QualityFlag::Preliminary.is_provisional());
assert!(!QualityFlag::Measured.is_provisional());
assert!(!QualityFlag::Substituted.is_provisional());
}
}