use rust_decimal::Decimal;
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 EnergyDirection {
Import,
Export,
Combined,
}
impl EnergyDirection {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::Import => "Bezug",
Self::Export => "Einspeisung",
Self::Combined => "Kombiniert",
}
}
}
#[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 RegisterUnit {
KWh,
KVarh,
KW,
KVar,
M3,
KWhTh,
}
impl RegisterUnit {
#[must_use]
pub fn symbol(self) -> &'static str {
match self {
Self::KWh => "kWh",
Self::KVarh => "kvarh",
Self::KW => "kW",
Self::KVar => "kvar",
Self::M3 => "m³",
Self::KWhTh => "kWh_th",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct MeterRegister {
pub malo_id: String,
pub meter_serial: String,
pub register_number: u8,
pub obis_code: ObisCode,
pub direction: EnergyDirection,
pub unit: RegisterUnit,
pub wandler_factor: Decimal,
pub valid_from: Date,
pub valid_to: Option<Date>,
}
impl MeterRegister {
#[must_use]
pub fn is_ht(&self) -> bool {
self.obis_code.is_ht()
}
#[must_use]
pub fn is_nt(&self) -> bool {
self.obis_code.is_nt()
}
#[must_use]
pub fn is_total(&self) -> bool {
self.obis_code.is_total_register()
}
#[must_use]
pub fn is_import(&self) -> bool {
matches!(
self.direction,
EnergyDirection::Import | EnergyDirection::Combined
) || self.obis_code.is_import()
}
#[must_use]
pub fn is_einspeisung(&self) -> bool {
matches!(self.direction, EnergyDirection::Export) || self.obis_code.is_einspeisung()
}
#[must_use]
pub fn is_active(&self, today: Date) -> bool {
self.valid_to.is_none_or(|end| end >= today)
}
#[must_use]
pub fn apply_wandler(&self, raw_value: Decimal) -> Decimal {
raw_value * self.wandler_factor
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::obis::ObisCode;
use rust_decimal_macros::dec;
use time::macros::date;
fn make_register(obis: ObisCode, register_number: u8) -> MeterRegister {
MeterRegister {
malo_id: "11X0-0000-0000-1".to_owned(),
meter_serial: "A1B2C3D4".to_owned(),
register_number,
obis_code: obis,
direction: EnergyDirection::Import,
unit: RegisterUnit::KWh,
wandler_factor: dec!(1),
valid_from: date!(2024 - 01 - 01),
valid_to: None,
}
}
#[test]
fn total_register_classification() {
let reg = make_register(ObisCode::STROM_BEZUG_TOTAL, 0);
assert!(reg.is_total());
assert!(!reg.is_ht());
assert!(!reg.is_nt());
}
#[test]
fn ht_register_classification() {
let reg = make_register(ObisCode::STROM_BEZUG_HT, 1);
assert!(reg.is_ht());
assert!(!reg.is_total());
assert!(!reg.is_nt());
}
#[test]
fn nt_register_classification() {
let reg = make_register(ObisCode::STROM_BEZUG_NT, 2);
assert!(reg.is_nt());
assert!(!reg.is_ht());
}
#[test]
fn wandler_factor_applied() {
let mut reg = make_register(ObisCode::STROM_BEZUG_TOTAL, 0);
reg.wandler_factor = dec!(100);
let raw = dec!(1234);
assert_eq!(reg.apply_wandler(raw), dec!(123400));
}
#[test]
fn active_status_check() {
let today = date!(2026 - 07 - 15);
let mut reg = make_register(ObisCode::STROM_BEZUG_TOTAL, 0);
assert!(reg.is_active(today));
reg.valid_to = Some(date!(2026 - 01 - 01));
assert!(!reg.is_active(today));
}
#[test]
fn einspeisung_direction() {
let mut reg = make_register(ObisCode::STROM_EINSPEISUNG_TOTAL, 0);
reg.direction = EnergyDirection::Export;
assert!(reg.is_einspeisung());
assert!(!reg.is_import());
}
#[test]
fn unit_symbols() {
assert_eq!(RegisterUnit::KWh.symbol(), "kWh");
assert_eq!(RegisterUnit::M3.symbol(), "m³");
assert_eq!(RegisterUnit::KVarh.symbol(), "kvarh");
}
}