use time::Date;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use crate::obis::ObisCode;
#[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 MarktRolle {
Nb,
Lf,
Msb,
Bkv,
Uenb,
Eiv,
Direktvermarkter,
Mgv,
Esa,
}
impl MarktRolle {
#[must_use]
pub fn abbreviation(self) -> &'static str {
match self {
Self::Nb => "NB",
Self::Lf => "LF",
Self::Msb => "MSB",
Self::Bkv => "BKV",
Self::Uenb => "ÜNB",
Self::Eiv => "EIV",
Self::Direktvermarkter => "DV",
Self::Mgv => "MGV",
Self::Esa => "ESA",
}
}
#[must_use]
pub fn is_mscons_receiver(self) -> bool {
matches!(
self,
Self::Lf | Self::Bkv | Self::Mgv | Self::Direktvermarkter
)
}
}
#[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 EnergyFlow {
Consumption,
Generation,
StorageCharge,
StorageDischarge,
Bidirectional,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct MeasurementPoint {
pub malo_id: String,
pub melo_id: Option<String>,
pub meter_serial: Option<String>,
pub obis_code: ObisCode,
pub sparte: crate::interval::Sparte,
pub energy_flow: EnergyFlow,
pub accountable_role: MarktRolle,
pub accountable_mp_id: String,
pub is_virtual: bool,
pub valid_from: Date,
pub valid_to: Option<Date>,
}
impl MeasurementPoint {
#[must_use]
pub fn is_active(&self, on_date: Date) -> bool {
on_date >= self.valid_from && self.valid_to.is_none_or(|end| on_date <= end)
}
#[must_use]
pub fn is_bezug(&self) -> bool {
matches!(self.energy_flow, EnergyFlow::Consumption) || self.obis_code.is_import()
}
#[must_use]
pub fn is_einspeisung(&self) -> bool {
matches!(self.energy_flow, EnergyFlow::Generation) || self.obis_code.is_einspeisung()
}
#[must_use]
pub fn is_reactive(&self) -> bool {
self.obis_code.is_reactive()
}
#[must_use]
pub fn is_gas(&self) -> bool {
matches!(self.sparte, crate::interval::Sparte::Gas)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{interval::Sparte, obis::ObisCode};
use time::macros::date;
fn bezug_point() -> MeasurementPoint {
MeasurementPoint {
malo_id: "51238696780".to_owned(),
melo_id: Some("DE0012345678901234567890123456789".to_owned()),
meter_serial: Some("MSN-001".to_owned()),
obis_code: ObisCode::STROM_BEZUG_TOTAL,
sparte: Sparte::Strom,
energy_flow: EnergyFlow::Consumption,
accountable_role: MarktRolle::Lf,
accountable_mp_id: "9900987654321".to_owned(),
is_virtual: false,
valid_from: date!(2026 - 01 - 01),
valid_to: None,
}
}
#[test]
fn active_within_validity() {
let mp = bezug_point();
assert!(mp.is_active(date!(2026 - 06 - 15)));
}
#[test]
fn inactive_before_valid_from() {
let mp = bezug_point();
assert!(!mp.is_active(date!(2025 - 12 - 31)));
}
#[test]
fn inactive_after_valid_to() {
let mut mp = bezug_point();
mp.valid_to = Some(date!(2026 - 06 - 30));
assert!(!mp.is_active(date!(2026 - 07 - 01)));
assert!(mp.is_active(date!(2026 - 06 - 30)));
}
#[test]
fn bezug_detection() {
let mp = bezug_point();
assert!(mp.is_bezug());
assert!(!mp.is_einspeisung());
}
#[test]
fn einspeisung_detection() {
let mut mp = bezug_point();
mp.obis_code = ObisCode::STROM_EINSPEISUNG_TOTAL;
mp.energy_flow = EnergyFlow::Generation;
assert!(mp.is_einspeisung());
assert!(!mp.is_bezug());
}
#[test]
fn gas_detection() {
let mut mp = bezug_point();
mp.sparte = Sparte::Gas;
mp.obis_code = ObisCode::GAS_VOLUME_M3;
assert!(mp.is_gas());
}
#[test]
fn virtual_meter_flag() {
let mut mp = bezug_point();
mp.is_virtual = true;
assert!(mp.is_virtual);
}
#[test]
fn markt_rolle_mscons_receiver() {
assert!(MarktRolle::Lf.is_mscons_receiver());
assert!(MarktRolle::Bkv.is_mscons_receiver());
assert!(!MarktRolle::Nb.is_mscons_receiver());
assert!(!MarktRolle::Msb.is_mscons_receiver());
}
}