1use ocpp_core::DateTime;
9use serde::Serialize;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
12pub enum Purpose {
13 StationMax,
15 TxDefault,
16 Tx,
17 ExternalConstraints,
19 PriorityCharging,
21 LocalGeneration,
23}
24
25impl Purpose {
26 pub fn parse(s: &str) -> Option<Purpose> {
27 Some(match s {
28 "ChargePointMaxProfile" | "ChargingStationMaxProfile" => Purpose::StationMax,
29 "TxDefaultProfile" => Purpose::TxDefault,
30 "TxProfile" => Purpose::Tx,
31 "ChargingStationExternalConstraints" => Purpose::ExternalConstraints,
32 "PriorityCharging" => Purpose::PriorityCharging,
33 "LocalGeneration" => Purpose::LocalGeneration,
34 _ => return None,
35 })
36 }
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
40pub enum Kind {
41 Absolute,
42 Recurring,
43 Relative,
44 Dynamic,
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
49pub enum Recurrency {
50 Daily,
51 Weekly,
52}
53
54impl Recurrency {
55 pub fn seconds(self) -> i64 {
56 match self {
57 Recurrency::Daily => 86_400,
58 Recurrency::Weekly => 7 * 86_400,
59 }
60 }
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
64pub enum Unit {
65 A,
66 W,
67}
68
69#[derive(Debug, Clone, PartialEq, Serialize)]
70pub struct Period {
71 pub start: i64,
73 pub limit: Option<f64>,
75 pub phases: Option<i64>,
76}
77
78#[derive(Debug, Clone, PartialEq, Serialize)]
79pub struct Schedule {
80 pub start: Option<DateTime>,
81 pub duration: Option<i64>,
82 pub unit: Unit,
83 pub periods: Vec<Period>,
84}
85
86#[derive(Debug, Clone, PartialEq, Serialize)]
87pub struct Profile {
88 pub id: i64,
89 pub evse: i64,
91 pub purpose: Purpose,
92 pub stack_level: i64,
93 pub kind: Kind,
94 pub recurrency: Option<Recurrency>,
95 pub valid_from: Option<DateTime>,
96 pub valid_to: Option<DateTime>,
97 pub transaction: Option<String>,
98 pub schedule: Schedule,
101 pub dyn_updated_at: Option<DateTime>,
103}
104
105impl Profile {
106 pub fn valid_at(&self, t: DateTime) -> bool {
107 self.valid_from.is_none_or(|f| f <= t) && self.valid_to.is_none_or(|to| t < to)
108 }
109
110 pub fn validity_overlaps(&self, other: &Profile) -> bool {
112 let starts_before_other_ends = match (self.valid_from, other.valid_to) {
113 (Some(a), Some(b)) => a < b,
114 _ => true,
115 };
116 let other_starts_before_self_ends = match (other.valid_from, self.valid_to) {
117 (Some(a), Some(b)) => a < b,
118 _ => true,
119 };
120 starts_before_other_ends && other_starts_before_self_ends
121 }
122}
123
124#[derive(Debug, Clone, PartialEq, Serialize)]
126pub struct RuleViolation {
127 pub rule: &'static str,
129 pub message: String,
130}
131
132impl RuleViolation {
133 pub fn new(rule: &'static str, message: impl Into<String>) -> Self {
134 RuleViolation {
135 rule,
136 message: message.into(),
137 }
138 }
139}
140
141pub fn check_structure(p: &Profile) -> Result<(), RuleViolation> {
143 let periods = &p.schedule.periods;
144 if periods.first().is_none_or(|f| f.start != 0) {
145 return Err(RuleViolation::new(
147 "SchedulePeriodStart",
148 "the first chargingSchedulePeriod must have startPeriod 0",
149 ));
150 }
151 if periods.windows(2).any(|w| w[1].start <= w[0].start) {
152 return Err(RuleViolation::new(
153 "SchedulePeriodOrder",
154 "chargingSchedulePeriod startPeriod values must be strictly increasing",
155 ));
156 }
157 match p.kind {
158 Kind::Absolute if p.schedule.start.is_none() => Err(RuleViolation::new(
159 "ProfileKind",
160 "an Absolute profile requires startSchedule",
161 )),
162 Kind::Recurring if p.recurrency.is_none() => Err(RuleViolation::new(
163 "ProfileKind",
164 "a Recurring profile requires recurrencyKind",
165 )),
166 Kind::Recurring if p.schedule.start.is_none() => Err(RuleViolation::new(
167 "ProfileKind",
168 "a Recurring profile requires startSchedule",
169 )),
170 Kind::Relative if p.schedule.start.is_some() => Err(RuleViolation::new(
171 "ProfileKind",
172 "a Relative profile must not have startSchedule",
173 )),
174 _ => Ok(()),
175 }
176 .and_then(|()| {
177 if let (Some(f), Some(t)) = (p.valid_from, p.valid_to)
178 && f >= t
179 {
180 return Err(RuleViolation::new(
181 "Validity",
182 "validFrom must be before validTo",
183 ));
184 }
185 Ok(())
186 })
187 .and_then(|()| match p.purpose {
188 Purpose::StationMax if p.evse != 0 => Err(RuleViolation::new(
189 "StationMaxEvse",
190 "a ChargingStationMaxProfile/ChargePointMaxProfile can only be set on EVSE/connector 0",
191 )),
192 Purpose::Tx if p.evse <= 0 => Err(RuleViolation::new(
193 "TxProfileEvse",
194 "a TxProfile can only be set on an EVSE/connector > 0",
195 )),
196 _ => Ok(()),
197 })
198}