use crate::interval::MeterInterval;
use crate::resolution::IntervalResolution;
#[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)
}
}
#[must_use]
pub fn detect_interval_length(intervals: &[MeterInterval]) -> Option<IntervalResolution> {
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 => IntervalResolution::QuarterHour, 1650..=1950 => IntervalResolution::HalfHour, 3300..=3900 => IntervalResolution::Hour, 82_800..=90_000 => IntervalResolution::Day,
other => IntervalResolution::Custom(u32::try_from(other).unwrap_or(u32::MAX)),
})
}
#[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 SeriesOrigin {
SmartMeterGateway,
Other,
}
impl SeriesOrigin {
pub const ALL: [Self; 2] = [Self::SmartMeterGateway, Self::Other];
}
#[must_use]
pub fn classify_messtyp(intervals: &[MeterInterval], origin: Option<SeriesOrigin>) -> Messtyp {
if origin == Some(SeriesOrigin::SmartMeterGateway) {
return Messtyp::IMsys;
}
match detect_interval_length(intervals) {
Some(
IntervalResolution::QuarterHour
| IntervalResolution::HalfHour
| IntervalResolution::Hour,
) => 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: 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 a_gateway_origin_outranks_the_interval_length() {
let intervals: Vec<_> = (0..4).map(iv_15min).collect();
assert_eq!(
classify_messtyp(&intervals, Some(SeriesOrigin::SmartMeterGateway)),
Messtyp::IMsys
);
assert_eq!(
classify_messtyp(&intervals, Some(SeriesOrigin::Other)),
Messtyp::Rlm
);
let base = datetime!(2026-01-01 0:00 UTC);
let daily = vec![MeterInterval {
from: base,
to: base + time::Duration::days(1),
value: dec!(24.0),
quality: QualityFlag::Measured,
obis_code: None,
}];
assert_eq!(
classify_messtyp(&daily, Some(SeriesOrigin::SmartMeterGateway)),
Messtyp::IMsys
);
assert_eq!(classify_messtyp(&daily, None), Messtyp::Slp);
}
#[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: 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(IntervalResolution::QuarterHour)
);
}
#[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());
}
}