1use bon::Builder;
6use serde::{Deserialize, Serialize};
7
8use crate::ocpi_lenient_enum;
9use crate::types::validate_fields;
10use crate::types::{
11 DateTime, DisplayText, Extensions, Number, OcpiString, Url, Validate, Validator, ViolationCode,
12};
13
14pub use crate::v2_3_0::locations::{
16 AdditionalGeoLocation, BusinessDetails, ConnectorFormat, EnergySource, EnergySourceCategory,
17 EnvironmentalImpactCategory, ExceptionalPeriod, GeoLocation, Image, ImageCategory, RegularHours, Status,
18 StatusSchedule,
19};
20
21#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
29#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
30pub struct EnvironmentalImpact {
31 pub source: EnvironmentalImpactCategory,
33 pub amount: Number,
35 #[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
37 pub extensions: Extensions,
38}
39
40impl Validate for EnvironmentalImpact {
41 fn validate_in(&self, v: &mut Validator) {
42 validate_fields!(self, v, source, amount);
43 }
44}
45
46#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Builder)]
53#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
54#[builder(on(_, into))]
55pub struct EnergyMix {
56 pub is_green_energy: bool,
58 #[serde(default, skip_serializing_if = "Vec::is_empty")]
60 #[builder(default)]
61 pub energy_sources: Vec<EnergySource>,
62 #[serde(default, skip_serializing_if = "Vec::is_empty")]
64 #[builder(default)]
65 pub environ_impact: Vec<EnvironmentalImpact>,
66 #[serde(default, skip_serializing_if = "Option::is_none")]
68 pub supplier_name: Option<OcpiString<64>>,
69 #[serde(default, skip_serializing_if = "Option::is_none")]
71 pub energy_product_name: Option<OcpiString<64>>,
72 #[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
74 #[builder(default)]
75 pub extensions: Extensions,
76}
77
78impl Validate for EnergyMix {
79 fn validate_in(&self, v: &mut Validator) {
80 validate_fields!(self, v, energy_sources, environ_impact, supplier_name, energy_product_name);
81 }
82}
83
84#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Builder)]
94#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
95#[builder(on(_, into))]
96pub struct Hours {
97 #[serde(default, skip_serializing_if = "Vec::is_empty")]
101 #[builder(default)]
102 pub regular_hours: Vec<RegularHours>,
103 #[serde(default, skip_serializing_if = "Option::is_none")]
105 pub twentyfourseven: Option<bool>,
106 #[serde(default, skip_serializing_if = "Vec::is_empty")]
108 #[builder(default)]
109 pub exceptional_openings: Vec<ExceptionalPeriod>,
110 #[serde(default, skip_serializing_if = "Vec::is_empty")]
112 #[builder(default)]
113 pub exceptional_closings: Vec<ExceptionalPeriod>,
114 #[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
116 #[builder(default)]
117 pub extensions: Extensions,
118}
119
120impl Hours {
121 #[must_use]
123 pub fn is_always_open(&self) -> bool {
124 self.twentyfourseven.unwrap_or(false)
125 }
126}
127
128impl Validate for Hours {
129 fn validate_in(&self, v: &mut Validator) {
130 validate_fields!(self, v, regular_hours, exceptional_openings, exceptional_closings);
131 match (self.regular_hours.is_empty(), self.twentyfourseven.is_some()) {
132 (true, false) => v.report(
133 ViolationCode::MissingConditional,
134 "Hours is a choice of one of two: either `regular_hours` or `twentyfourseven` \
135 must be given",
136 ),
137 (false, true) => v.report(
138 ViolationCode::Inconsistent,
139 "Hours is a choice of one of two: `regular_hours` and `twentyfourseven` are \
140 alternatives, not a combination",
141 ),
142 _ => {}
143 }
144 }
145}
146
147#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Builder)]
159#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
160#[builder(on(_, into))]
161pub struct Location {
162 pub id: OcpiString<39>,
164 #[serde(rename = "type")]
166 pub location_type: LocationType,
167 #[serde(default, skip_serializing_if = "Option::is_none")]
169 pub name: Option<OcpiString<255>>,
170 pub address: OcpiString<45>,
172 pub city: OcpiString<45>,
174 pub postal_code: OcpiString<10>,
176 pub country: OcpiString<3>,
178 pub coordinates: GeoLocation,
180 #[serde(default, skip_serializing_if = "Vec::is_empty")]
182 #[builder(default)]
183 pub related_locations: Vec<AdditionalGeoLocation>,
184 #[serde(default, skip_serializing_if = "Vec::is_empty")]
186 #[builder(default)]
187 pub evses: Vec<Evse>,
188 #[serde(default, skip_serializing_if = "Vec::is_empty")]
190 #[builder(default)]
191 pub directions: Vec<DisplayText>,
192 #[serde(default, skip_serializing_if = "Option::is_none")]
194 pub operator: Option<BusinessDetails>,
195 #[serde(default, skip_serializing_if = "Option::is_none")]
197 pub suboperator: Option<BusinessDetails>,
198 #[serde(default, skip_serializing_if = "Option::is_none")]
200 pub owner: Option<BusinessDetails>,
201 #[serde(default, skip_serializing_if = "Vec::is_empty")]
203 #[builder(default)]
204 pub facilities: Vec<Facility>,
205 #[serde(default, skip_serializing_if = "Option::is_none")]
207 pub time_zone: Option<OcpiString<255>>,
208 #[serde(default, skip_serializing_if = "Option::is_none")]
210 pub opening_times: Option<Hours>,
211 #[serde(default, skip_serializing_if = "Option::is_none")]
213 pub charging_when_closed: Option<bool>,
214 #[serde(default, skip_serializing_if = "Vec::is_empty")]
216 #[builder(default)]
217 pub images: Vec<Image>,
218 #[serde(default, skip_serializing_if = "Option::is_none")]
220 pub energy_mix: Option<EnergyMix>,
221 pub last_updated: DateTime,
223 #[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
225 #[builder(default)]
226 pub extensions: Extensions,
227}
228
229impl Location {
230 #[must_use]
232 pub fn charging_when_closed_or_default(&self) -> bool {
233 self.charging_when_closed.unwrap_or(true)
234 }
235
236 #[must_use]
241 pub fn evse(&self, uid: &str) -> Option<&Evse> {
242 self.evses.iter().find(|e| e.uid.as_str() == uid)
243 }
244}
245
246impl Validate for Location {
247 fn validate_in(&self, v: &mut Validator) {
248 validate_fields!(
249 self, v, id, location_type as "type", name, address, city, postal_code, country,
250 coordinates, related_locations, evses, directions, operator, suboperator, owner,
251 facilities, time_zone, opening_times, images, energy_mix, last_updated,
252 );
253 }
254}
255
256#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Builder)]
260#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
261#[builder(on(_, into))]
262pub struct Evse {
263 pub uid: OcpiString<39>,
265 #[serde(default, skip_serializing_if = "Option::is_none")]
267 pub evse_id: Option<OcpiString<48>>,
268 pub status: Status,
270 #[serde(default, skip_serializing_if = "Vec::is_empty")]
272 #[builder(default)]
273 pub status_schedule: Vec<StatusSchedule>,
274 #[serde(default, skip_serializing_if = "Vec::is_empty")]
276 #[builder(default)]
277 pub capabilities: Vec<Capability>,
278 pub connectors: Vec<Connector>,
280 #[serde(default, skip_serializing_if = "Option::is_none")]
282 pub floor_level: Option<OcpiString<4>>,
283 #[serde(default, skip_serializing_if = "Option::is_none")]
285 pub coordinates: Option<GeoLocation>,
286 #[serde(default, skip_serializing_if = "Option::is_none")]
288 pub physical_reference: Option<OcpiString<16>>,
289 #[serde(default, skip_serializing_if = "Vec::is_empty")]
291 #[builder(default)]
292 pub directions: Vec<DisplayText>,
293 #[serde(default, skip_serializing_if = "Vec::is_empty")]
295 #[builder(default)]
296 pub parking_restrictions: Vec<ParkingRestriction>,
297 #[serde(default, skip_serializing_if = "Vec::is_empty")]
299 #[builder(default)]
300 pub images: Vec<Image>,
301 pub last_updated: DateTime,
303 #[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
305 #[builder(default)]
306 pub extensions: Extensions,
307}
308
309impl Validate for Evse {
310 fn validate_in(&self, v: &mut Validator) {
311 validate_fields!(
312 self,
313 v,
314 uid,
315 evse_id,
316 status_schedule,
317 capabilities,
318 connectors,
319 floor_level,
320 coordinates,
321 physical_reference,
322 directions,
323 parking_restrictions,
324 images,
325 last_updated,
326 );
327 if self.connectors.is_empty() {
328 v.report_at(
329 "connectors",
330 ViolationCode::EmptyRequiredList,
331 "an EVSE has cardinality `+` connectors: at least one is required",
332 );
333 }
334 }
335}
336
337#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Builder)]
345#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
346#[builder(on(_, into))]
347pub struct Connector {
348 pub id: OcpiString<36>,
350 pub standard: ConnectorType,
352 pub format: ConnectorFormat,
354 pub power_type: PowerType,
356 pub voltage: i32,
358 pub amperage: i32,
360 #[serde(default, skip_serializing_if = "Option::is_none")]
365 pub tariff_id: Option<OcpiString<36>>,
366 #[serde(default, skip_serializing_if = "Option::is_none")]
368 pub terms_and_conditions: Option<Url>,
369 pub last_updated: DateTime,
371 #[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
373 #[builder(default)]
374 pub extensions: Extensions,
375}
376
377impl Validate for Connector {
378 fn validate_in(&self, v: &mut Validator) {
379 validate_fields!(
380 self,
381 v,
382 id,
383 standard,
384 format,
385 power_type,
386 tariff_id,
387 terms_and_conditions,
388 last_updated,
389 );
390 if self.voltage <= 0 {
391 v.report_at("voltage", ViolationCode::OutOfRange, "must be a positive voltage");
392 }
393 if self.amperage <= 0 {
394 v.report_at("amperage", ViolationCode::OutOfRange, "must be a positive amperage");
395 }
396 }
397}
398
399ocpi_lenient_enum! {
400 pub enum LocationType {
408 OnStreet = "ON_STREET",
410 ParkingGarage = "PARKING_GARAGE",
412 UndergroundGarage = "UNDERGROUND_GARAGE",
414 ParkingLot = "PARKING_LOT",
416 Other = "OTHER",
418 Unknown = "UNKNOWN",
420 }
421}
422
423ocpi_lenient_enum! {
424 pub enum Capability {
430 ChargingProfileCapable = "CHARGING_PROFILE_CAPABLE",
432 CreditCardPayable = "CREDIT_CARD_PAYABLE",
434 RemoteStartStopCapable = "REMOTE_START_STOP_CAPABLE",
436 Reservable = "RESERVABLE",
438 RfidReader = "RFID_READER",
440 UnlockCapable = "UNLOCK_CAPABLE",
442 }
443}
444
445ocpi_lenient_enum! {
446 pub enum ConnectorType {
455 Chademo = "CHADEMO",
457 DomesticA = "DOMESTIC_A",
459 DomesticB = "DOMESTIC_B",
461 DomesticC = "DOMESTIC_C",
463 DomesticD = "DOMESTIC_D",
465 DomesticE = "DOMESTIC_E",
467 DomesticF = "DOMESTIC_F",
469 DomesticG = "DOMESTIC_G",
471 DomesticH = "DOMESTIC_H",
473 DomesticI = "DOMESTIC_I",
475 DomesticJ = "DOMESTIC_J",
477 DomesticK = "DOMESTIC_K",
479 DomesticL = "DOMESTIC_L",
481 Iec603092Single16 = "IEC_60309_2_single_16",
483 Iec603092Three16 = "IEC_60309_2_three_16",
485 Iec603092Three32 = "IEC_60309_2_three_32",
487 Iec603092Three64 = "IEC_60309_2_three_64",
489 Iec62196T1 = "IEC_62196_T1",
491 Iec62196T1Combo = "IEC_62196_T1_COMBO",
493 Iec62196T2 = "IEC_62196_T2",
495 Iec62196T2Combo = "IEC_62196_T2_COMBO",
497 Iec62196T3A = "IEC_62196_T3A",
499 Iec62196T3C = "IEC_62196_T3C",
501 TeslaR = "TESLA_R",
503 TeslaS = "TESLA_S",
505 }
506}
507
508ocpi_lenient_enum! {
509 pub enum Facility {
513 Hotel = "HOTEL",
515 Restaurant = "RESTAURANT",
517 Cafe = "CAFE",
519 Mall = "MALL",
521 Supermarket = "SUPERMARKET",
523 Sport = "SPORT",
525 RecreationArea = "RECREATION_AREA",
527 Nature = "NATURE",
529 Museum = "MUSEUM",
531 BusStop = "BUS_STOP",
533 TaxiStand = "TAXI_STAND",
535 TrainStation = "TRAIN_STATION",
537 Airport = "AIRPORT",
539 CarpoolParking = "CARPOOL_PARKING",
541 FuelStation = "FUEL_STATION",
543 Wifi = "WIFI",
545 }
546}
547
548ocpi_lenient_enum! {
549 pub enum ParkingRestriction {
553 EvOnly = "EV_ONLY",
555 Plugged = "PLUGGED",
557 Disabled = "DISABLED",
559 Customers = "CUSTOMERS",
561 Motorcycles = "MOTORCYCLES",
563 }
564}
565
566ocpi_lenient_enum! {
567 pub enum PowerType {
573 Ac1Phase = "AC_1_PHASE",
575 Ac3Phase = "AC_3_PHASE",
577 Dc = "DC",
579 }
580}
581
582#[cfg(test)]
583mod tests {
584 use super::*;
585
586 #[test]
587 fn the_2_1_1_enums_are_much_smaller_than_the_later_ones() {
588 assert_eq!(ConnectorType::ALL_KNOWN.len(), 25);
589 let blue: ConnectorType = "IEC_60309_2_single_16".into();
591 assert!(blue.is_known(), "the blue 16 A industrial socket is a 2.1.1 value");
592 assert_eq!(serde_json::to_string(&blue).unwrap(), "\"IEC_60309_2_single_16\"");
593 assert_eq!(Capability::ALL_KNOWN.len(), 6);
594 assert_eq!(PowerType::ALL_KNOWN.len(), 3);
595 let mcs: ConnectorType = "MCS".into();
597 assert_eq!(serde_json::to_string(&mcs).unwrap(), "\"MCS\"");
598 assert!(mcs.validate().is_err());
600 }
601
602 #[test]
603 fn a_2_1_1_location_has_no_owner_fields() {
604 let json = r#"{"id":"LOC1","type":"ON_STREET","address":"F.Rooseveltlaan 3A","city":"Gent","postal_code":"9000","country":"BEL","coordinates":{"latitude":"51.047599","longitude":"3.729944"},"last_updated":"2015-06-29T20:39:09Z"}"#;
605 let location: Location = serde_json::from_str(json).unwrap();
606 assert_eq!(location.location_type, LocationType::OnStreet);
607 assert!(location.validate().is_ok());
608 assert_eq!(serde_json::to_string(&location).unwrap(), json);
609 }
610
611 #[test]
612 fn hours_is_a_choice_of_one_of_two_in_2_1_1() {
613 let weekdays: Hours = serde_json::from_str(
616 r#"{"regular_hours":[{"weekday":1,"period_begin":"08:00","period_end":"20:00"}]}"#,
617 )
618 .unwrap();
619 assert!(weekdays.validate().is_ok());
620 assert!(!weekdays.is_always_open());
621
622 let always: Hours = serde_json::from_str(r#"{"twentyfourseven":true}"#).unwrap();
623 assert!(always.validate().is_ok());
624 assert!(always.is_always_open());
625
626 assert!(serde_json::from_str::<Hours>("{}").unwrap().validate().is_err());
628 let both: Hours = serde_json::from_str(
629 r#"{"twentyfourseven":true,"regular_hours":[{"weekday":1,"period_begin":"08:00","period_end":"20:00"}]}"#,
630 )
631 .unwrap();
632 assert!(both.validate().is_err());
633 }
634
635 #[test]
636 fn the_environmental_impact_field_is_named_source_in_2_1_1() {
637 let json = r#"{"source":"CARBON_DIOXIDE","amount":230}"#;
639 let impact: EnvironmentalImpact = serde_json::from_str(json).unwrap();
640 assert_eq!(impact.source, EnvironmentalImpactCategory::CarbonDioxide);
641 assert_eq!(serde_json::to_string(&impact).unwrap(), json);
642 assert!(impact.extensions.is_empty(), "nothing fell through into extensions");
643 }
644
645 #[test]
646 fn evse_uids_compare_case_sensitively_because_2_1_1_says_string() {
647 let location: Location = serde_json::from_str(
648 r#"{"id":"LOC1","type":"ON_STREET","address":"a","city":"b","postal_code":"c","country":"NLD","coordinates":{"latitude":"51.047599","longitude":"3.729944"},"evses":[{"uid":"AB123","status":"AVAILABLE","connectors":[{"id":"1","standard":"IEC_62196_T2","format":"SOCKET","power_type":"AC_3_PHASE","voltage":400,"amperage":32,"last_updated":"2015-06-29T20:39:09Z"}],"last_updated":"2015-06-29T20:39:09Z"}],"last_updated":"2015-06-29T20:39:09Z"}"#,
649 )
650 .unwrap();
651 assert!(location.evse("AB123").is_some());
652 assert!(location.evse("ab123").is_none(), "2.1.1 types EVSE.uid as string, not CiString");
653 }
654}