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 {
pub const ALL: [Self; 3] = [Self::Slp, Self::Rlm, Self::IMsys];
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Slp => "SLP",
Self::Rlm => "RLM",
Self::IMsys => "I_MSYS",
}
}
#[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();
IntervalResolution::from_observed_seconds(durations[durations.len() / 2])
}
#[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 const fn as_str(self) -> &'static str {
match self {
Self::SmartMeterGateway => "SMART_METER_GATEWAY",
Self::Other => "OTHER",
}
}
}
crate::codes::string_codes! {
Messtyp;
SeriesOrigin;
}
#[must_use]
pub fn classify_messtyp(
intervals: &[MeterInterval],
origin: Option<SeriesOrigin>,
) -> Option<Messtyp> {
if origin == Some(SeriesOrigin::SmartMeterGateway) {
return Some(Messtyp::IMsys);
}
if intervals.is_empty() {
return None;
}
Some(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), Some(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)),
Some(Messtyp::IMsys)
);
assert_eq!(
classify_messtyp(&intervals, Some(SeriesOrigin::Other)),
Some(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)),
Some(Messtyp::IMsys)
);
assert_eq!(classify_messtyp(&daily, None), Some(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), Some(Messtyp::Slp));
}
#[test]
fn an_empty_series_classifies_as_nothing() {
assert_eq!(classify_messtyp(&[], None), None);
assert_eq!(classify_messtyp(&[], Some(SeriesOrigin::Other)), None);
assert_eq!(
classify_messtyp(&[], Some(SeriesOrigin::SmartMeterGateway)),
Some(Messtyp::IMsys)
);
}
#[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());
}
}