ocpp_service/smart/
v21.rs1use ocpp21::{
8 ChargingProfile, ChargingProfileKindEnum, ChargingRateUnitEnum, ChargingSchedule,
9 ChargingScheduleUpdate, RecurrencyKindEnum,
10};
11use serde_json::{Value, json};
12
13use super::model::{
14 self, Kind, Period, Profile, Purpose, Recurrency, RuleViolation, Schedule, Unit,
15};
16
17pub fn schedule(s: &ChargingSchedule) -> Schedule {
18 Schedule {
19 start: s.start_schedule,
20 duration: s.duration,
21 unit: match s.charging_rate_unit {
22 ChargingRateUnitEnum::A => Unit::A,
23 ChargingRateUnitEnum::W => Unit::W,
24 },
25 periods: s
26 .charging_schedule_period
27 .iter()
28 .map(|p| Period {
29 start: p.start_period,
30 limit: p.limit,
31 phases: p.number_phases,
32 })
33 .collect(),
34 }
35}
36
37pub fn normalize(
39 evse_id: i64,
40 p: &ChargingProfile,
41 update: Option<(&ChargingScheduleUpdate, ocpp_core::DateTime)>,
42) -> Profile {
43 let mut schedule = schedule(&p.charging_schedule[0]);
44 let mut dyn_updated_at = p.dyn_update_time;
45 if let Some((u, at)) = update
46 && let Some(first) = schedule.periods.first_mut()
47 {
48 if u.limit.is_some() {
49 first.limit = u.limit;
50 }
51 dyn_updated_at = Some(at);
52 }
53 Profile {
54 id: p.id,
55 evse: evse_id,
56 purpose: Purpose::parse(p.charging_profile_purpose.as_str())
57 .expect("2.1 purposes are known"),
58 stack_level: p.stack_level,
59 kind: match p.charging_profile_kind {
60 ChargingProfileKindEnum::Absolute => Kind::Absolute,
61 ChargingProfileKindEnum::Recurring => Kind::Recurring,
62 ChargingProfileKindEnum::Relative => Kind::Relative,
63 ChargingProfileKindEnum::Dynamic => Kind::Dynamic,
64 },
65 recurrency: p.recurrency_kind.map(|r| match r {
66 RecurrencyKindEnum::Daily => Recurrency::Daily,
67 RecurrencyKindEnum::Weekly => Recurrency::Weekly,
68 }),
69 valid_from: p.valid_from,
70 valid_to: p.valid_to,
71 transaction: p.transaction_id.clone(),
72 schedule,
73 dyn_updated_at,
74 }
75}
76
77pub fn decode(v: &Value) -> Result<ChargingProfile, String> {
78 ocpp_core::decode_value(v.clone()).map_err(|e| e.to_string())
79}
80
81pub fn prepare(
82 evse_id: i64,
83 mut p: ChargingProfile,
84 active_tx: Option<&str>,
85) -> Result<(Profile, Value), RuleViolation> {
86 let purpose = p.charging_profile_purpose.as_str();
87 if purpose == "ChargingStationExternalConstraints" {
88 return Err(RuleViolation::new(
89 "K01.FR.22",
90 "the CSMS must not set ChargingStationExternalConstraints profiles",
91 ));
92 }
93 if purpose == "TxProfile" {
94 let Some(active) = active_tx else {
95 return Err(RuleViolation::new(
96 "K01.FR.03",
97 "a TxProfile requires an active transaction on the EVSE",
98 ));
99 };
100 match &p.transaction_id {
101 None => p.transaction_id = Some(active.to_owned()),
102 Some(t) if t != active => {
103 return Err(RuleViolation::new(
104 "K01.FR.03",
105 format!("transactionId {t} is not the active transaction {active} of the EVSE"),
106 ));
107 }
108 Some(_) => {}
109 }
110 }
111 let dynamic = p.charging_profile_kind == ChargingProfileKindEnum::Dynamic;
112 if dynamic
113 && (p.charging_schedule.len() != 1
114 || p.charging_schedule[0].charging_schedule_period.len() != 1)
115 {
116 return Err(RuleViolation::new(
117 "K28.FR.01",
118 "a Dynamic profile has exactly one chargingSchedule with one chargingSchedulePeriod",
119 ));
120 }
121 if !dynamic && p.dyn_update_interval.is_some() {
122 return Err(RuleViolation::new(
123 "K28.FR.04",
124 "dynUpdateInterval only applies to Dynamic profiles",
125 ));
126 }
127 for s in &p.charging_schedule {
128 if purpose == "PriorityCharging" && s.duration.is_some() {
129 return Err(RuleViolation::new(
130 "K01.FR.73",
131 "a PriorityCharging schedule must not have a duration",
132 ));
133 }
134 for per in &s.charging_schedule_period {
135 if per.phase_to_use.is_some() && per.number_phases != Some(1) {
136 return Err(RuleViolation::new(
137 "K01.FR.19",
138 "phaseToUse may only be set when numberPhases is 1",
139 ));
140 }
141 let has_l23 = per.limit_l2.is_some() || per.limit_l3.is_some();
142 if has_l23 && purpose != "TxProfile" {
143 return Err(RuleViolation::new(
144 "K01.FR.143",
145 "limit_L2/limit_L3 are only allowed in a TxProfile",
146 ));
147 }
148 if has_l23 && per.limit.is_none() {
149 return Err(RuleViolation::new(
150 "K01.FR.145",
151 "limit_L2/limit_L3 require limit (phase L1)",
152 ));
153 }
154 }
155 }
156 let profile = normalize(evse_id, &p, None);
157 model::check_structure(&profile)?;
158 let payload = json!({ "evseId": evse_id, "chargingProfile": p });
159 Ok((profile, payload))
160}