ocpi-kit 0.2.0

OCPI (Open Charge Point Interface) toolkit for EV roaming: spec-exact typed models for OCPI 2.3.0 / 2.2.1 / 2.1.1, transport envelope, client, server and hub building blocks, and an auditable tariff engine.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
//! The *Locations* module of OCPI 2.1.1.
//!
//! Spec: 2.1.1 §mod_locations

use bon::Builder;
use serde::{Deserialize, Serialize};

use crate::ocpi_lenient_enum;
use crate::types::validate_fields;
use crate::types::{
    DateTime, DisplayText, Extensions, Number, OcpiString, Url, Validate, Validator, ViolationCode,
};

// Wire-identical to OCPI 2.3.0.
pub use crate::v2_3_0::locations::{
    AdditionalGeoLocation, BusinessDetails, ConnectorFormat, EnergySource, EnergySourceCategory,
    EnvironmentalImpactCategory, ExceptionalPeriod, GeoLocation, Image, ImageCategory, RegularHours, Status,
    StatusSchedule,
};

/// Waste produced or emitted per kWh, in OCPI 2.1.1.
///
/// **The field is named `source` here.** OCPI 2.2 renamed it to `category`, which is what
/// [`v2_3_0::locations::EnvironmentalImpact`](crate::v2_3_0::locations::EnvironmentalImpact) uses.
/// Reusing the later type would silently drop a 2.1.1 peer's value into `extensions`.
///
/// Spec: 2.1.1 §mod_locations_environmentalimpact_class
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct EnvironmentalImpact {
    /// The category of this value.
    pub source: EnvironmentalImpactCategory,
    /// Amount of this portion in g/kWh.
    pub amount: Number,
    /// Undocumented JSON fields, preserved verbatim.
    #[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
    pub extensions: Extensions,
}

impl Validate for EnvironmentalImpact {
    fn validate_in(&self, v: &mut Validator) {
        validate_fields!(self, v, source, amount);
    }
}

/// The energy mix and environmental impact of the energy supplied, in OCPI 2.1.1.
///
/// Field-for-field the same as later versions; it is redefined only because its
/// `environ_impact` holds the 2.1.1 [`EnvironmentalImpact`], whose field is `source`.
///
/// Spec: 2.1.1 §mod_locations_energymix_class
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Builder)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[builder(on(_, into))]
pub struct EnergyMix {
    /// True if 100% from regenerative sources.
    pub is_green_energy: bool,
    /// Energy sources of this location's tariff.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    #[builder(default)]
    pub energy_sources: Vec<EnergySource>,
    /// Nuclear waste and CO2 exhaust of this location's tariff.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    #[builder(default)]
    pub environ_impact: Vec<EnvironmentalImpact>,
    /// Name of the energy supplier.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub supplier_name: Option<OcpiString<64>>,
    /// Name of the energy supplier's product or tariff plan.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub energy_product_name: Option<OcpiString<64>>,
    /// Undocumented JSON fields, preserved verbatim.
    #[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
    #[builder(default)]
    pub extensions: Extensions,
}

impl Validate for EnergyMix {
    fn validate_in(&self, v: &mut Validator) {
        validate_fields!(self, v, energy_sources, environ_impact, supplier_name, energy_product_name);
    }
}

/// Opening and access hours, in OCPI 2.1.1.
///
/// > *Choice: one of two — `regular_hours` … `twentyfourseven`*
///
/// In OCPI 2.1.1 the two are **alternatives**, and a peer that publishes weekday hours sends no
/// `twentyfourseven` at all. OCPI 2.2 made `twentyfourseven` required, so reusing the later type
/// here would fail to decode a perfectly ordinary 2.1.1 Location.
///
/// Spec: 2.1.1 §mod_locations_hours_class
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Builder)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[builder(on(_, into))]
pub struct Hours {
    /// Regular weekday-based hours.
    ///
    /// > *Should not be set for representing 24/7 as this is the most common case.*
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    #[builder(default)]
    pub regular_hours: Vec<RegularHours>,
    /// True to represent 24 hours a day and 7 days a week, except the given exceptions.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub twentyfourseven: Option<bool>,
    /// Periods the station is operating or accessible, additional to `regular_hours`.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    #[builder(default)]
    pub exceptional_openings: Vec<ExceptionalPeriod>,
    /// Periods the station is not operating or accessible, overriding everything else.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    #[builder(default)]
    pub exceptional_closings: Vec<ExceptionalPeriod>,
    /// Undocumented JSON fields, preserved verbatim.
    #[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
    #[builder(default)]
    pub extensions: Extensions,
}

impl Hours {
    /// Whether the location is open around the clock, applying the 2.1.1 choice.
    #[must_use]
    pub fn is_always_open(&self) -> bool {
        self.twentyfourseven.unwrap_or(false)
    }
}

impl Validate for Hours {
    fn validate_in(&self, v: &mut Validator) {
        validate_fields!(self, v, regular_hours, exceptional_openings, exceptional_closings);
        match (self.regular_hours.is_empty(), self.twentyfourseven.is_some()) {
            (true, false) => v.report(
                ViolationCode::MissingConditional,
                "Hours is a choice of one of two: either `regular_hours` or `twentyfourseven` \
                 must be given",
            ),
            (false, true) => v.report(
                ViolationCode::Inconsistent,
                "Hours is a choice of one of two: `regular_hours` and `twentyfourseven` are \
                 alternatives, not a combination",
            ),
            _ => {}
        }
    }
}

/// Where a group of EVSEs is installed, in OCPI 2.1.1.
///
/// Compared with later versions this object has **no owner fields**: `country_code` and
/// `party_id` came in with OCPI 2.2. In 2.1.1 the owner is known only from the URL a
/// client-owned object is pushed to, and from the credentials handshake.
///
/// It also has a required [`LocationType`], which 2.2 replaced with the optional
/// [`ParkingType`](crate::v2_3_0::locations::ParkingType), and its `id` is a `string(39)` rather
/// than a `CiString(36)`.
///
/// Spec: 2.1.1 §mod_locations_location_object
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Builder)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[builder(on(_, into))]
pub struct Location {
    /// Uniquely identifies the location within the CPO's platform.
    pub id: OcpiString<39>,
    /// The general type of the charge point location.
    #[serde(rename = "type")]
    pub location_type: LocationType,
    /// Display name of the location.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub name: Option<OcpiString<255>>,
    /// Street/block name and house number if available.
    pub address: OcpiString<45>,
    /// City or town.
    pub city: OcpiString<45>,
    /// Postal code of the location. **Required** in 2.1.1; optional from 2.2 onwards.
    pub postal_code: OcpiString<10>,
    /// ISO 3166-1 alpha-3 code for the country of this location.
    pub country: OcpiString<3>,
    /// Coordinates of the location.
    pub coordinates: GeoLocation,
    /// Geographical locations of related points relevant to the user.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    #[builder(default)]
    pub related_locations: Vec<AdditionalGeoLocation>,
    /// The EVSEs that belong to this Location.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    #[builder(default)]
    pub evses: Vec<Evse>,
    /// Human-readable directions on how to reach the location.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    #[builder(default)]
    pub directions: Vec<DisplayText>,
    /// Information of the operator.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub operator: Option<BusinessDetails>,
    /// Information of the suboperator if available.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub suboperator: Option<BusinessDetails>,
    /// Information of the owner if available.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub owner: Option<BusinessDetails>,
    /// Facilities this charging location directly belongs to.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    #[builder(default)]
    pub facilities: Vec<Facility>,
    /// One of IANA tzdata's TZ values. **Optional** in 2.1.1; required from 2.2 onwards.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub time_zone: Option<OcpiString<255>>,
    /// When the EVSEs at the location can be accessed for charging.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub opening_times: Option<Hours>,
    /// Whether the EVSEs still charge outside the opening hours. Default: `true`.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub charging_when_closed: Option<bool>,
    /// Links to images related to the location.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    #[builder(default)]
    pub images: Vec<Image>,
    /// Details on the energy supplied at this location.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub energy_mix: Option<EnergyMix>,
    /// Timestamp when this Location or one of its EVSEs or Connectors was last updated.
    pub last_updated: DateTime,
    /// Undocumented JSON fields, preserved verbatim.
    #[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
    #[builder(default)]
    pub extensions: Extensions,
}

impl Location {
    /// Whether the EVSEs keep charging outside opening hours, applying the spec's default.
    #[must_use]
    pub fn charging_when_closed_or_default(&self) -> bool {
        self.charging_when_closed.unwrap_or(true)
    }

    /// Finds an EVSE by its `uid`.
    ///
    /// The comparison is **case-sensitive**: 2.1.1 types `EVSE.uid` as `string(39)`, not as a
    /// `CiString`, and this crate follows the specification of each version exactly.
    #[must_use]
    pub fn evse(&self, uid: &str) -> Option<&Evse> {
        self.evses.iter().find(|e| e.uid.as_str() == uid)
    }
}

impl Validate for Location {
    fn validate_in(&self, v: &mut Validator) {
        validate_fields!(
            self, v, id, location_type as "type", name, address, city, postal_code, country,
            coordinates, related_locations, evses, directions, operator, suboperator, owner,
            facilities, time_zone, opening_times, images, energy_mix, last_updated,
        );
    }
}

/// The part that controls the power supply to a single EV, in OCPI 2.1.1.
///
/// Spec: 2.1.1 §mod_locations_evse_object
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Builder)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[builder(on(_, into))]
pub struct Evse {
    /// Uniquely identifies the EVSE within the CPO's platform.
    pub uid: OcpiString<39>,
    /// The human-readable EVSE ID in the eMI3 format.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub evse_id: Option<OcpiString<48>>,
    /// The current status of the EVSE.
    pub status: Status,
    /// Planned status updates of the EVSE.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    #[builder(default)]
    pub status_schedule: Vec<StatusSchedule>,
    /// Functionalities that the EVSE is capable of.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    #[builder(default)]
    pub capabilities: Vec<Capability>,
    /// Available connectors on the EVSE. Cardinality `+`.
    pub connectors: Vec<Connector>,
    /// Level on which the charging station is located.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub floor_level: Option<OcpiString<4>>,
    /// Coordinates of the EVSE.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub coordinates: Option<GeoLocation>,
    /// A number/string printed on the outside of the EVSE for visual identification.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub physical_reference: Option<OcpiString<16>>,
    /// Directions on how to reach the EVSE from the Location.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    #[builder(default)]
    pub directions: Vec<DisplayText>,
    /// The restrictions that apply to the parking spot.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    #[builder(default)]
    pub parking_restrictions: Vec<ParkingRestriction>,
    /// Links to images related to the EVSE.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    #[builder(default)]
    pub images: Vec<Image>,
    /// Timestamp when this EVSE or one of its Connectors was last updated.
    pub last_updated: DateTime,
    /// Undocumented JSON fields, preserved verbatim.
    #[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
    #[builder(default)]
    pub extensions: Extensions,
}

impl Validate for Evse {
    fn validate_in(&self, v: &mut Validator) {
        validate_fields!(
            self,
            v,
            uid,
            evse_id,
            status_schedule,
            capabilities,
            connectors,
            floor_level,
            coordinates,
            physical_reference,
            directions,
            parking_restrictions,
            images,
            last_updated,
        );
        if self.connectors.is_empty() {
            v.report_at(
                "connectors",
                ViolationCode::EmptyRequiredList,
                "an EVSE has cardinality `+` connectors: at least one is required",
            );
        }
    }
}

/// The socket, or cable and plug, available for the EV to use, in OCPI 2.1.1.
///
/// The electrical fields are named `voltage` and `amperage` here; OCPI 2.2 renamed them to
/// `max_voltage` and `max_amperage` and added `max_electric_power`. `tariff_id` is a single
/// optional value; 2.2 made it the list `tariff_ids`.
///
/// Spec: 2.1.1 §mod_locations_connector_object
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Builder)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[builder(on(_, into))]
pub struct Connector {
    /// Identifier of the connector within the EVSE.
    pub id: OcpiString<36>,
    /// The standard of the installed connector.
    pub standard: ConnectorType,
    /// The format (socket/cable) of the installed connector.
    pub format: ConnectorFormat,
    /// Whether the connector supplies AC or DC, and on how many phases.
    pub power_type: PowerType,
    /// Voltage of the connector (line to neutral for `AC_3_PHASE`), in volt.
    pub voltage: i32,
    /// Maximum amperage of the connector, in ampere.
    pub amperage: i32,
    /// Identifier of the current charging tariff structure.
    ///
    /// > *For a "Free of Charge" tariff this field should be set, and point to a defined "Free of
    /// > Charge" tariff.*
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub tariff_id: Option<OcpiString<36>>,
    /// URL to the operator's terms and conditions.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub terms_and_conditions: Option<Url>,
    /// Timestamp when this Connector was last updated.
    pub last_updated: DateTime,
    /// Undocumented JSON fields, preserved verbatim.
    #[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
    #[builder(default)]
    pub extensions: Extensions,
}

impl Validate for Connector {
    fn validate_in(&self, v: &mut Validator) {
        validate_fields!(
            self,
            v,
            id,
            standard,
            format,
            power_type,
            tariff_id,
            terms_and_conditions,
            last_updated,
        );
        if self.voltage <= 0 {
            v.report_at("voltage", ViolationCode::OutOfRange, "must be a positive voltage");
        }
        if self.amperage <= 0 {
            v.report_at("amperage", ViolationCode::OutOfRange, "must be a positive amperage");
        }
    }
}

ocpi_lenient_enum! {
    /// The general type of the charge point location.
    ///
    /// Removed in OCPI 2.2, which replaced it with the optional
    /// [`ParkingType`](crate::v2_3_0::locations::ParkingType) and dropped the `OTHER`/`UNKNOWN`
    /// escape hatches.
    ///
    /// Spec: 2.1.1 §mod_locations_locationtype_enum
    pub enum LocationType {
        /// Parking in public space.
        OnStreet = "ON_STREET",
        /// Multistorey car park.
        ParkingGarage = "PARKING_GARAGE",
        /// Multistorey car park, mainly underground.
        UndergroundGarage = "UNDERGROUND_GARAGE",
        /// A cleared area intended for parking vehicles.
        ParkingLot = "PARKING_LOT",
        /// None of the given possibilities.
        Other = "OTHER",
        /// Not known by the operator. The default.
        Unknown = "UNKNOWN",
    }
}

ocpi_lenient_enum! {
    /// The capabilities of an EVSE, in OCPI 2.1.1.
    ///
    /// Six values; OCPI 2.2 grew this to thirteen.
    ///
    /// Spec: 2.1.1 §mod_locations_capability_enum
    pub enum Capability {
        /// The EVSE supports charging profiles.
        ChargingProfileCapable = "CHARGING_PROFILE_CAPABLE",
        /// Payment of a charging session can be done using a credit card.
        CreditCardPayable = "CREDIT_CARD_PAYABLE",
        /// The EVSE can remotely be started/stopped.
        RemoteStartStopCapable = "REMOTE_START_STOP_CAPABLE",
        /// The EVSE can be reserved.
        Reservable = "RESERVABLE",
        /// Charging at this EVSE can be authorized with an RFID token.
        RfidReader = "RFID_READER",
        /// Connectors have a mechanical lock that can be requested to be unlocked.
        UnlockCapable = "UNLOCK_CAPABLE",
    }
}

ocpi_lenient_enum! {
    /// The socket or plug standard of the charging point, in OCPI 2.1.1.
    ///
    /// Twenty values. Everything OCPI 2.2 and 2.3.0 added — the GB/T, IEC 60309, NEMA,
    /// pantograph, ChaoJi, MCS and SAE J3400 families — is absent, which is precisely why
    /// [`ocpi_lenient_enum!`] is used here: a 2.1.1 peer that has installed a CCS-adjacent plug
    /// invented in the last decade will send a value this list does not have.
    ///
    /// Spec: 2.1.1 §mod_locations_connectortype_enum
    pub enum ConnectorType {
        /// CHAdeMO, DC.
        Chademo = "CHADEMO",
        /// Standard/Domestic household, type "A", NEMA 1-15, 2 pins.
        DomesticA = "DOMESTIC_A",
        /// Standard/Domestic household, type "B", NEMA 5-15, 3 pins.
        DomesticB = "DOMESTIC_B",
        /// Standard/Domestic household, type "C", CEE 7/17, 2 pins.
        DomesticC = "DOMESTIC_C",
        /// Standard/Domestic household, type "D", 3 pin.
        DomesticD = "DOMESTIC_D",
        /// Standard/Domestic household, type "E", CEE 7/5, 3 pins.
        DomesticE = "DOMESTIC_E",
        /// Standard/Domestic household, type "F", CEE 7/4, Schuko, 3 pins.
        DomesticF = "DOMESTIC_F",
        /// Standard/Domestic household, type "G", BS 1363, Commonwealth, 3 pins.
        DomesticG = "DOMESTIC_G",
        /// Standard/Domestic household, type "H", SI-32, 3 pins.
        DomesticH = "DOMESTIC_H",
        /// Standard/Domestic household, type "I", AS 3112, 3 pins.
        DomesticI = "DOMESTIC_I",
        /// Standard/Domestic household, type "J", SEV 1011, 3 pins.
        DomesticJ = "DOMESTIC_J",
        /// Standard/Domestic household, type "K", DS 60884-2-D1, 3 pins.
        DomesticK = "DOMESTIC_K",
        /// Standard/Domestic household, type "L", CEI 23-16-VII, 3 pins.
        DomesticL = "DOMESTIC_L",
        /// IEC 60309-2 Industrial Connector, single phase 16 A (usually blue).
        Iec603092Single16 = "IEC_60309_2_single_16",
        /// IEC 60309-2 Industrial Connector, three phases 16 A (usually red).
        Iec603092Three16 = "IEC_60309_2_three_16",
        /// IEC 60309-2 Industrial Connector, three phases 32 A (usually red).
        Iec603092Three32 = "IEC_60309_2_three_32",
        /// IEC 60309-2 Industrial Connector, three phases 64 A (usually red).
        Iec603092Three64 = "IEC_60309_2_three_64",
        /// IEC 62196 Type 1 "SAE J1772".
        Iec62196T1 = "IEC_62196_T1",
        /// Combo Type 1 based, DC.
        Iec62196T1Combo = "IEC_62196_T1_COMBO",
        /// IEC 62196 Type 2 "Mennekes".
        Iec62196T2 = "IEC_62196_T2",
        /// Combo Type 2 based, DC.
        Iec62196T2Combo = "IEC_62196_T2_COMBO",
        /// IEC 62196 Type 3A.
        Iec62196T3A = "IEC_62196_T3A",
        /// IEC 62196 Type 3C "Scame".
        Iec62196T3C = "IEC_62196_T3C",
        /// Tesla Connector "Roadster"-type (round, 4 pin).
        TeslaR = "TESLA_R",
        /// Tesla Connector "Model-S"-type (oval, 5 pin).
        TeslaS = "TESLA_S",
    }
}

ocpi_lenient_enum! {
    /// Facilities a charging location directly belongs to, in OCPI 2.1.1.
    ///
    /// Spec: 2.1.1 §mod_locations_facility_enum
    pub enum Facility {
        /// A hotel.
        Hotel = "HOTEL",
        /// A restaurant.
        Restaurant = "RESTAURANT",
        /// A cafe.
        Cafe = "CAFE",
        /// A mall or shopping center.
        Mall = "MALL",
        /// A supermarket.
        Supermarket = "SUPERMARKET",
        /// Sport facilities.
        Sport = "SPORT",
        /// A recreation area.
        RecreationArea = "RECREATION_AREA",
        /// Located in, or close to, a park or nature reserve.
        Nature = "NATURE",
        /// A museum.
        Museum = "MUSEUM",
        /// A bus stop.
        BusStop = "BUS_STOP",
        /// A taxi stand.
        TaxiStand = "TAXI_STAND",
        /// A train station.
        TrainStation = "TRAIN_STATION",
        /// An airport.
        Airport = "AIRPORT",
        /// A carpool parking.
        CarpoolParking = "CARPOOL_PARKING",
        /// A fuel station.
        FuelStation = "FUEL_STATION",
        /// Wifi or other type of internet available.
        Wifi = "WIFI",
    }
}

ocpi_lenient_enum! {
    /// Restrictions on the parking spot, in OCPI 2.1.1.
    ///
    /// Spec: 2.1.1 §mod_locations_parkingrestriction_enum
    pub enum ParkingRestriction {
        /// Reserved parking spot for electric vehicles.
        EvOnly = "EV_ONLY",
        /// Parking is only allowed while plugged in (charging).
        Plugged = "PLUGGED",
        /// Reserved parking spot for disabled people with a valid ID.
        Disabled = "DISABLED",
        /// Parking spot for customers or guests only.
        Customers = "CUSTOMERS",
        /// Parking spot only suitable for (electric) motorcycles or scooters.
        Motorcycles = "MOTORCYCLES",
    }
}

ocpi_lenient_enum! {
    /// Whether a connector supplies AC or DC, in OCPI 2.1.1.
    ///
    /// The two-phase variants arrived in OCPI 2.2.
    ///
    /// Spec: 2.1.1 §mod_locations_powertype_enum
    pub enum PowerType {
        /// AC single phase.
        Ac1Phase = "AC_1_PHASE",
        /// AC three phases.
        Ac3Phase = "AC_3_PHASE",
        /// Direct current.
        Dc = "DC",
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn the_2_1_1_enums_are_much_smaller_than_the_later_ones() {
        assert_eq!(ConnectorType::ALL_KNOWN.len(), 25);
        // The four IEC 60309 industrial sockets are the only lower-case wire values in OCPI.
        let blue: ConnectorType = "IEC_60309_2_single_16".into();
        assert!(blue.is_known(), "the blue 16 A industrial socket is a 2.1.1 value");
        assert_eq!(serde_json::to_string(&blue).unwrap(), "\"IEC_60309_2_single_16\"");
        assert_eq!(Capability::ALL_KNOWN.len(), 6);
        assert_eq!(PowerType::ALL_KNOWN.len(), 3);
        // A connector standard invented after 2.1.1 still decodes …
        let mcs: ConnectorType = "MCS".into();
        assert_eq!(serde_json::to_string(&mcs).unwrap(), "\"MCS\"");
        // … and is reported, because 2.1.1 declares the enum closed.
        assert!(mcs.validate().is_err());
    }

    #[test]
    fn a_2_1_1_location_has_no_owner_fields() {
        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"}"#;
        let location: Location = serde_json::from_str(json).unwrap();
        assert_eq!(location.location_type, LocationType::OnStreet);
        assert!(location.validate().is_ok());
        assert_eq!(serde_json::to_string(&location).unwrap(), json);
    }

    #[test]
    fn hours_is_a_choice_of_one_of_two_in_2_1_1() {
        // A 2.1.1 peer publishing weekday hours sends no `twentyfourseven` at all, which the
        // OCPI 2.2 shape would refuse to decode.
        let weekdays: Hours = serde_json::from_str(
            r#"{"regular_hours":[{"weekday":1,"period_begin":"08:00","period_end":"20:00"}]}"#,
        )
        .unwrap();
        assert!(weekdays.validate().is_ok());
        assert!(!weekdays.is_always_open());

        let always: Hours = serde_json::from_str(r#"{"twentyfourseven":true}"#).unwrap();
        assert!(always.validate().is_ok());
        assert!(always.is_always_open());

        // Neither, or both, is a violation: they are alternatives.
        assert!(serde_json::from_str::<Hours>("{}").unwrap().validate().is_err());
        let both: Hours = serde_json::from_str(
            r#"{"twentyfourseven":true,"regular_hours":[{"weekday":1,"period_begin":"08:00","period_end":"20:00"}]}"#,
        )
        .unwrap();
        assert!(both.validate().is_err());
    }

    #[test]
    fn the_environmental_impact_field_is_named_source_in_2_1_1() {
        // OCPI 2.2 renamed it to `category`; reusing the later type would drop the value.
        let json = r#"{"source":"CARBON_DIOXIDE","amount":230}"#;
        let impact: EnvironmentalImpact = serde_json::from_str(json).unwrap();
        assert_eq!(impact.source, EnvironmentalImpactCategory::CarbonDioxide);
        assert_eq!(serde_json::to_string(&impact).unwrap(), json);
        assert!(impact.extensions.is_empty(), "nothing fell through into extensions");
    }

    #[test]
    fn evse_uids_compare_case_sensitively_because_2_1_1_says_string() {
        let location: Location = serde_json::from_str(
            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"}"#,
        )
        .unwrap();
        assert!(location.evse("AB123").is_some());
        assert!(location.evse("ab123").is_none(), "2.1.1 types EVSE.uid as string, not CiString");
    }
}