1use bon::Builder;
21use serde::{Deserialize, Serialize};
22
23use crate::ocpi_lenient_enum;
24use crate::types::validate_fields;
25use crate::types::{
26 CiString, CountryCode, DateTime, DisplayText, EvseId, Extensions, OcpiString, PartyId, PartyRef, Url,
27 Validate, Validator, ViolationCode,
28};
29
30use super::tokens::TokenType;
31
32pub use crate::v2_3_0::locations::{
34 AdditionalGeoLocation, BusinessDetails, Capability, ConnectorFormat, EnergyMix, EnergySource,
35 EnergySourceCategory, EnvironmentalImpact, EnvironmentalImpactCategory, ExceptionalPeriod, Facility,
36 GeoLocation, Hours, Image, ImageCategory, ParkingType, PowerType, RegularHours, Status, StatusSchedule,
37};
38
39#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Builder)]
43#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
44#[builder(on(_, into))]
45pub struct Location {
46 pub country_code: CountryCode,
48 pub party_id: PartyId,
50 pub id: CiString<36>,
52 pub publish: bool,
54 #[serde(default, skip_serializing_if = "Vec::is_empty")]
56 #[builder(default)]
57 pub publish_allowed_to: Vec<PublishTokenType>,
58 #[serde(default, skip_serializing_if = "Option::is_none")]
60 pub name: Option<OcpiString<255>>,
61 pub address: OcpiString<255>,
63 pub city: OcpiString<45>,
65 #[serde(default, skip_serializing_if = "Option::is_none")]
67 pub postal_code: Option<OcpiString<10>>,
68 #[serde(default, skip_serializing_if = "Option::is_none")]
70 pub state: Option<OcpiString<45>>,
71 pub country: OcpiString<3>,
73 pub coordinates: GeoLocation,
75 #[serde(default, skip_serializing_if = "Vec::is_empty")]
77 #[builder(default)]
78 pub related_locations: Vec<AdditionalGeoLocation>,
79 #[serde(default, skip_serializing_if = "Option::is_none")]
81 pub parking_type: Option<ParkingType>,
82 #[serde(default, skip_serializing_if = "Vec::is_empty")]
84 #[builder(default)]
85 pub evses: Vec<Evse>,
86 #[serde(default, skip_serializing_if = "Vec::is_empty")]
88 #[builder(default)]
89 pub directions: Vec<DisplayText>,
90 #[serde(default, skip_serializing_if = "Option::is_none")]
92 pub operator: Option<BusinessDetails>,
93 #[serde(default, skip_serializing_if = "Option::is_none")]
95 pub suboperator: Option<BusinessDetails>,
96 #[serde(default, skip_serializing_if = "Option::is_none")]
98 pub owner: Option<BusinessDetails>,
99 #[serde(default, skip_serializing_if = "Vec::is_empty")]
101 #[builder(default)]
102 pub facilities: Vec<Facility>,
103 pub time_zone: OcpiString<255>,
105 #[serde(default, skip_serializing_if = "Option::is_none")]
107 pub opening_times: Option<Hours>,
108 #[serde(default, skip_serializing_if = "Option::is_none")]
110 pub charging_when_closed: Option<bool>,
111 #[serde(default, skip_serializing_if = "Vec::is_empty")]
113 #[builder(default)]
114 pub images: Vec<Image>,
115 #[serde(default, skip_serializing_if = "Option::is_none")]
117 pub energy_mix: Option<EnergyMix>,
118 pub last_updated: DateTime,
120 #[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
122 #[builder(default)]
123 pub extensions: Extensions,
124}
125
126impl Location {
127 #[must_use]
129 pub fn owner_party(&self) -> PartyRef {
130 PartyRef { country_code: self.country_code.clone(), party_id: self.party_id.clone() }
131 }
132
133 #[must_use]
135 pub fn charging_when_closed_or_default(&self) -> bool {
136 self.charging_when_closed.unwrap_or(true)
137 }
138
139 #[must_use]
141 pub fn evse(&self, uid: &str) -> Option<&Evse> {
142 self.evses.iter().find(|e| e.uid.eq_ignore_case(uid))
143 }
144
145 #[must_use]
149 pub fn may_publish_to(&self, token: Option<&PublishTokenType>) -> bool {
150 if self.publish {
151 return true;
152 }
153 token.is_some_and(|t| self.publish_allowed_to.iter().any(|allowed| allowed.matches(t)))
154 }
155}
156
157impl Validate for Location {
158 fn validate_in(&self, v: &mut Validator) {
159 validate_fields!(
160 self,
161 v,
162 country_code,
163 party_id,
164 id,
165 publish_allowed_to,
166 name,
167 address,
168 city,
169 postal_code,
170 state,
171 country,
172 coordinates,
173 related_locations,
174 parking_type,
175 evses,
176 directions,
177 operator,
178 suboperator,
179 owner,
180 facilities,
181 time_zone,
182 opening_times,
183 images,
184 energy_mix,
185 last_updated,
186 );
187 if self.publish && !self.publish_allowed_to.is_empty() {
188 v.report_at(
189 "publish_allowed_to",
190 ViolationCode::Inconsistent,
191 "this field may only be used when `publish` is false",
192 );
193 }
194 }
195}
196
197#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Builder)]
201#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
202#[builder(on(_, into))]
203pub struct Evse {
204 pub uid: CiString<36>,
206 #[serde(default, skip_serializing_if = "Option::is_none")]
208 pub evse_id: Option<EvseId>,
209 pub status: Status,
211 #[serde(default, skip_serializing_if = "Vec::is_empty")]
213 #[builder(default)]
214 pub status_schedule: Vec<StatusSchedule>,
215 #[serde(default, skip_serializing_if = "Vec::is_empty")]
217 #[builder(default)]
218 pub capabilities: Vec<Capability>,
219 pub connectors: Vec<Connector>,
221 #[serde(default, skip_serializing_if = "Option::is_none")]
223 pub floor_level: Option<OcpiString<4>>,
224 #[serde(default, skip_serializing_if = "Option::is_none")]
226 pub coordinates: Option<GeoLocation>,
227 #[serde(default, skip_serializing_if = "Option::is_none")]
229 pub physical_reference: Option<OcpiString<16>>,
230 #[serde(default, skip_serializing_if = "Vec::is_empty")]
232 #[builder(default)]
233 pub directions: Vec<DisplayText>,
234 #[serde(default, skip_serializing_if = "Vec::is_empty")]
236 #[builder(default)]
237 pub parking_restrictions: Vec<ParkingRestriction>,
238 #[serde(default, skip_serializing_if = "Vec::is_empty")]
240 #[builder(default)]
241 pub images: Vec<Image>,
242 pub last_updated: DateTime,
244 #[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
246 #[builder(default)]
247 pub extensions: Extensions,
248}
249
250impl Evse {
251 #[must_use]
253 pub fn requires_connector_id_on_start(&self) -> bool {
254 self.capabilities.contains(&Capability::StartSessionConnectorRequired)
255 }
256
257 #[must_use]
259 pub fn connector(&self, id: &str) -> Option<&Connector> {
260 self.connectors.iter().find(|c| c.id.eq_ignore_case(id))
261 }
262}
263
264impl Validate for Evse {
265 fn validate_in(&self, v: &mut Validator) {
266 validate_fields!(
267 self,
268 v,
269 uid,
270 evse_id,
271 status_schedule,
272 capabilities,
273 connectors,
274 floor_level,
275 coordinates,
276 physical_reference,
277 directions,
278 parking_restrictions,
279 images,
280 last_updated,
281 );
282 if self.connectors.is_empty() {
283 v.report_at(
284 "connectors",
285 ViolationCode::EmptyRequiredList,
286 "an EVSE has cardinality `+` connectors: at least one is required",
287 );
288 }
289 }
290}
291
292#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Builder)]
296#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
297#[builder(on(_, into))]
298pub struct Connector {
299 pub id: CiString<36>,
301 pub standard: ConnectorType,
303 pub format: ConnectorFormat,
305 pub power_type: PowerType,
307 pub max_voltage: i32,
309 pub max_amperage: i32,
311 #[serde(default, skip_serializing_if = "Option::is_none")]
313 pub max_electric_power: Option<i32>,
314 #[serde(default, skip_serializing_if = "Vec::is_empty")]
316 #[builder(default)]
317 pub tariff_ids: Vec<CiString<36>>,
318 #[serde(default, skip_serializing_if = "Option::is_none")]
320 pub terms_and_conditions: Option<Url>,
321 pub last_updated: DateTime,
323 #[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
325 #[builder(default)]
326 pub extensions: Extensions,
327}
328
329impl Validate for Connector {
330 fn validate_in(&self, v: &mut Validator) {
331 validate_fields!(
332 self,
333 v,
334 id,
335 standard,
336 format,
337 power_type,
338 tariff_ids,
339 terms_and_conditions,
340 last_updated,
341 );
342 if self.max_voltage <= 0 {
343 v.report_at("max_voltage", ViolationCode::OutOfRange, "must be a positive voltage");
344 }
345 if self.max_amperage <= 0 {
346 v.report_at("max_amperage", ViolationCode::OutOfRange, "must be a positive amperage");
347 }
348 }
349}
350
351#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, Builder)]
358#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
359#[builder(on(_, into))]
360pub struct PublishTokenType {
361 #[serde(default, skip_serializing_if = "Option::is_none")]
363 pub uid: Option<CiString<36>>,
364 #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
366 pub token_type: Option<TokenType>,
367 #[serde(default, skip_serializing_if = "Option::is_none")]
369 pub visual_number: Option<OcpiString<64>>,
370 #[serde(default, skip_serializing_if = "Option::is_none")]
372 pub issuer: Option<OcpiString<64>>,
373 #[serde(default, skip_serializing_if = "Option::is_none")]
375 pub group_id: Option<CiString<36>>,
376 #[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
378 #[builder(default)]
379 pub extensions: Extensions,
380}
381
382impl PublishTokenType {
383 #[must_use]
385 pub fn matches(&self, candidate: &Self) -> bool {
386 fn agree<T: PartialEq>(required: Option<&T>, given: Option<&T>) -> bool {
387 required.is_none_or(|r| given == Some(r))
388 }
389 agree(self.uid.as_ref(), candidate.uid.as_ref())
390 && agree(self.token_type.as_ref(), candidate.token_type.as_ref())
391 && agree(self.visual_number.as_ref(), candidate.visual_number.as_ref())
392 && agree(self.issuer.as_ref(), candidate.issuer.as_ref())
393 && agree(self.group_id.as_ref(), candidate.group_id.as_ref())
394 }
395}
396
397impl Validate for PublishTokenType {
398 fn validate_in(&self, v: &mut Validator) {
399 validate_fields!(self, v, uid, token_type as "type", visual_number, issuer, group_id);
400 if self.uid.is_none() && self.visual_number.is_none() && self.group_id.is_none() {
401 v.report(
402 ViolationCode::MissingConditional,
403 "at least one of `uid`, `visual_number` or `group_id` SHALL be set",
404 );
405 }
406 if self.uid.is_some() && self.token_type.is_none() {
407 v.report_at("type", ViolationCode::MissingConditional, "SHALL be set when `uid` is set");
408 }
409 if self.visual_number.is_some() && self.issuer.is_none() {
410 v.report_at(
411 "issuer",
412 ViolationCode::MissingConditional,
413 "SHALL be set when `visual_number` is set",
414 );
415 }
416 }
417}
418
419ocpi_lenient_enum! {
420 pub enum ConnectorType {
428 Chademo = "CHADEMO",
430 ChaoJi = "CHAOJI",
432 DomesticA = "DOMESTIC_A",
434 DomesticB = "DOMESTIC_B",
436 DomesticC = "DOMESTIC_C",
438 DomesticD = "DOMESTIC_D",
440 DomesticE = "DOMESTIC_E",
442 DomesticF = "DOMESTIC_F",
444 DomesticG = "DOMESTIC_G",
446 DomesticH = "DOMESTIC_H",
448 DomesticI = "DOMESTIC_I",
450 DomesticJ = "DOMESTIC_J",
452 DomesticK = "DOMESTIC_K",
454 DomesticL = "DOMESTIC_L",
456 DomesticM = "DOMESTIC_M",
458 DomesticN = "DOMESTIC_N",
460 DomesticO = "DOMESTIC_O",
462 GbtAc = "GBT_AC",
464 GbtDc = "GBT_DC",
466 Iec603092Single16 = "IEC_60309_2_single_16",
468 Iec603092Three16 = "IEC_60309_2_three_16",
470 Iec603092Three32 = "IEC_60309_2_three_32",
472 Iec603092Three64 = "IEC_60309_2_three_64",
474 Iec62196T1 = "IEC_62196_T1",
476 Iec62196T1Combo = "IEC_62196_T1_COMBO",
478 Iec62196T2 = "IEC_62196_T2",
480 Iec62196T2Combo = "IEC_62196_T2_COMBO",
482 Iec62196T3A = "IEC_62196_T3A",
484 Iec62196T3C = "IEC_62196_T3C",
486 Nema520 = "NEMA_5_20",
488 Nema630 = "NEMA_6_30",
490 Nema650 = "NEMA_6_50",
492 Nema1030 = "NEMA_10_30",
494 Nema1050 = "NEMA_10_50",
496 Nema1430 = "NEMA_14_30",
498 Nema1450 = "NEMA_14_50",
500 PantographBottomUp = "PANTOGRAPH_BOTTOM_UP",
502 PantographTopDown = "PANTOGRAPH_TOP_DOWN",
504 TeslaR = "TESLA_R",
506 TeslaS = "TESLA_S",
508 }
509}
510
511ocpi_lenient_enum! {
512 pub enum ParkingRestriction {
518 EvOnly = "EV_ONLY",
520 Plugged = "PLUGGED",
522 Disabled = "DISABLED",
524 Customers = "CUSTOMERS",
526 Motorcycles = "MOTORCYCLES",
528 }
529}
530
531#[cfg(test)]
532mod tests {
533 use super::*;
534
535 #[test]
536 fn the_2_3_0_connector_types_are_absent_but_still_decode() {
537 assert!(ConnectorType::ALL_KNOWN_WIRE.iter().all(|v| *v != "MCS"));
538 let mcs: ConnectorType = "MCS".into();
539 assert!(!mcs.is_known(), "MCS arrived in OCPI 2.3.0");
540 assert_eq!(serde_json::to_string(&mcs).unwrap(), "\"MCS\"");
542 assert!(mcs.validate().is_err());
544 assert!(ConnectorType::Iec62196T2.validate().is_ok());
545 }
546
547 #[test]
548 fn the_2_3_0_parking_restrictions_are_absent() {
549 assert_eq!(ConnectorType::ALL_KNOWN.len(), 40);
550 assert_eq!(ParkingRestriction::ALL_KNOWN.len(), 5);
551 assert!(!ParkingRestriction::from("TENANTS").is_known());
552 }
553
554 #[test]
555 fn wire_identical_types_are_the_same_rust_type_in_both_versions() {
556 let geo: GeoLocation = crate::v2_3_0::locations::GeoLocation::new("52.010", "4.35000").unwrap();
558 let _: crate::v2_3_0::locations::GeoLocation = geo;
559 }
560}