1#[cfg(test)]
2mod tests;
3
4#[cfg(test)]
5mod tests_ir;
6
7#[cfg(test)]
8mod tests_cdr_ir;
9
10use crate::{json, schema::Warning, Caveat};
11
12use super::{
13 build, ir_object, ocpi_enum, BuilderKind, Enum, Field, Integrity, List, Number, Object, Scalar,
14 Schema, Str,
15};
16
17const MAX_LEN_IDENTIFIER: usize = 36;
22const MAX_LEN_LONG_IDENTIFIER: usize = 39;
23const MAX_LEN_EVSE_ID: usize = 48;
24
25const MAX_LEN_CURRENCY: usize = 3;
27const MAX_LEN_COUNTRY: usize = 3;
28const MAX_LEN_LANGUAGE: usize = 2;
29
30const MAX_LEN_LATITUDE: usize = 10;
32const MAX_LEN_LONGITUDE: usize = 11;
33const MAX_LEN_POSTAL_CODE: usize = 10;
34const MAX_LEN_ADDRESS: usize = 45;
35const MAX_LEN_CITY: usize = 45;
36
37const MAX_LEN_NAME: usize = 255;
40const MAX_LEN_URL: usize = 255;
41const MAX_LEN_METER_ID: usize = 255;
42const MAX_LEN_REMARK: usize = 255;
43const MAX_LEN_TIME_ZONE: usize = 255;
44const MAX_LEN_ENERGY_MIX_NAME: usize = 64;
45const MAX_LEN_DISPLAY_TEXT: usize = 512;
46const MAX_LEN_FLOOR_LEVEL: usize = 4;
47const MAX_LEN_PHYSICAL_REFERENCE: usize = 16;
48
49static ANY: Schema = Schema::Scalar(Scalar::Any);
52
53ocpi_enum!(ConnectorFormat {
60 Socket = "SOCKET",
61 Cable = "CABLE",
62});
63
64ocpi_enum!(PowerType {
66 Ac1Phase = "AC_1_PHASE",
67 Ac3Phase = "AC_3_PHASE",
68 Dc = "DC",
69});
70
71ocpi_enum!(ConnectorType {
73 Chademo = "CHADEMO",
74 DomesticA = "DOMESTIC_A",
75 DomesticB = "DOMESTIC_B",
76 DomesticC = "DOMESTIC_C",
77 DomesticD = "DOMESTIC_D",
78 DomesticE = "DOMESTIC_E",
79 DomesticF = "DOMESTIC_F",
80 DomesticG = "DOMESTIC_G",
81 DomesticH = "DOMESTIC_H",
82 DomesticI = "DOMESTIC_I",
83 DomesticJ = "DOMESTIC_J",
84 DomesticK = "DOMESTIC_K",
85 DomesticL = "DOMESTIC_L",
86 Iec603092Single16 = "IEC_60309_2_single_16",
87 Iec603092Three16 = "IEC_60309_2_three_16",
88 Iec603092Three32 = "IEC_60309_2_three_32",
89 Iec603092Three64 = "IEC_60309_2_three_64",
90 Iec62196T1 = "IEC_62196_T1",
91 Iec62196T1Combo = "IEC_62196_T1_COMBO",
92 Iec62196T2 = "IEC_62196_T2",
93 Iec62196T2Combo = "IEC_62196_T2_COMBO",
94 Iec62196T3A = "IEC_62196_T3A",
95 Iec62196T3C = "IEC_62196_T3C",
96 TeslaR = "TESLA_R",
97 TeslaS = "TESLA_S",
98});
99
100ocpi_enum!(Capability {
102 ChargingProfileCapable = "CHARGING_PROFILE_CAPABLE",
103 CreditCardPayable = "CREDIT_CARD_PAYABLE",
104 RemoteStartStopCapable = "REMOTE_START_STOP_CAPABLE",
105 Reservable = "RESERVABLE",
106 RfidReader = "RFID_READER",
107 UnlockCapable = "UNLOCK_CAPABLE",
108});
109
110ocpi_enum!(ParkingRestriction {
112 EvOnly = "EV_ONLY",
113 Plugged = "PLUGGED",
114 Disabled = "DISABLED",
115 Customers = "CUSTOMERS",
116 Motorcycles = "MOTORCYCLES",
117});
118
119ocpi_enum!(Status {
121 Available = "AVAILABLE",
122 Blocked = "BLOCKED",
123 Charging = "CHARGING",
124 Inoperative = "INOPERATIVE",
125 OutOfOrder = "OUTOFORDER",
126 Planned = "PLANNED",
127 Removed = "REMOVED",
128 Reserved = "RESERVED",
129 Unknown = "UNKNOWN",
130});
131
132ocpi_enum!(Facility {
134 Hotel = "HOTEL",
135 Restaurant = "RESTAURANT",
136 Cafe = "CAFE",
137 Mall = "MALL",
138 Supermarket = "SUPERMARKET",
139 Sport = "SPORT",
140 RecreationArea = "RECREATION_AREA",
141 Nature = "NATURE",
142 Museum = "MUSEUM",
143 BusStop = "BUS_STOP",
144 TaxiStand = "TAXI_STAND",
145 TrainStation = "TRAIN_STATION",
146 Airport = "AIRPORT",
147 CarpoolParking = "CARPOOL_PARKING",
148 FuelStation = "FUEL_STATION",
149 Wifi = "WIFI",
150});
151
152ocpi_enum!(LocationType {
154 OnStreet = "ON_STREET",
155 ParkingGarage = "PARKING_GARAGE",
156 UndergroundGarage = "UNDERGROUND_GARAGE",
157 ParkingLot = "PARKING_LOT",
158 Other = "OTHER",
159 Unknown = "UNKNOWN",
160});
161
162ocpi_enum!(AuthMethod {
164 AuthRequest = "AUTH_REQUEST",
165 Whitelist = "WHITELIST",
166});
167
168ocpi_enum!(CdrDimensionType {
170 Energy = "ENERGY",
171 Flat = "FLAT",
172 MaxCurrent = "MAX_CURRENT",
173 MinCurrent = "MIN_CURRENT",
174 ParkingTime = "PARKING_TIME",
175 Time = "TIME",
176});
177
178ocpi_enum!(TariffDimensionType {
180 Energy = "ENERGY",
181 Flat = "FLAT",
182 ParkingTime = "PARKING_TIME",
183 Time = "TIME",
184});
185
186ocpi_enum!(DayOfWeek {
188 Monday = "MONDAY",
189 Tuesday = "TUESDAY",
190 Wednesday = "WEDNESDAY",
191 Thursday = "THURSDAY",
192 Friday = "FRIDAY",
193 Saturday = "SATURDAY",
194 Sunday = "SUNDAY",
195});
196
197ocpi_enum!(EnergySourceCategory {
199 Nuclear = "NUCLEAR",
200 GeneralFossil = "GENERAL_FOSSIL",
201 Coal = "COAL",
202 Gas = "GAS",
203 GeneralGreen = "GENERAL_GREEN",
204 Solar = "SOLAR",
205 Wind = "WIND",
206 Water = "WATER",
207});
208
209ocpi_enum!(EnvironmentalImpactCategory {
211 NuclearWaste = "NUCLEAR_WASTE",
212 CarbonDioxide = "CARBON_DIOXIDE",
213});
214
215static CAPABILITY: Schema = Schema::Scalar(Scalar::Enum(Capability::VARIANTS));
217static PARKING_RESTRICTION: Schema = Schema::Scalar(Scalar::Enum(ParkingRestriction::VARIANTS));
218static FACILITY: Schema = Schema::Scalar(Scalar::Enum(Facility::VARIANTS));
219static DAY_OF_WEEK: Schema = Schema::Scalar(Scalar::Enum(DayOfWeek::VARIANTS));
220
221static GEO_LOCATION_OBJ: Object = Object {
222 fields: &[
224 Field::required("latitude", Scalar::StringMax(MAX_LEN_LATITUDE)),
225 Field::required("longitude", Scalar::StringMax(MAX_LEN_LONGITUDE)),
226 ],
227 kind: BuilderKind::Ignore,
228};
229
230static DISPLAY_TEXT_OBJ: Object = Object {
231 fields: &[
232 Field::required("language", Scalar::StringMax(MAX_LEN_LANGUAGE)),
233 Field::required("text", Scalar::StringMax(MAX_LEN_DISPLAY_TEXT)),
234 ],
235 kind: BuilderKind::Ignore,
236};
237static DISPLAY_TEXT: Schema = Schema::Object(&DISPLAY_TEXT_OBJ);
238
239static ENERGY_SOURCE_OBJ: Object = Object {
240 fields: &[
241 Field::required("percentage", Scalar::Number),
242 Field::required("source", Scalar::Enum(EnergySourceCategory::VARIANTS)),
243 ],
244 kind: BuilderKind::Ignore,
245};
246static ENERGY_SOURCE: Schema = Schema::Object(&ENERGY_SOURCE_OBJ);
247
248static ENVIRONMENTAL_IMPACT_OBJ: Object = Object {
251 fields: &[
252 Field::required("amount", Scalar::Number),
253 Field::required(
254 "source",
255 Scalar::Enum(EnvironmentalImpactCategory::VARIANTS),
256 ),
257 ],
258 kind: BuilderKind::Ignore,
259};
260static ENVIRONMENTAL_IMPACT: Schema = Schema::Object(&ENVIRONMENTAL_IMPACT_OBJ);
261
262static ENERGY_MIX_OBJ: Object = Object {
263 fields: &[
264 Field::optional(
265 "energy_product_name",
266 Scalar::StringMax(MAX_LEN_ENERGY_MIX_NAME),
267 ),
268 Field::optional_array("energy_sources", &ENERGY_SOURCE),
269 Field::optional_array("environ_impact", &ENVIRONMENTAL_IMPACT),
270 Field::required("is_green_energy", Scalar::Boolean),
271 Field::optional("supplier_name", Scalar::StringMax(MAX_LEN_ENERGY_MIX_NAME)),
272 ],
273 kind: BuilderKind::Ignore,
274};
275
276static CONNECTOR_OBJ: Object = Object {
279 fields: &[
280 Field::required("amperage", Scalar::Number),
281 Field::required("format", Scalar::Enum(ConnectorFormat::VARIANTS)),
282 Field::required("id", Scalar::StringMax(MAX_LEN_IDENTIFIER)),
283 Field::required("last_updated", Scalar::String),
284 Field::required("power_type", Scalar::Enum(PowerType::VARIANTS)),
285 Field::required("standard", Scalar::Enum(ConnectorType::VARIANTS)),
286 Field::optional("tariff_id", Scalar::StringMax(MAX_LEN_IDENTIFIER)),
287 Field::optional("terms_and_conditions", Scalar::StringMax(MAX_LEN_URL)),
288 Field::required("voltage", Scalar::Number),
289 ],
290 kind: BuilderKind::Ignore,
291};
292static CONNECTOR: Schema = Schema::Object(&CONNECTOR_OBJ);
293
294static EVSE_OBJ: Object = Object {
297 fields: &[
298 Field::optional_array("capabilities", &CAPABILITY),
299 Field::required_array("connectors", &CONNECTOR),
300 Field::optional_object("coordinates", &GEO_LOCATION_OBJ),
301 Field::optional_array("directions", &DISPLAY_TEXT),
302 Field::optional("evse_id", Scalar::StringMax(MAX_LEN_EVSE_ID)),
303 Field::optional("floor_level", Scalar::StringMax(MAX_LEN_FLOOR_LEVEL)),
304 Field::optional_array("images", &ANY),
305 Field::required("last_updated", Scalar::String),
306 Field::optional_array("parking_restrictions", &PARKING_RESTRICTION),
307 Field::optional(
308 "physical_reference",
309 Scalar::StringMax(MAX_LEN_PHYSICAL_REFERENCE),
310 ),
311 Field::required("status", Scalar::Enum(Status::VARIANTS)),
312 Field::optional_array("status_schedule", &ANY),
313 Field::required("uid", Scalar::StringMax(MAX_LEN_LONG_IDENTIFIER)),
314 ],
315 kind: BuilderKind::Ignore,
316};
317static EVSE: Schema = Schema::Object(&EVSE_OBJ);
318
319static LOCATION_OBJ: Object = Object {
324 fields: &[
325 Field::required("address", Scalar::StringMax(MAX_LEN_ADDRESS)),
326 Field::optional("charging_when_closed", Scalar::Boolean),
327 Field::required("city", Scalar::StringMax(MAX_LEN_CITY)),
328 Field::required_object("coordinates", &GEO_LOCATION_OBJ),
329 Field::required("country", Scalar::StringMax(MAX_LEN_COUNTRY)),
330 Field::optional_array("directions", &DISPLAY_TEXT),
331 Field::optional_object("energy_mix", &ENERGY_MIX_OBJ),
332 Field::optional_array("evses", &EVSE),
333 Field::optional_array("facilities", &FACILITY),
334 Field::required("id", Scalar::StringMax(MAX_LEN_LONG_IDENTIFIER)),
335 Field::optional_array("images", &ANY),
336 Field::required("last_updated", Scalar::String),
337 Field::optional("name", Scalar::StringMax(MAX_LEN_NAME)),
338 Field::optional("opening_times", Scalar::Any),
339 Field::optional("operator", Scalar::Any),
340 Field::optional("owner", Scalar::Any),
341 Field::required("postal_code", Scalar::StringMax(MAX_LEN_POSTAL_CODE)),
342 Field::optional_array("related_locations", &ANY),
343 Field::optional("suboperator", Scalar::Any),
344 Field::optional("time_zone", Scalar::StringMax(MAX_LEN_TIME_ZONE)),
345 Field::required("type", Scalar::Enum(LocationType::VARIANTS)),
346 ],
347 kind: BuilderKind::V211Location,
348};
349
350static CDR_DIMENSION_OBJ: Object = Object {
351 fields: &[
352 Field::required("type", Scalar::Enum(CdrDimensionType::VARIANTS)),
353 Field::required("volume", Scalar::Number),
354 ],
355 kind: BuilderKind::V211CdrDimension,
356};
357static CDR_DIMENSION: Schema = Schema::Object(&CDR_DIMENSION_OBJ);
358
359static CHARGING_PERIOD_OBJ: Object = Object {
362 fields: &[
363 Field::required_array("dimensions", &CDR_DIMENSION),
364 Field::required("start_date_time", Scalar::String),
365 ],
366 kind: BuilderKind::V211ChargingPeriod,
367};
368static CHARGING_PERIOD: Schema = Schema::Object(&CHARGING_PERIOD_OBJ);
369
370static PRICE_COMPONENT_OBJ: Object = Object {
373 fields: &[
374 Field::required("price", Scalar::Number),
375 Field::required("step_size", Scalar::Number),
376 Field::required("type", Scalar::Enum(TariffDimensionType::VARIANTS)),
377 ],
378 kind: BuilderKind::V211PriceComponent,
379};
380static PRICE_COMPONENT: Schema = Schema::Object(&PRICE_COMPONENT_OBJ);
381
382static TARIFF_RESTRICTIONS_OBJ: Object = Object {
386 fields: &[
387 Field::optional_array("day_of_week", &DAY_OF_WEEK),
388 Field::optional("end_date", Scalar::String),
389 Field::optional("end_time", Scalar::String),
390 Field::optional("max_duration", Scalar::Number),
391 Field::optional("max_kwh", Scalar::Number),
392 Field::optional("max_power", Scalar::Number),
393 Field::optional("min_duration", Scalar::Number),
394 Field::optional("min_kwh", Scalar::Number),
395 Field::optional("min_power", Scalar::Number),
396 Field::optional("start_date", Scalar::String),
397 Field::optional("start_time", Scalar::String),
398 ],
399 kind: BuilderKind::V211Restrictions,
400};
401
402static TARIFF_ELEMENT_OBJ: Object = Object {
403 fields: &[
404 Field::required_array("price_components", &PRICE_COMPONENT),
405 Field::optional_object("restrictions", &TARIFF_RESTRICTIONS_OBJ),
406 ],
407 kind: BuilderKind::V211Element,
408};
409static TARIFF_ELEMENT: Schema = Schema::Object(&TARIFF_ELEMENT_OBJ);
410
411static TARIFF_OBJ: Object = Object {
415 fields: &[
416 Field::required("currency", Scalar::StringMax(MAX_LEN_CURRENCY)),
417 Field::required_array("elements", &TARIFF_ELEMENT),
418 Field::optional_object("energy_mix", &ENERGY_MIX_OBJ),
419 Field::required("id", Scalar::StringMax(MAX_LEN_IDENTIFIER)),
420 Field::required("last_updated", Scalar::String),
421 Field::optional_array("tariff_alt_text", &DISPLAY_TEXT),
422 Field::optional("tariff_alt_url", Scalar::StringMax(MAX_LEN_URL)),
423 ],
424 kind: BuilderKind::V211Tariff,
425};
426
427static TARIFF: Schema = Schema::Object(&TARIFF_OBJ);
432
433static CDR_OBJ: Object = Object {
443 fields: &[
444 Field::required("auth_id", Scalar::StringMax(MAX_LEN_IDENTIFIER)),
445 Field::required("auth_method", Scalar::Enum(AuthMethod::VARIANTS)),
446 Field::required_array("charging_periods", &CHARGING_PERIOD),
447 Field::required("currency", Scalar::StringMax(MAX_LEN_CURRENCY)),
448 Field::required("id", Scalar::StringMax(MAX_LEN_IDENTIFIER)),
449 Field::required("last_updated", Scalar::String),
450 Field::required_object("location", &LOCATION_OBJ),
451 Field::optional("meter_id", Scalar::StringMax(MAX_LEN_METER_ID)),
452 Field::optional("remark", Scalar::StringMax(MAX_LEN_REMARK)),
453 Field::required("start_date_time", Scalar::String),
454 Field::required("stop_date_time", Scalar::String),
455 Field::optional_array("tariffs", &TARIFF),
456 Field::required("total_cost", Scalar::Number),
457 Field::required("total_energy", Scalar::Number),
458 Field::optional("total_parking_time", Scalar::Number),
459 Field::required("total_time", Scalar::Number),
460 ],
461 kind: BuilderKind::V211Cdr,
462};
463
464static CDR: Schema = Schema::Object(&CDR_OBJ);
470
471#[derive(Clone, Debug)]
480pub(crate) struct Cdr<'buf> {
481 pub currency: Integrity<Str<'buf>>,
482 pub start_date_time: Integrity<Str<'buf>>,
483 pub stop_date_time: Integrity<Str<'buf>>,
484 pub charging_periods: Integrity<List<'buf, ChargingPeriod<'buf>>>,
485 pub total_cost: Integrity<Number<'buf>>,
486 pub total_energy: Integrity<Number<'buf>>,
487 pub total_time: Integrity<Number<'buf>>,
488 pub total_parking_time: Integrity<Option<Number<'buf>>>,
489 pub tariffs: Integrity<Option<List<'buf, Tariff<'buf>>>>,
490 pub location: Integrity<Location<'buf>>,
491}
492
493#[derive(Clone, Debug)]
498pub(crate) struct Location<'buf> {
499 pub country: Integrity<Str<'buf>>,
500 pub time_zone: Integrity<Option<Str<'buf>>>,
501}
502
503#[derive(Clone, Debug)]
505pub(crate) struct ChargingPeriod<'buf> {
506 pub start_date_time: Integrity<Str<'buf>>,
507 pub dimensions: Integrity<List<'buf, Dimension<'buf>>>,
508}
509
510#[derive(Clone, Debug)]
512pub(crate) struct Dimension<'buf> {
513 pub dimension_type: Integrity<Enum<'buf, CdrDimensionType>>,
514 pub volume: Integrity<Number<'buf>>,
515}
516
517#[derive(Clone, Debug)]
527pub(crate) struct Tariff<'buf> {
528 pub currency: Integrity<Str<'buf>>,
529 pub id: Integrity<Str<'buf>>,
530 pub elements: Integrity<List<'buf, Element<'buf>>>,
531}
532
533#[derive(Clone, Debug)]
535pub(crate) struct Element<'buf> {
536 pub price_components: Integrity<List<'buf, PriceComponent<'buf>>>,
537 pub restrictions: Integrity<Option<Restrictions<'buf>>>,
538}
539
540#[derive(Clone, Debug)]
542pub(crate) struct PriceComponent<'buf> {
543 pub dimension_type: Integrity<Enum<'buf, TariffDimensionType>>,
544 pub price: Integrity<Number<'buf>>,
545 pub step_size: Integrity<Number<'buf>>,
546}
547
548#[derive(Clone, Debug)]
551pub(crate) struct Restrictions<'buf> {
552 pub start_time: Integrity<Option<Str<'buf>>>,
553 pub end_time: Integrity<Option<Str<'buf>>>,
554 pub start_date: Integrity<Option<Str<'buf>>>,
555 pub end_date: Integrity<Option<Str<'buf>>>,
556 pub min_kwh: Integrity<Option<Number<'buf>>>,
557 pub max_kwh: Integrity<Option<Number<'buf>>>,
558 pub min_power: Integrity<Option<Number<'buf>>>,
559 pub max_power: Integrity<Option<Number<'buf>>>,
560 pub min_duration: Integrity<Option<Number<'buf>>>,
561 pub max_duration: Integrity<Option<Number<'buf>>>,
562 pub day_of_week: Integrity<Option<List<'buf, Enum<'buf, DayOfWeek>>>>,
563}
564
565ir_object!(Cdr {
568 currency,
569 start_date_time,
570 stop_date_time,
571 charging_periods,
572 total_cost,
573 total_energy,
574 total_time,
575 total_parking_time,
576 tariffs,
577 location,
578});
579ir_object!(Location { country, time_zone });
580ir_object!(ChargingPeriod {
581 start_date_time,
582 dimensions,
583});
584ir_object!(Dimension {
585 dimension_type,
586 volume,
587});
588ir_object!(Tariff {
589 currency,
590 id,
591 elements,
592});
593ir_object!(Element {
594 price_components,
595 restrictions,
596});
597ir_object!(PriceComponent {
598 dimension_type,
599 price,
600 step_size,
601});
602ir_object!(Restrictions {
603 start_time,
604 end_time,
605 start_date,
606 end_date,
607 min_kwh,
608 max_kwh,
609 min_power,
610 max_power,
611 min_duration,
612 max_duration,
613 day_of_week,
614});
615
616pub(crate) fn build_tariff<'buf>(doc: &json::Document<'buf>) -> Caveat<Tariff<'buf>, Warning> {
621 let (node, warnings) = super::walk(doc, &TARIFF).into_parts();
622
623 let tariff = if let build::Node::V211Tariff(tariff) = node {
626 tariff
627 } else {
628 Tariff::new(doc.root())
629 };
630
631 Caveat::new(tariff, warnings)
632}
633
634pub(crate) fn build_cdr<'buf>(doc: &json::Document<'buf>) -> Caveat<Cdr<'buf>, Warning> {
639 let (node, warnings) = super::walk(doc, &CDR).into_parts();
640
641 let cdr = if let build::Node::V211Cdr(cdr) = node {
644 cdr
645 } else {
646 Cdr::new(doc.root())
647 };
648
649 Caveat::new(cdr, warnings)
650}