use ocpp_core::DateTime;
use serde::Serialize;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
pub enum Purpose {
StationMax,
TxDefault,
Tx,
ExternalConstraints,
PriorityCharging,
LocalGeneration,
}
impl Purpose {
pub fn parse(s: &str) -> Option<Purpose> {
Some(match s {
"ChargePointMaxProfile" | "ChargingStationMaxProfile" => Purpose::StationMax,
"TxDefaultProfile" => Purpose::TxDefault,
"TxProfile" => Purpose::Tx,
"ChargingStationExternalConstraints" => Purpose::ExternalConstraints,
"PriorityCharging" => Purpose::PriorityCharging,
"LocalGeneration" => Purpose::LocalGeneration,
_ => return None,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub enum Kind {
Absolute,
Recurring,
Relative,
Dynamic,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub enum Recurrency {
Daily,
Weekly,
}
impl Recurrency {
pub fn seconds(self) -> i64 {
match self {
Recurrency::Daily => 86_400,
Recurrency::Weekly => 7 * 86_400,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub enum Unit {
A,
W,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct Period {
pub start: i64,
pub limit: Option<f64>,
pub phases: Option<i64>,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct Schedule {
pub start: Option<DateTime>,
pub duration: Option<i64>,
pub unit: Unit,
pub periods: Vec<Period>,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct Profile {
pub id: i64,
pub evse: i64,
pub purpose: Purpose,
pub stack_level: i64,
pub kind: Kind,
pub recurrency: Option<Recurrency>,
pub valid_from: Option<DateTime>,
pub valid_to: Option<DateTime>,
pub transaction: Option<String>,
pub schedule: Schedule,
pub dyn_updated_at: Option<DateTime>,
}
impl Profile {
pub fn valid_at(&self, t: DateTime) -> bool {
self.valid_from.is_none_or(|f| f <= t) && self.valid_to.is_none_or(|to| t < to)
}
pub fn validity_overlaps(&self, other: &Profile) -> bool {
let starts_before_other_ends = match (self.valid_from, other.valid_to) {
(Some(a), Some(b)) => a < b,
_ => true,
};
let other_starts_before_self_ends = match (other.valid_from, self.valid_to) {
(Some(a), Some(b)) => a < b,
_ => true,
};
starts_before_other_ends && other_starts_before_self_ends
}
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct RuleViolation {
pub rule: &'static str,
pub message: String,
}
impl RuleViolation {
pub fn new(rule: &'static str, message: impl Into<String>) -> Self {
RuleViolation {
rule,
message: message.into(),
}
}
}
pub fn check_structure(p: &Profile) -> Result<(), RuleViolation> {
let periods = &p.schedule.periods;
if periods.first().is_none_or(|f| f.start != 0) {
return Err(RuleViolation::new(
"SchedulePeriodStart",
"the first chargingSchedulePeriod must have startPeriod 0",
));
}
if periods.windows(2).any(|w| w[1].start <= w[0].start) {
return Err(RuleViolation::new(
"SchedulePeriodOrder",
"chargingSchedulePeriod startPeriod values must be strictly increasing",
));
}
match p.kind {
Kind::Absolute if p.schedule.start.is_none() => Err(RuleViolation::new(
"ProfileKind",
"an Absolute profile requires startSchedule",
)),
Kind::Recurring if p.recurrency.is_none() => Err(RuleViolation::new(
"ProfileKind",
"a Recurring profile requires recurrencyKind",
)),
Kind::Recurring if p.schedule.start.is_none() => Err(RuleViolation::new(
"ProfileKind",
"a Recurring profile requires startSchedule",
)),
Kind::Relative if p.schedule.start.is_some() => Err(RuleViolation::new(
"ProfileKind",
"a Relative profile must not have startSchedule",
)),
_ => Ok(()),
}
.and_then(|()| {
if let (Some(f), Some(t)) = (p.valid_from, p.valid_to)
&& f >= t
{
return Err(RuleViolation::new(
"Validity",
"validFrom must be before validTo",
));
}
Ok(())
})
.and_then(|()| match p.purpose {
Purpose::StationMax if p.evse != 0 => Err(RuleViolation::new(
"StationMaxEvse",
"a ChargingStationMaxProfile/ChargePointMaxProfile can only be set on EVSE/connector 0",
)),
Purpose::Tx if p.evse <= 0 => Err(RuleViolation::new(
"TxProfileEvse",
"a TxProfile can only be set on an EVSE/connector > 0",
)),
_ => Ok(()),
})
}