Skip to main content

ocpi_kit/v2_1_1/
locations.rs

1//! The *Locations* module of OCPI 2.1.1.
2//!
3//! Spec: 2.1.1 §mod_locations
4
5use 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
14// Wire-identical to OCPI 2.3.0.
15pub 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/// Waste produced or emitted per kWh, in OCPI 2.1.1.
22///
23/// **The field is named `source` here.** OCPI 2.2 renamed it to `category`, which is what
24/// [`v2_3_0::locations::EnvironmentalImpact`](crate::v2_3_0::locations::EnvironmentalImpact) uses.
25/// Reusing the later type would silently drop a 2.1.1 peer's value into `extensions`.
26///
27/// Spec: 2.1.1 §mod_locations_environmentalimpact_class
28#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
29#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
30pub struct EnvironmentalImpact {
31    /// The category of this value.
32    pub source: EnvironmentalImpactCategory,
33    /// Amount of this portion in g/kWh.
34    pub amount: Number,
35    /// Undocumented JSON fields, preserved verbatim.
36    #[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/// The energy mix and environmental impact of the energy supplied, in OCPI 2.1.1.
47///
48/// Field-for-field the same as later versions; it is redefined only because its
49/// `environ_impact` holds the 2.1.1 [`EnvironmentalImpact`], whose field is `source`.
50///
51/// Spec: 2.1.1 §mod_locations_energymix_class
52#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Builder)]
53#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
54#[builder(on(_, into))]
55pub struct EnergyMix {
56    /// True if 100% from regenerative sources.
57    pub is_green_energy: bool,
58    /// Energy sources of this location's tariff.
59    #[serde(default, skip_serializing_if = "Vec::is_empty")]
60    #[builder(default)]
61    pub energy_sources: Vec<EnergySource>,
62    /// Nuclear waste and CO2 exhaust of this location's tariff.
63    #[serde(default, skip_serializing_if = "Vec::is_empty")]
64    #[builder(default)]
65    pub environ_impact: Vec<EnvironmentalImpact>,
66    /// Name of the energy supplier.
67    #[serde(default, skip_serializing_if = "Option::is_none")]
68    pub supplier_name: Option<OcpiString<64>>,
69    /// Name of the energy supplier's product or tariff plan.
70    #[serde(default, skip_serializing_if = "Option::is_none")]
71    pub energy_product_name: Option<OcpiString<64>>,
72    /// Undocumented JSON fields, preserved verbatim.
73    #[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/// Opening and access hours, in OCPI 2.1.1.
85///
86/// > *Choice: one of two — `regular_hours` … `twentyfourseven`*
87///
88/// In OCPI 2.1.1 the two are **alternatives**, and a peer that publishes weekday hours sends no
89/// `twentyfourseven` at all. OCPI 2.2 made `twentyfourseven` required, so reusing the later type
90/// here would fail to decode a perfectly ordinary 2.1.1 Location.
91///
92/// Spec: 2.1.1 §mod_locations_hours_class
93#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Builder)]
94#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
95#[builder(on(_, into))]
96pub struct Hours {
97    /// Regular weekday-based hours.
98    ///
99    /// > *Should not be set for representing 24/7 as this is the most common case.*
100    #[serde(default, skip_serializing_if = "Vec::is_empty")]
101    #[builder(default)]
102    pub regular_hours: Vec<RegularHours>,
103    /// True to represent 24 hours a day and 7 days a week, except the given exceptions.
104    #[serde(default, skip_serializing_if = "Option::is_none")]
105    pub twentyfourseven: Option<bool>,
106    /// Periods the station is operating or accessible, additional to `regular_hours`.
107    #[serde(default, skip_serializing_if = "Vec::is_empty")]
108    #[builder(default)]
109    pub exceptional_openings: Vec<ExceptionalPeriod>,
110    /// Periods the station is not operating or accessible, overriding everything else.
111    #[serde(default, skip_serializing_if = "Vec::is_empty")]
112    #[builder(default)]
113    pub exceptional_closings: Vec<ExceptionalPeriod>,
114    /// Undocumented JSON fields, preserved verbatim.
115    #[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
116    #[builder(default)]
117    pub extensions: Extensions,
118}
119
120impl Hours {
121    /// Whether the location is open around the clock, applying the 2.1.1 choice.
122    #[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/// Where a group of EVSEs is installed, in OCPI 2.1.1.
148///
149/// Compared with later versions this object has **no owner fields**: `country_code` and
150/// `party_id` came in with OCPI 2.2. In 2.1.1 the owner is known only from the URL a
151/// client-owned object is pushed to, and from the credentials handshake.
152///
153/// It also has a required [`LocationType`], which 2.2 replaced with the optional
154/// [`ParkingType`](crate::v2_3_0::locations::ParkingType), and its `id` is a `string(39)` rather
155/// than a `CiString(36)`.
156///
157/// Spec: 2.1.1 §mod_locations_location_object
158#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Builder)]
159#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
160#[builder(on(_, into))]
161pub struct Location {
162    /// Uniquely identifies the location within the CPO's platform.
163    pub id: OcpiString<39>,
164    /// The general type of the charge point location.
165    #[serde(rename = "type")]
166    pub location_type: LocationType,
167    /// Display name of the location.
168    #[serde(default, skip_serializing_if = "Option::is_none")]
169    pub name: Option<OcpiString<255>>,
170    /// Street/block name and house number if available.
171    pub address: OcpiString<45>,
172    /// City or town.
173    pub city: OcpiString<45>,
174    /// Postal code of the location. **Required** in 2.1.1; optional from 2.2 onwards.
175    pub postal_code: OcpiString<10>,
176    /// ISO 3166-1 alpha-3 code for the country of this location.
177    pub country: OcpiString<3>,
178    /// Coordinates of the location.
179    pub coordinates: GeoLocation,
180    /// Geographical locations of related points relevant to the user.
181    #[serde(default, skip_serializing_if = "Vec::is_empty")]
182    #[builder(default)]
183    pub related_locations: Vec<AdditionalGeoLocation>,
184    /// The EVSEs that belong to this Location.
185    #[serde(default, skip_serializing_if = "Vec::is_empty")]
186    #[builder(default)]
187    pub evses: Vec<Evse>,
188    /// Human-readable directions on how to reach the location.
189    #[serde(default, skip_serializing_if = "Vec::is_empty")]
190    #[builder(default)]
191    pub directions: Vec<DisplayText>,
192    /// Information of the operator.
193    #[serde(default, skip_serializing_if = "Option::is_none")]
194    pub operator: Option<BusinessDetails>,
195    /// Information of the suboperator if available.
196    #[serde(default, skip_serializing_if = "Option::is_none")]
197    pub suboperator: Option<BusinessDetails>,
198    /// Information of the owner if available.
199    #[serde(default, skip_serializing_if = "Option::is_none")]
200    pub owner: Option<BusinessDetails>,
201    /// Facilities this charging location directly belongs to.
202    #[serde(default, skip_serializing_if = "Vec::is_empty")]
203    #[builder(default)]
204    pub facilities: Vec<Facility>,
205    /// One of IANA tzdata's TZ values. **Optional** in 2.1.1; required from 2.2 onwards.
206    #[serde(default, skip_serializing_if = "Option::is_none")]
207    pub time_zone: Option<OcpiString<255>>,
208    /// When the EVSEs at the location can be accessed for charging.
209    #[serde(default, skip_serializing_if = "Option::is_none")]
210    pub opening_times: Option<Hours>,
211    /// Whether the EVSEs still charge outside the opening hours. Default: `true`.
212    #[serde(default, skip_serializing_if = "Option::is_none")]
213    pub charging_when_closed: Option<bool>,
214    /// Links to images related to the location.
215    #[serde(default, skip_serializing_if = "Vec::is_empty")]
216    #[builder(default)]
217    pub images: Vec<Image>,
218    /// Details on the energy supplied at this location.
219    #[serde(default, skip_serializing_if = "Option::is_none")]
220    pub energy_mix: Option<EnergyMix>,
221    /// Timestamp when this Location or one of its EVSEs or Connectors was last updated.
222    pub last_updated: DateTime,
223    /// Undocumented JSON fields, preserved verbatim.
224    #[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
225    #[builder(default)]
226    pub extensions: Extensions,
227}
228
229impl Location {
230    /// Whether the EVSEs keep charging outside opening hours, applying the spec's default.
231    #[must_use]
232    pub fn charging_when_closed_or_default(&self) -> bool {
233        self.charging_when_closed.unwrap_or(true)
234    }
235
236    /// Finds an EVSE by its `uid`.
237    ///
238    /// The comparison is **case-sensitive**: 2.1.1 types `EVSE.uid` as `string(39)`, not as a
239    /// `CiString`, and this crate follows the specification of each version exactly.
240    #[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/// The part that controls the power supply to a single EV, in OCPI 2.1.1.
257///
258/// Spec: 2.1.1 §mod_locations_evse_object
259#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Builder)]
260#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
261#[builder(on(_, into))]
262pub struct Evse {
263    /// Uniquely identifies the EVSE within the CPO's platform.
264    pub uid: OcpiString<39>,
265    /// The human-readable EVSE ID in the eMI3 format.
266    #[serde(default, skip_serializing_if = "Option::is_none")]
267    pub evse_id: Option<OcpiString<48>>,
268    /// The current status of the EVSE.
269    pub status: Status,
270    /// Planned status updates of the EVSE.
271    #[serde(default, skip_serializing_if = "Vec::is_empty")]
272    #[builder(default)]
273    pub status_schedule: Vec<StatusSchedule>,
274    /// Functionalities that the EVSE is capable of.
275    #[serde(default, skip_serializing_if = "Vec::is_empty")]
276    #[builder(default)]
277    pub capabilities: Vec<Capability>,
278    /// Available connectors on the EVSE. Cardinality `+`.
279    pub connectors: Vec<Connector>,
280    /// Level on which the charging station is located.
281    #[serde(default, skip_serializing_if = "Option::is_none")]
282    pub floor_level: Option<OcpiString<4>>,
283    /// Coordinates of the EVSE.
284    #[serde(default, skip_serializing_if = "Option::is_none")]
285    pub coordinates: Option<GeoLocation>,
286    /// A number/string printed on the outside of the EVSE for visual identification.
287    #[serde(default, skip_serializing_if = "Option::is_none")]
288    pub physical_reference: Option<OcpiString<16>>,
289    /// Directions on how to reach the EVSE from the Location.
290    #[serde(default, skip_serializing_if = "Vec::is_empty")]
291    #[builder(default)]
292    pub directions: Vec<DisplayText>,
293    /// The restrictions that apply to the parking spot.
294    #[serde(default, skip_serializing_if = "Vec::is_empty")]
295    #[builder(default)]
296    pub parking_restrictions: Vec<ParkingRestriction>,
297    /// Links to images related to the EVSE.
298    #[serde(default, skip_serializing_if = "Vec::is_empty")]
299    #[builder(default)]
300    pub images: Vec<Image>,
301    /// Timestamp when this EVSE or one of its Connectors was last updated.
302    pub last_updated: DateTime,
303    /// Undocumented JSON fields, preserved verbatim.
304    #[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/// The socket, or cable and plug, available for the EV to use, in OCPI 2.1.1.
338///
339/// The electrical fields are named `voltage` and `amperage` here; OCPI 2.2 renamed them to
340/// `max_voltage` and `max_amperage` and added `max_electric_power`. `tariff_id` is a single
341/// optional value; 2.2 made it the list `tariff_ids`.
342///
343/// Spec: 2.1.1 §mod_locations_connector_object
344#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Builder)]
345#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
346#[builder(on(_, into))]
347pub struct Connector {
348    /// Identifier of the connector within the EVSE.
349    pub id: OcpiString<36>,
350    /// The standard of the installed connector.
351    pub standard: ConnectorType,
352    /// The format (socket/cable) of the installed connector.
353    pub format: ConnectorFormat,
354    /// Whether the connector supplies AC or DC, and on how many phases.
355    pub power_type: PowerType,
356    /// Voltage of the connector (line to neutral for `AC_3_PHASE`), in volt.
357    pub voltage: i32,
358    /// Maximum amperage of the connector, in ampere.
359    pub amperage: i32,
360    /// Identifier of the current charging tariff structure.
361    ///
362    /// > *For a "Free of Charge" tariff this field should be set, and point to a defined "Free of
363    /// > Charge" tariff.*
364    #[serde(default, skip_serializing_if = "Option::is_none")]
365    pub tariff_id: Option<OcpiString<36>>,
366    /// URL to the operator's terms and conditions.
367    #[serde(default, skip_serializing_if = "Option::is_none")]
368    pub terms_and_conditions: Option<Url>,
369    /// Timestamp when this Connector was last updated.
370    pub last_updated: DateTime,
371    /// Undocumented JSON fields, preserved verbatim.
372    #[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    /// The general type of the charge point location.
401    ///
402    /// Removed in OCPI 2.2, which replaced it with the optional
403    /// [`ParkingType`](crate::v2_3_0::locations::ParkingType) and dropped the `OTHER`/`UNKNOWN`
404    /// escape hatches.
405    ///
406    /// Spec: 2.1.1 §mod_locations_locationtype_enum
407    pub enum LocationType {
408        /// Parking in public space.
409        OnStreet = "ON_STREET",
410        /// Multistorey car park.
411        ParkingGarage = "PARKING_GARAGE",
412        /// Multistorey car park, mainly underground.
413        UndergroundGarage = "UNDERGROUND_GARAGE",
414        /// A cleared area intended for parking vehicles.
415        ParkingLot = "PARKING_LOT",
416        /// None of the given possibilities.
417        Other = "OTHER",
418        /// Not known by the operator. The default.
419        Unknown = "UNKNOWN",
420    }
421}
422
423ocpi_lenient_enum! {
424    /// The capabilities of an EVSE, in OCPI 2.1.1.
425    ///
426    /// Six values; OCPI 2.2 grew this to thirteen.
427    ///
428    /// Spec: 2.1.1 §mod_locations_capability_enum
429    pub enum Capability {
430        /// The EVSE supports charging profiles.
431        ChargingProfileCapable = "CHARGING_PROFILE_CAPABLE",
432        /// Payment of a charging session can be done using a credit card.
433        CreditCardPayable = "CREDIT_CARD_PAYABLE",
434        /// The EVSE can remotely be started/stopped.
435        RemoteStartStopCapable = "REMOTE_START_STOP_CAPABLE",
436        /// The EVSE can be reserved.
437        Reservable = "RESERVABLE",
438        /// Charging at this EVSE can be authorized with an RFID token.
439        RfidReader = "RFID_READER",
440        /// Connectors have a mechanical lock that can be requested to be unlocked.
441        UnlockCapable = "UNLOCK_CAPABLE",
442    }
443}
444
445ocpi_lenient_enum! {
446    /// The socket or plug standard of the charging point, in OCPI 2.1.1.
447    ///
448    /// Twenty values. Everything OCPI 2.2 and 2.3.0 added — the GB/T, IEC 60309, NEMA,
449    /// pantograph, ChaoJi, MCS and SAE J3400 families — is absent, which is precisely why
450    /// [`ocpi_lenient_enum!`] is used here: a 2.1.1 peer that has installed a CCS-adjacent plug
451    /// invented in the last decade will send a value this list does not have.
452    ///
453    /// Spec: 2.1.1 §mod_locations_connectortype_enum
454    pub enum ConnectorType {
455        /// CHAdeMO, DC.
456        Chademo = "CHADEMO",
457        /// Standard/Domestic household, type "A", NEMA 1-15, 2 pins.
458        DomesticA = "DOMESTIC_A",
459        /// Standard/Domestic household, type "B", NEMA 5-15, 3 pins.
460        DomesticB = "DOMESTIC_B",
461        /// Standard/Domestic household, type "C", CEE 7/17, 2 pins.
462        DomesticC = "DOMESTIC_C",
463        /// Standard/Domestic household, type "D", 3 pin.
464        DomesticD = "DOMESTIC_D",
465        /// Standard/Domestic household, type "E", CEE 7/5, 3 pins.
466        DomesticE = "DOMESTIC_E",
467        /// Standard/Domestic household, type "F", CEE 7/4, Schuko, 3 pins.
468        DomesticF = "DOMESTIC_F",
469        /// Standard/Domestic household, type "G", BS 1363, Commonwealth, 3 pins.
470        DomesticG = "DOMESTIC_G",
471        /// Standard/Domestic household, type "H", SI-32, 3 pins.
472        DomesticH = "DOMESTIC_H",
473        /// Standard/Domestic household, type "I", AS 3112, 3 pins.
474        DomesticI = "DOMESTIC_I",
475        /// Standard/Domestic household, type "J", SEV 1011, 3 pins.
476        DomesticJ = "DOMESTIC_J",
477        /// Standard/Domestic household, type "K", DS 60884-2-D1, 3 pins.
478        DomesticK = "DOMESTIC_K",
479        /// Standard/Domestic household, type "L", CEI 23-16-VII, 3 pins.
480        DomesticL = "DOMESTIC_L",
481        /// IEC 60309-2 Industrial Connector, single phase 16 A (usually blue).
482        Iec603092Single16 = "IEC_60309_2_single_16",
483        /// IEC 60309-2 Industrial Connector, three phases 16 A (usually red).
484        Iec603092Three16 = "IEC_60309_2_three_16",
485        /// IEC 60309-2 Industrial Connector, three phases 32 A (usually red).
486        Iec603092Three32 = "IEC_60309_2_three_32",
487        /// IEC 60309-2 Industrial Connector, three phases 64 A (usually red).
488        Iec603092Three64 = "IEC_60309_2_three_64",
489        /// IEC 62196 Type 1 "SAE J1772".
490        Iec62196T1 = "IEC_62196_T1",
491        /// Combo Type 1 based, DC.
492        Iec62196T1Combo = "IEC_62196_T1_COMBO",
493        /// IEC 62196 Type 2 "Mennekes".
494        Iec62196T2 = "IEC_62196_T2",
495        /// Combo Type 2 based, DC.
496        Iec62196T2Combo = "IEC_62196_T2_COMBO",
497        /// IEC 62196 Type 3A.
498        Iec62196T3A = "IEC_62196_T3A",
499        /// IEC 62196 Type 3C "Scame".
500        Iec62196T3C = "IEC_62196_T3C",
501        /// Tesla Connector "Roadster"-type (round, 4 pin).
502        TeslaR = "TESLA_R",
503        /// Tesla Connector "Model-S"-type (oval, 5 pin).
504        TeslaS = "TESLA_S",
505    }
506}
507
508ocpi_lenient_enum! {
509    /// Facilities a charging location directly belongs to, in OCPI 2.1.1.
510    ///
511    /// Spec: 2.1.1 §mod_locations_facility_enum
512    pub enum Facility {
513        /// A hotel.
514        Hotel = "HOTEL",
515        /// A restaurant.
516        Restaurant = "RESTAURANT",
517        /// A cafe.
518        Cafe = "CAFE",
519        /// A mall or shopping center.
520        Mall = "MALL",
521        /// A supermarket.
522        Supermarket = "SUPERMARKET",
523        /// Sport facilities.
524        Sport = "SPORT",
525        /// A recreation area.
526        RecreationArea = "RECREATION_AREA",
527        /// Located in, or close to, a park or nature reserve.
528        Nature = "NATURE",
529        /// A museum.
530        Museum = "MUSEUM",
531        /// A bus stop.
532        BusStop = "BUS_STOP",
533        /// A taxi stand.
534        TaxiStand = "TAXI_STAND",
535        /// A train station.
536        TrainStation = "TRAIN_STATION",
537        /// An airport.
538        Airport = "AIRPORT",
539        /// A carpool parking.
540        CarpoolParking = "CARPOOL_PARKING",
541        /// A fuel station.
542        FuelStation = "FUEL_STATION",
543        /// Wifi or other type of internet available.
544        Wifi = "WIFI",
545    }
546}
547
548ocpi_lenient_enum! {
549    /// Restrictions on the parking spot, in OCPI 2.1.1.
550    ///
551    /// Spec: 2.1.1 §mod_locations_parkingrestriction_enum
552    pub enum ParkingRestriction {
553        /// Reserved parking spot for electric vehicles.
554        EvOnly = "EV_ONLY",
555        /// Parking is only allowed while plugged in (charging).
556        Plugged = "PLUGGED",
557        /// Reserved parking spot for disabled people with a valid ID.
558        Disabled = "DISABLED",
559        /// Parking spot for customers or guests only.
560        Customers = "CUSTOMERS",
561        /// Parking spot only suitable for (electric) motorcycles or scooters.
562        Motorcycles = "MOTORCYCLES",
563    }
564}
565
566ocpi_lenient_enum! {
567    /// Whether a connector supplies AC or DC, in OCPI 2.1.1.
568    ///
569    /// The two-phase variants arrived in OCPI 2.2.
570    ///
571    /// Spec: 2.1.1 §mod_locations_powertype_enum
572    pub enum PowerType {
573        /// AC single phase.
574        Ac1Phase = "AC_1_PHASE",
575        /// AC three phases.
576        Ac3Phase = "AC_3_PHASE",
577        /// Direct current.
578        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        // The four IEC 60309 industrial sockets are the only lower-case wire values in OCPI.
590        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        // A connector standard invented after 2.1.1 still decodes …
596        let mcs: ConnectorType = "MCS".into();
597        assert_eq!(serde_json::to_string(&mcs).unwrap(), "\"MCS\"");
598        // … and is reported, because 2.1.1 declares the enum closed.
599        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        // A 2.1.1 peer publishing weekday hours sends no `twentyfourseven` at all, which the
614        // OCPI 2.2 shape would refuse to decode.
615        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        // Neither, or both, is a violation: they are alternatives.
627        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        // OCPI 2.2 renamed it to `category`; reusing the later type would drop the value.
638        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}