1#[cfg(test)]
4mod test;
5
6#[cfg(test)]
7mod test_clamp_date_time_span;
8
9#[cfg(test)]
10mod test_gen_time_events;
11
12#[cfg(test)]
13mod test_generate;
14
15#[cfg(test)]
16mod test_generate_from_single_elem_tariff;
17
18#[cfg(test)]
19mod test_local_to_utc;
20
21#[cfg(test)]
22mod test_periods;
23
24#[cfg(test)]
25mod test_power_to_time;
26
27#[cfg(test)]
28mod test_popular_tariffs;
29
30mod v2x;
31
32use std::{
33 cmp::{max, min},
34 fmt,
35 ops::Range,
36};
37
38use chrono::{DateTime, Datelike as _, NaiveDateTime, NaiveTime, TimeDelta, Utc};
39use rust_decimal::Decimal;
40use rust_decimal_macros::dec;
41use tracing::{debug, instrument, warn};
42
43use crate::{
44 country, currency,
45 duration::{AsHms as _, ToHoursDecimal},
46 energy::{Ampere, Kw, Kwh},
47 from_warning_all,
48 number::{FromDecimal as _, RoundDecimal as _},
49 price, tariff,
50 warning::{self, GatherWarnings as _, IntoCaveat as _, WithElement as _},
51 Price, SaturatingAdd as _, ToDuration as _,
52};
53
54const MIN_CS_DURATION_SECS: i64 = 120;
56
57type DateTimeSpan = Range<DateTime<Utc>>;
58pub type Verdict<T> = crate::Verdict<T, Warning>;
60pub type Caveat<T> = warning::Caveat<T, Warning>;
62
63macro_rules! some_dec_or_bail {
65 ($elem:expr, $opt:expr, $warnings:expr, $msg:literal) => {
66 match $opt {
67 Some(v) => v,
68 None => {
69 return $warnings.bail($elem.as_element(), Warning::Decimal($msg));
70 }
71 }
72 };
73}
74
75macro_rules! some_time_delta_or_bail {
77 ($elem:expr, $opt:expr, $warnings:expr, $msg:literal) => {
78 match $opt {
79 Some(v) => v,
80 None => {
81 return $warnings.bail($elem.as_element(), Warning::TimeDelta($msg));
82 }
83 }
84 };
85}
86
87#[derive(Debug)]
89pub struct Report {
90 pub tariff_id: String,
92
93 pub tariff_currency_code: currency::Code,
95
96 pub partial_cdr: PartialCdr,
103}
104
105#[derive(Debug)]
113pub struct PartialCdr {
114 pub currency_code: currency::Code,
116
117 pub party_id: Option<CpoId>,
125
126 pub start_date_time: DateTime<Utc>,
128
129 pub end_date_time: DateTime<Utc>,
131
132 pub total_energy: Option<Kwh>,
134
135 pub total_charging_duration: Option<TimeDelta>,
139
140 pub total_idle_duration: Option<TimeDelta>,
144
145 pub total_cost: Option<Price>,
147
148 pub total_energy_cost: Option<Price>,
150
151 pub total_fixed_cost: Option<Price>,
153
154 pub total_idle_duration_cost: Option<Price>,
156
157 pub total_charging_duration_cost: Option<Price>,
159
160 pub charging_periods: Vec<ChargingPeriod>,
163}
164
165#[derive(Clone, Debug)]
170pub struct CpoId {
171 pub country_code: country::Code,
173
174 pub id: String,
176}
177
178impl<'buf> From<tariff::CpoId<'buf>> for CpoId {
179 fn from(value: tariff::CpoId<'buf>) -> Self {
180 let tariff::CpoId { country_code, id } = value;
181 CpoId {
182 country_code,
183 id: id.to_string(),
184 }
185 }
186}
187
188impl fmt::Display for CpoId {
190 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
191 write!(f, "{}{}", self.country_code.into_alpha_2_str(), self.id)
192 }
193}
194
195#[derive(Debug)]
199pub struct ChargingPeriod {
200 pub start_date_time: DateTime<Utc>,
203
204 pub dimensions: Vec<Dimension>,
206
207 pub tariff_id: Option<String>,
211}
212
213#[derive(Debug)]
217pub struct Dimension {
218 pub dimension_type: DimensionType,
220
221 pub volume: Decimal,
223}
224
225#[derive(Debug, Clone, PartialEq, Eq)]
229pub enum DimensionType {
230 Energy,
232
233 MaxCurrent,
235
236 MinCurrent,
238
239 MaxPower,
241
242 MinPower,
244
245 ParkingTime,
247
248 ReservationTime,
250
251 Time,
253}
254
255#[derive(Clone)]
257pub struct Config {
258 pub timezone: chrono_tz::Tz,
260
261 pub end_date_time: DateTime<Utc>,
263
264 pub max_current_supply_amp: Decimal,
266
267 pub requested_kwh: Decimal,
272
273 pub max_power_supply_kw: Decimal,
282
283 pub start_date_time: DateTime<Utc>,
285}
286
287pub fn cdr_from_tariff(tariff_elem: &tariff::Versioned<'_>, config: &Config) -> Verdict<Report> {
289 let mut warnings = warning::Set::new();
290 let (metrics, timezone) = metrics(tariff_elem, config)?.gather_warnings_into(&mut warnings);
298
299 let tariff = tariff_elem.to_v221()?.gather_warnings_into(&mut warnings);
300
301 if !is_tariff_active(&metrics.start_date_time, &tariff) {
302 warnings.insert(tariff_elem.as_element(), tariff::Warning::NotActive.into());
303 }
304
305 let timeline = timeline(timezone, &metrics, &tariff);
306 let charging_periods = charge_periods(&metrics, timeline);
307
308 let report = price::periods(metrics.end_date_time, timezone, &tariff, charging_periods)
309 .with_element(tariff_elem.as_element())?
310 .gather_warnings_into(&mut warnings);
311
312 let price::PeriodsReport {
313 billable: _,
314 periods,
315 totals,
316 total_costs,
317 } = report;
318
319 let charging_periods = periods
320 .into_iter()
321 .map(|period| {
322 let price::PeriodReport {
323 start_date_time,
324 end_date_time: _,
325 dimensions,
326 } = period;
327 let duration_charging = dimensions.duration_charging.as_ref().map(|dim| Dimension {
328 dimension_type: DimensionType::Time,
329 volume: ToHoursDecimal::to_hours_dec_in_ocpi_precision(&dim.volume),
330 });
331 let duration_idle = dimensions.duration_idle.as_ref().map(|dim| Dimension {
332 dimension_type: DimensionType::ParkingTime,
333 volume: ToHoursDecimal::to_hours_dec_in_ocpi_precision(&dim.volume),
334 });
335 let energy = dimensions.energy.as_ref().map(|dim| Dimension {
336 dimension_type: DimensionType::Energy,
337 volume: dim.volume.into(),
338 });
339 let dimensions = vec![energy, duration_idle, duration_charging]
340 .into_iter()
341 .flatten()
342 .collect();
343
344 ChargingPeriod {
345 start_date_time,
346 dimensions,
347 tariff_id: Some(tariff.id.to_string()),
348 }
349 })
350 .collect();
351
352 let mut total_cost = total_costs.total();
353
354 if let Some(total_cost) = total_cost.as_mut() {
355 if let Some(min_price) = tariff.min_price {
356 if *total_cost < min_price {
357 *total_cost = min_price;
358 warnings.insert(
359 tariff_elem.as_element(),
360 tariff::Warning::TotalCostClampedToMin.into(),
361 );
362 }
363 }
364
365 if let Some(max_price) = tariff.max_price {
366 if *total_cost > max_price {
367 *total_cost = max_price;
368 warnings.insert(
369 tariff_elem.as_element(),
370 tariff::Warning::TotalCostClampedToMax.into(),
371 );
372 }
373 }
374 }
375
376 let report = Report {
377 tariff_id: tariff.id.to_string(),
378 tariff_currency_code: tariff.currency,
379 partial_cdr: PartialCdr {
380 party_id: tariff.party_id.map(CpoId::from),
381 start_date_time: metrics.start_date_time,
382 end_date_time: metrics.end_date_time,
383 currency_code: tariff.currency,
384 total_energy: totals.energy.round_to_ocpi_scale(),
385 total_charging_duration: totals.duration_charging,
386 total_idle_duration: totals.duration_idle,
387 total_cost: total_cost.round_to_ocpi_scale(),
388 total_energy_cost: total_costs.energy.round_to_ocpi_scale(),
389 total_fixed_cost: total_costs.fixed.round_to_ocpi_scale(),
390 total_idle_duration_cost: total_costs.duration_idle.round_to_ocpi_scale(),
391 total_charging_duration_cost: total_costs.duration_charging.round_to_ocpi_scale(),
392 charging_periods,
393 },
394 };
395
396 Ok(report.into_caveat(warnings))
397}
398
399struct EventCollector {
401 session_duration: TimeDelta,
403
404 events: Vec<Event>,
406}
407
408impl EventCollector {
409 fn with_session_duration(session_duration: TimeDelta) -> Self {
411 Self {
412 session_duration,
413 events: vec![],
414 }
415 }
416
417 fn push(&mut self, duration_from_start: TimeDelta, event_kind: EventKind) {
419 if duration_from_start <= self.session_duration {
420 self.events.push(Event {
421 duration_from_start,
422 kind: event_kind,
423 });
424 }
425 }
426
427 fn into_inner(self) -> Vec<Event> {
429 self.events
430 }
431}
432
433fn timeline(
435 timezone: chrono_tz::Tz,
436 metrics: &Metrics,
437 tariff: &tariff::v221::Tariff<'_>,
438) -> Timeline {
439 let Metrics {
440 start_date_time: cdr_start,
441 end_date_time: cdr_end,
442 duration_charging,
443 duration_parking,
444 max_power_supply,
445 max_current_supply,
446
447 energy_supplied: _,
448 } = metrics;
449
450 let mut events = {
451 let session_duration = duration_parking.map(|d| duration_charging.saturating_add(d));
452 let mut events =
453 EventCollector::with_session_duration(session_duration.unwrap_or(*duration_charging));
454
455 events.push(TimeDelta::seconds(0), EventKind::SessionStart);
456 events.push(*duration_charging, EventKind::ChargingEnd);
457
458 if let Some(dt) = session_duration {
459 events.push(
460 dt,
461 EventKind::ParkingEnd {
462 start: *duration_charging,
463 },
464 );
465 }
466
467 events
468 };
469
470 let mut emit_current = false;
473
474 let mut emit_power = false;
477
478 for elem in &tariff.elements {
479 if elem
482 .restrictions
483 .as_ref()
484 .is_some_and(|r| r.reservation.is_some())
485 {
486 continue;
487 }
488
489 if let Some((time_restrictions, energy_restrictions)) = elem
490 .restrictions
491 .as_ref()
492 .map(tariff::v221::Restrictions::restrictions_by_category)
493 {
494 generate_time_events(
495 &mut events,
496 timezone,
497 *cdr_start..*cdr_end,
498 time_restrictions,
499 );
500
501 let v2x::EnergyRestrictions {
502 min_kwh,
503 max_kwh,
504 min_current,
505 max_current,
506 min_power,
507 max_power,
508 } = energy_restrictions;
509
510 if !emit_current {
511 emit_current = (min_current..=max_current).contains(&Some(*max_current_supply));
516 }
517
518 if !emit_power {
519 emit_power = (min_power..=max_power).contains(&Some(*max_power_supply));
524 }
525
526 generate_energy_events(
527 &mut events,
528 metrics.duration_charging,
529 metrics.energy_supplied,
530 min_kwh,
531 max_kwh,
532 );
533 }
534 }
535
536 let events = events.into_inner();
537
538 Timeline {
539 events,
540 emit_current,
541 emit_power,
542 }
543}
544
545fn generate_time_events(
547 events: &mut EventCollector,
548 timezone: chrono_tz::Tz,
549 cdr_span: DateTimeSpan,
550 restrictions: v2x::TimeRestrictions,
551) {
552 const MIDNIGHT: NaiveTime = NaiveTime::from_hms_opt(0, 0, 0)
553 .expect("The hour, minute and second values are correct and hardcoded");
554 const ONE_DAY: TimeDelta = TimeDelta::days(1);
555
556 let v2x::TimeRestrictions {
557 start_time,
558 end_time,
559 start_date,
560 end_date,
561 min_duration,
562 max_duration,
563 weekdays,
564 } = restrictions;
565
566 let cdr_duration = cdr_span.end.signed_duration_since(cdr_span.start);
567
568 if let Some(dt) = min_duration {
569 if cdr_duration > dt {
570 events.push(dt, EventKind::MinDuration);
571 }
572 }
573
574 if let Some(dt) = max_duration {
575 if cdr_duration > dt {
576 events.push(dt, EventKind::MaxDuration);
577 }
578 }
579
580 let (start_date_time, end_date_time) =
590 if let (Some(start_time), Some(end_time)) = (start_time, end_time) {
591 if end_time < start_time {
592 (
593 start_date.map(|d| d.and_time(start_time)),
594 end_date.map(|d| {
595 let (end_time, _) = end_time.overflowing_add_signed(ONE_DAY);
596 d.and_time(end_time)
597 }),
598 )
599 } else {
600 (
601 start_date.map(|d| d.and_time(start_time)),
602 end_date.map(|d| d.and_time(end_time)),
603 )
604 }
605 } else {
606 (
607 start_date.map(|d| d.and_time(start_time.unwrap_or(MIDNIGHT))),
608 end_date.map(|d| d.and_time(end_time.unwrap_or(MIDNIGHT))),
609 )
610 };
611
612 let event_span = clamp_date_time_span(
615 start_date_time.and_then(|d| local_to_utc(timezone, d)),
616 end_date_time.and_then(|d| local_to_utc(timezone, d)),
617 cdr_span,
618 );
619
620 if let Some(start_time) = start_time {
621 gen_naive_time_events(
622 events,
623 &event_span,
624 timezone,
625 start_time,
626 &weekdays,
627 EventKind::StartTime,
628 );
629 }
630
631 if let Some(end_time) = end_time {
632 gen_naive_time_events(
633 events,
634 &event_span,
635 timezone,
636 end_time,
637 &weekdays,
638 EventKind::EndTime,
639 );
640 }
641}
642
643fn local_to_utc(timezone: chrono_tz::Tz, date_time: NaiveDateTime) -> Option<DateTime<Utc>> {
649 use chrono::offset::LocalResult;
650
651 let result = date_time.and_local_timezone(timezone);
652
653 let local_date_time = match result {
654 LocalResult::Single(d) => d,
655 LocalResult::Ambiguous(earliest, _latest) => earliest,
656 LocalResult::None => return None,
657 };
658
659 Some(local_date_time.to_utc())
660}
661
662fn gen_naive_time_events(
664 events: &mut EventCollector,
665 event_span: &Range<DateTime<Utc>>,
666 timezone: chrono_tz::Tz,
667 time: NaiveTime,
668 weekdays: &v2x::WeekdaySet,
669 kind: EventKind,
670) {
671 let local_start_time = event_span.start.with_timezone(&timezone).time();
672 let time_delta = time.signed_duration_since(local_start_time);
673 let cdr_duration = event_span.end.signed_duration_since(event_span.start);
674
675 let time_delta = if time_delta.num_seconds().is_negative() {
677 time_delta.saturating_add(TimeDelta::days(1))
678 } else {
679 time_delta
680 };
681
682 if time_delta.num_seconds().is_negative() {
684 return;
685 }
686
687 let Some(remainder) = cdr_duration.checked_sub(&time_delta) else {
689 warn!("TimeDelta overflow");
690 return;
691 };
692
693 if remainder.num_seconds().is_positive() {
694 let duration_from_start = time_delta;
695 let Some(date) = event_span.start.checked_add_signed(duration_from_start) else {
696 warn!("Date out of range");
697 return;
698 };
699
700 if weekdays.contains(date.weekday()) {
701 events.push(time_delta, kind);
703 }
704
705 for day in 1..=remainder.num_days() {
706 let Some(duration_from_start) = time_delta.checked_add(&TimeDelta::days(day)) else {
707 warn!("Date out of range");
708 break;
709 };
710 let Some(date) = event_span.start.checked_add_signed(duration_from_start) else {
711 warn!("Date out of range");
712 break;
713 };
714
715 if weekdays.contains(date.weekday()) {
716 events.push(duration_from_start, kind);
717 }
718 }
719 }
720}
721
722fn generate_energy_events(
724 events: &mut EventCollector,
725 duration_charging: TimeDelta,
726 energy_supplied: Kwh,
727 min_kwh: Option<Kwh>,
728 max_kwh: Option<Kwh>,
729) {
730 if let Some(dt) = min_kwh.and_then(|kwh| power_to_time(kwh, energy_supplied, duration_charging))
731 {
732 events.push(dt, EventKind::MinKwh);
733 }
734
735 if let Some(dt) = max_kwh.and_then(|kwh| power_to_time(kwh, energy_supplied, duration_charging))
736 {
737 events.push(dt, EventKind::MaxKwh);
738 }
739}
740
741#[instrument]
743fn power_to_time(power: Kwh, power_total: Kwh, duration_total: TimeDelta) -> Option<TimeDelta> {
744 if power == power_total {
747 return Some(duration_total);
748 }
749
750 let power = Decimal::from(power);
753 let power_total = Decimal::from(power_total);
755
756 let Some(factor) = power.checked_div(power_total) else {
758 return Some(TimeDelta::zero());
759 };
760
761 if factor.is_sign_negative() || factor > dec!(1.0) {
762 return None;
763 }
764
765 let hours_dec = duration_total.to_hours_dec();
766 let duration_from_start = factor.checked_mul(hours_dec)?;
767 Some(duration_from_start.to_duration())
768}
769
770fn charge_periods(metrics: &Metrics, timeline: Timeline) -> Vec<price::Period> {
772 enum ChargingPhase {
774 Charging,
775 Parking,
776 }
777
778 let Metrics {
779 start_date_time: cdr_start,
780 max_power_supply,
781 max_current_supply,
782
783 end_date_time: _,
784 duration_charging: _,
785 duration_parking: _,
786 energy_supplied: _,
787 } = metrics;
788
789 let Timeline {
790 mut events,
791 emit_current,
792 emit_power,
793 } = timeline;
794
795 events.sort_unstable_by_key(|e| e.duration_from_start);
796
797 let mut periods = vec![];
798 let emit_current = emit_current.then_some(*max_current_supply);
799 let emit_power = emit_power.then_some(*max_power_supply);
800 let mut charging_phase = ChargingPhase::Charging;
802
803 for items in events.windows(2) {
804 let [event, event_next] = items else {
805 unreachable!("The window size is 2");
806 };
807
808 let Event {
809 duration_from_start,
810 kind,
811 } = event;
812
813 if let EventKind::ChargingEnd = kind {
814 charging_phase = ChargingPhase::Parking;
815 }
816
817 let Some(duration) = event_next
818 .duration_from_start
819 .checked_sub(duration_from_start)
820 else {
821 warn!("TimeDelta overflow");
822 break;
823 };
824
825 let Some(start_date_time) = cdr_start.checked_add_signed(*duration_from_start) else {
826 warn!("TimeDelta overflow");
827 break;
828 };
829
830 let consumed = if let ChargingPhase::Charging = charging_phase {
831 let Some(energy) =
832 Decimal::from(*max_power_supply).checked_mul(duration.to_hours_dec())
833 else {
834 warn!("Decimal overflow");
835 break;
836 };
837 price::Consumed {
838 duration_charging: Some(duration),
839 duration_idle: None,
840 energy: Some(Kwh::from_decimal(energy)),
841 current_max: emit_current,
842 current_min: emit_current,
843 power_max: emit_power,
844 power_min: emit_power,
845 }
846 } else {
847 price::Consumed {
848 duration_charging: None,
849 duration_idle: Some(duration),
850 energy: None,
851 current_max: None,
852 current_min: None,
853 power_max: None,
854 power_min: None,
855 }
856 };
857
858 let period = price::Period {
859 start_date_time,
860 consumed,
861 };
862
863 periods.push(period);
864 }
865
866 periods
867}
868
869fn clamp_date_time_span(
875 min_date: Option<DateTime<Utc>>,
876 max_date: Option<DateTime<Utc>>,
877 span: DateTimeSpan,
878) -> DateTimeSpan {
879 let (min_date, max_date) = (min(min_date, max_date), max(min_date, max_date));
881
882 let start = min_date.filter(|d| &span.start < d).unwrap_or(span.start);
883 let end = max_date.filter(|d| &span.end > d).unwrap_or(span.end);
884
885 DateTimeSpan { start, end }
886}
887
888struct Timeline {
890 events: Vec<Event>,
892
893 emit_current: bool,
895
896 emit_power: bool,
898}
899
900struct Event {
902 duration_from_start: TimeDelta,
904
905 kind: EventKind,
907}
908
909impl fmt::Debug for Event {
910 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
911 f.debug_struct("Event")
912 .field("duration_from_start", &self.duration_from_start.as_hms())
913 .field("kind", &self.kind)
914 .finish()
915 }
916}
917
918#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
920enum EventKind {
921 SessionStart,
927
928 ChargingEnd,
933
934 ParkingEnd {
939 start: TimeDelta,
941 },
942
943 StartTime,
944
945 EndTime,
946
947 MinDuration,
952
953 MaxDuration,
958
959 MinKwh,
961
962 MaxKwh,
964}
965
966impl fmt::Debug for EventKind {
967 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
968 match self {
969 Self::SessionStart => write!(f, "SessionStart"),
970 Self::ChargingEnd => write!(f, "ChargingEnd"),
971 Self::ParkingEnd { start } => f
972 .debug_struct("ParkingEnd")
973 .field("start", &start.as_hms())
974 .finish(),
975 Self::StartTime => write!(f, "StartTime"),
976 Self::EndTime => write!(f, "EndTime"),
977 Self::MinDuration => write!(f, "MinDuration"),
978 Self::MaxDuration => write!(f, "MaxDuration"),
979 Self::MinKwh => write!(f, "MinKwh"),
980 Self::MaxKwh => write!(f, "MaxKwh"),
981 }
982 }
983}
984
985#[derive(Debug)]
987struct Metrics {
988 end_date_time: DateTime<Utc>,
990
991 start_date_time: DateTime<Utc>,
993
994 duration_charging: TimeDelta,
999
1000 duration_parking: Option<TimeDelta>,
1004
1005 energy_supplied: Kwh,
1007
1008 max_current_supply: Ampere,
1010
1011 max_power_supply: Kw,
1013}
1014
1015#[instrument(skip_all)]
1017fn metrics(elem: &tariff::Versioned<'_>, config: &Config) -> Verdict<(Metrics, chrono_tz::Tz)> {
1018 let warnings = warning::Set::new();
1019
1020 let Config {
1021 start_date_time,
1022 end_date_time,
1023 max_power_supply_kw,
1024 requested_kwh: max_energy_battery_kwh,
1025 max_current_supply_amp,
1026 timezone,
1027 } = config;
1028 let duration_session = end_date_time.signed_duration_since(start_date_time);
1029
1030 debug!("duration_session: {}", duration_session.as_hms());
1031
1032 if duration_session.abs() != duration_session {
1034 return warnings.bail(elem.as_element(), Warning::StartDateTimeIsAfterEndDateTime);
1035 }
1036
1037 if duration_session.num_seconds() < MIN_CS_DURATION_SECS {
1038 return warnings.bail(elem.as_element(), Warning::DurationBelowMinimum);
1039 }
1040
1041 if max_energy_battery_kwh.is_zero() {
1042 return warnings.bail(elem.as_element(), Warning::RequestedKwhIsZero);
1043 }
1044
1045 let duration_full_charge = some_dec_or_bail!(
1047 elem,
1048 max_energy_battery_kwh.checked_div(*max_power_supply_kw),
1049 warnings,
1050 "Unable to calculate charging time"
1051 )
1052 .to_duration();
1053 debug!("duration_full_charge: {}", duration_full_charge.as_hms());
1054
1055 let duration_charging = TimeDelta::min(duration_full_charge, duration_session);
1057
1058 let energy_supplied_kwh = some_dec_or_bail!(
1059 elem,
1060 max_power_supply_kw.checked_mul(duration_charging.to_hours_dec()),
1061 warnings,
1062 "Unable to calculate the energy supplied during the charging time"
1063 );
1064
1065 let duration_parking = some_time_delta_or_bail!(
1066 elem,
1067 duration_session.checked_sub(&duration_charging),
1068 warnings,
1069 "Unable to calculate `idle_duration`"
1070 );
1071
1072 debug!(
1073 "duration_charging: {}, duration_parking: {}",
1074 duration_charging.as_hms(),
1075 duration_parking.as_hms()
1076 );
1077
1078 let metrics = Metrics {
1079 end_date_time: *end_date_time,
1080 start_date_time: *start_date_time,
1081 duration_charging,
1082 duration_parking: Some(duration_parking).filter(|dt| dt.num_seconds().is_positive()),
1083 energy_supplied: Kwh::from_decimal(energy_supplied_kwh),
1084 max_current_supply: Ampere::from_decimal(*max_current_supply_amp),
1085 max_power_supply: Kw::from_decimal(*max_power_supply_kw),
1086 };
1087
1088 Ok((metrics, *timezone).into_caveat(warnings))
1089}
1090
1091fn is_tariff_active(cdr_start: &DateTime<Utc>, tariff: &tariff::v221::Tariff<'_>) -> bool {
1092 match (tariff.start_date_time, tariff.end_date_time) {
1093 (None, None) => true,
1094 (None, Some(end)) => (..end).contains(cdr_start),
1095 (Some(start), None) => (start..).contains(cdr_start),
1096 (Some(start), Some(end)) => (start..end).contains(cdr_start),
1097 }
1098}
1099
1100#[derive(Debug)]
1101pub enum Warning {
1103 Decimal(&'static str),
1105
1106 DurationBelowMinimum,
1108
1109 Price(price::Warning),
1111
1112 StartDateTimeIsAfterEndDateTime,
1114
1115 RequestedKwhIsZero,
1117
1118 Tariff(tariff::Warning),
1120
1121 TimeDelta(&'static str),
1123}
1124
1125impl crate::Warning for Warning {
1126 fn id(&self) -> warning::Id {
1127 match self {
1128 Self::Decimal(_) => warning::Id::from_static("decimal_error"),
1129 Self::DurationBelowMinimum => warning::Id::from_static("duration_below_minimum"),
1130 Self::Price(kind) => kind.id(),
1131 Self::StartDateTimeIsAfterEndDateTime => {
1132 warning::Id::from_static("start_time_after_end_time")
1133 }
1134 Self::RequestedKwhIsZero => warning::Id::from_static("requested_kwh_is_zero"),
1135 Self::TimeDelta(_) => warning::Id::from_static("timedelta_error"),
1136 Self::Tariff(kind) => kind.id(),
1137 }
1138 }
1139}
1140
1141impl fmt::Display for Warning {
1142 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1143 match self {
1144 Self::Decimal(msg) | Self::TimeDelta(msg) => f.write_str(msg),
1145 Self::DurationBelowMinimum => write!(
1146 f,
1147 "The duration of the chargesession is below the minimum: {MIN_CS_DURATION_SECS}"
1148 ),
1149 Self::Price(warnings) => {
1150 write!(f, "Price warnings: {warnings:?}")
1151 }
1152 Self::StartDateTimeIsAfterEndDateTime => {
1153 write!(f, "The `start_date_time` is after the `end_date_time`")
1154 }
1155 Self::RequestedKwhIsZero => write!(f, "The `requested_kwh` in the `Config` is zero"),
1156 Self::Tariff(warnings) => {
1157 write!(f, "Tariff warnings: {warnings:?}")
1158 }
1159 }
1160 }
1161}
1162
1163from_warning_all!(
1164 tariff::Warning => Warning::Tariff,
1165 price::Warning => Warning::Price
1166);