use serde::Deserialize;
use std::sync::OnceLock;
use super::{ConditionResult, EvaluationContext};
use crate::eval::format_validators::parse_obis;
const EMBEDDED_JSON: &str = include_str!("../../data/verwendungszwecke.json");
#[derive(Debug, Clone, Deserialize)]
pub struct Verwendungszweck {
pub code: String,
pub name: String,
pub locations: Vec<String>,
pub roles: Vec<String>,
#[serde(default)]
pub explanation: Option<String>,
#[serde(default)]
pub obis_messarten: Vec<u32>,
#[serde(default)]
pub lieferrichtung: Option<String>,
}
#[derive(Debug, Deserialize)]
struct Table {
codes: Vec<Verwendungszweck>,
}
pub fn codes() -> &'static [Verwendungszweck] {
static TABLE: OnceLock<Vec<Verwendungszweck>> = OnceLock::new();
TABLE.get_or_init(|| {
serde_json::from_str::<Table>(EMBEDDED_JSON)
.expect("embedded verwendungszwecke.json")
.codes
})
}
pub fn location_and_role(condition: u32) -> Option<(&'static str, &'static str)> {
Some(match condition {
185 | 381 => ("MaLo", "NB"),
186 | 382 => ("MaLo", "LF"),
187 | 383 => ("MaLo", "ÜNB"),
274 | 422 => ("Tranche", "NB"),
275 | 423 => ("Tranche", "LF"),
276 | 424 => ("Tranche", "ÜNB"),
353 => ("NeLo", "NB"),
354 => ("NeLo", "LF"),
_ => return None,
})
}
pub fn allows(value: &str, location: &str, role: &str, obis: Option<&str>) -> bool {
let Some(entry) = codes().iter().find(|c| c.code == value) else {
return false;
};
if !entry.locations.iter().any(|l| l == location) || !entry.roles.iter().any(|r| r == role) {
return false;
}
let Some((c, d)) = obis.and_then(parse_obis) else {
return true;
};
if !entry.obis_messarten.is_empty() && !entry.obis_messarten.contains(&d) {
return false;
}
!matches!(
(entry.lieferrichtung.as_deref(), c),
(Some("Verbrauch"), 2) | (Some("Erzeugung"), 1)
)
}
pub fn evaluate(condition: u32, ctx: &EvaluationContext) -> ConditionResult {
let Some((location, role)) = location_and_role(condition) else {
return ConditionResult::Unknown;
};
let Some(value) = ctx.resolved_value.filter(|v| !v.is_empty()) else {
return ConditionResult::True;
};
let obis = ctx
.resolved_segment
.and_then(|elements| elements.get(1))
.and_then(|c212| c212.first())
.map(String::as_str)
.filter(|v| !v.is_empty());
ConditionResult::from(allows(value, location, role, obis))
}
pub fn candidate_values(message_type: &str, fv: &str, condition: u32) -> Vec<String> {
if message_type != "UTILMD" || fv != "FV2610" {
return Vec::new();
}
let Some((location, role)) = location_and_role(condition) else {
return Vec::new();
};
codes()
.iter()
.filter(|c| c.locations.iter().any(|l| l == location) && c.roles.iter().any(|r| r == role))
.map(|c| c.code.clone())
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_list_loads() {
assert_eq!(codes().len(), 19);
let v16 = codes().iter().find(|c| c.code == "V16").unwrap();
assert_eq!(v16.roles, ["ÜNB"]);
}
#[test]
fn a_code_is_judged_by_location_role_and_obis() {
assert!(allows("V01", "MaLo", "NB", Some("1-0:1.8.0")));
assert!(!allows("V01", "MaLo", "ÜNB", Some("1-0:1.8.0")));
assert!(!allows("V01", "Tranche", "NB", Some("1-0:1.8.0")));
assert!(!allows("V01", "MaLo", "NB", Some("1-0:2.8.0")));
assert!(allows("V16", "MaLo", "ÜNB", Some("1-1:1.29.0")));
assert!(!allows("V16", "MaLo", "ÜNB", Some("1-0:1.8.0")));
assert!(allows("V05", "MaLo", "LF", Some("1-0:1.9.1")));
assert!(allows("V05", "MaLo", "LF", Some("1-0:1.6.0")));
assert!(!allows("V05", "MaLo", "LF", Some("1-0:1.29.0")));
assert!(!allows("5XL", "MaLo", "LF", None));
}
#[test]
fn candidates_follow_location_and_role() {
let lf_tranche = candidate_values("UTILMD", "FV2610", 423);
assert!(lf_tranche.contains(&"V07".to_string()));
assert!(!lf_tranche.contains(&"V01".to_string()));
assert!(candidate_values("UTILMD", "FV2604", 423).is_empty());
}
}