use crate::interval::MeterInterval;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "SCREAMING_SNAKE_CASE"))]
pub enum Messtyp {
Slp,
Rlm,
IMsys,
}
impl Messtyp {
#[must_use]
pub fn supports_spitzenleistung(&self) -> bool {
matches!(self, Messtyp::Rlm | Messtyp::IMsys)
}
#[must_use]
pub fn supports_dynamic_tariff(&self) -> bool {
matches!(self, Messtyp::IMsys)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IntervalLengthClass {
FifteenMin,
ThirtyMin,
SixtyMin,
Daily,
Monthly,
Other(i64),
}
impl IntervalLengthClass {
#[must_use]
pub fn seconds(&self) -> i64 {
match self {
IntervalLengthClass::FifteenMin => 900,
IntervalLengthClass::ThirtyMin => 1800,
IntervalLengthClass::SixtyMin => 3600,
IntervalLengthClass::Daily => 86400,
IntervalLengthClass::Monthly => 86400 * 30, IntervalLengthClass::Other(s) => *s,
}
}
}
#[must_use]
pub fn detect_interval_length(intervals: &[MeterInterval]) -> Option<IntervalLengthClass> {
if intervals.is_empty() {
return None;
}
let mut durations: Vec<i64> = intervals
.iter()
.map(|iv| iv.duration_secs())
.filter(|&d| d > 0)
.collect();
if durations.is_empty() {
return None;
}
durations.sort_unstable();
let median = durations[durations.len() / 2];
Some(match median {
750..=1050 => IntervalLengthClass::FifteenMin, 1650..=1950 => IntervalLengthClass::ThirtyMin, 3300..=3900 => IntervalLengthClass::SixtyMin, 82800..=90000 => IntervalLengthClass::Daily, _ => IntervalLengthClass::Other(median),
})
}
#[must_use]
pub fn classify_messtyp(intervals: &[MeterInterval], source: Option<&str>) -> Messtyp {
if let Some(src) = source {
let src_upper = src.to_uppercase();
if src_upper.contains("SMGW") || src_upper.contains("CLS") || src_upper.contains("IMSYS") {
return Messtyp::IMsys;
}
}
match detect_interval_length(intervals) {
Some(
IntervalLengthClass::FifteenMin
| IntervalLengthClass::ThirtyMin
| IntervalLengthClass::SixtyMin,
) => Messtyp::Rlm,
_ => Messtyp::Slp,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::interval::QualityFlag;
use rust_decimal::dec;
use time::macros::datetime;
fn iv_15min(i: u8) -> MeterInterval {
let base = datetime!(2026-01-01 0:00 UTC);
MeterInterval {
from: base + time::Duration::minutes(i as i64 * 15),
to: base + time::Duration::minutes(i as i64 * 15 + 15),
value_kwh: dec!(2.0),
quality: QualityFlag::Measured,
obis_code: None,
}
}
#[test]
fn classify_rlm_from_15min_intervals() {
let intervals: Vec<_> = (0..4).map(iv_15min).collect();
assert_eq!(classify_messtyp(&intervals, None), Messtyp::Rlm);
}
#[test]
fn classify_imsys_from_smgw_source() {
let intervals: Vec<_> = (0..4).map(iv_15min).collect();
assert_eq!(classify_messtyp(&intervals, Some("SMGW")), Messtyp::IMsys);
assert_eq!(
classify_messtyp(&intervals, Some("CLS_GATEWAY")),
Messtyp::IMsys
);
}
#[test]
fn classify_slp_from_daily_intervals() {
let base = datetime!(2026-01-01 0:00 UTC);
let intervals = vec![MeterInterval {
from: base,
to: base + time::Duration::days(1),
value_kwh: dec!(24.0),
quality: QualityFlag::Measured,
obis_code: None,
}];
assert_eq!(classify_messtyp(&intervals, None), Messtyp::Slp);
}
#[test]
fn detect_15min_length() {
let intervals: Vec<_> = (0..4).map(iv_15min).collect();
assert_eq!(
detect_interval_length(&intervals),
Some(IntervalLengthClass::FifteenMin)
);
}
#[test]
fn imsys_supports_dynamic_tariff_enwg_41a() {
assert!(Messtyp::IMsys.supports_dynamic_tariff());
assert!(!Messtyp::Rlm.supports_dynamic_tariff());
assert!(!Messtyp::Slp.supports_dynamic_tariff());
}
}