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>;
59pub type Caveat<T> = warning::Caveat<T, Warning>;
60
61macro_rules! some_dec_or_bail {
63 ($elem:expr, $opt:expr, $warnings:expr, $msg:literal) => {
64 match $opt {
65 Some(v) => v,
66 None => {
67 return $warnings.bail($elem.as_element(), Warning::Decimal($msg));
68 }
69 }
70 };
71}
72
73macro_rules! some_time_delta_or_bail {
75 ($elem:expr, $opt:expr, $warnings:expr, $msg:literal) => {
76 match $opt {
77 Some(v) => v,
78 None => {
79 return $warnings.bail($elem.as_element(), Warning::TimeDelta($msg));
80 }
81 }
82 };
83}
84
85#[derive(Debug)]
87pub struct Report {
88 pub tariff_id: String,
90
91 pub tariff_currency_code: currency::Code,
93
94 pub partial_cdr: PartialCdr,
101}
102
103#[derive(Debug)]
111pub struct PartialCdr {
112 pub currency_code: currency::Code,
114
115 pub party_id: Option<CpoId>,
123
124 pub start_date_time: DateTime<Utc>,
126
127 pub end_date_time: DateTime<Utc>,
129
130 pub total_energy: Option<Kwh>,
132
133 pub total_charging_duration: Option<TimeDelta>,
137
138 pub total_idle_duration: Option<TimeDelta>,
142
143 pub total_cost: Option<Price>,
145
146 pub total_energy_cost: Option<Price>,
148
149 pub total_fixed_cost: Option<Price>,
151
152 pub total_idle_duration_cost: Option<Price>,
154
155 pub total_charging_duration_cost: Option<Price>,
157
158 pub charging_periods: Vec<ChargingPeriod>,
161}
162
163#[derive(Clone, Debug)]
168pub struct CpoId {
169 pub country_code: country::Code,
171
172 pub id: String,
174}
175
176impl<'buf> From<tariff::CpoId<'buf>> for CpoId {
177 fn from(value: tariff::CpoId<'buf>) -> Self {
178 let tariff::CpoId { country_code, id } = value;
179 CpoId {
180 country_code,
181 id: id.to_string(),
182 }
183 }
184}
185
186impl fmt::Display for CpoId {
188 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
189 write!(f, "{}{}", self.country_code.into_alpha_2_str(), self.id)
190 }
191}
192
193#[derive(Debug)]
197pub struct ChargingPeriod {
198 pub start_date_time: DateTime<Utc>,
201
202 pub dimensions: Vec<Dimension>,
204
205 pub tariff_id: Option<String>,
209}
210
211#[derive(Debug)]
215pub struct Dimension {
216 pub dimension_type: DimensionType,
217
218 pub volume: Decimal,
220}
221
222#[derive(Debug, Clone, PartialEq, Eq)]
226pub enum DimensionType {
227 Energy,
229
230 MaxCurrent,
232
233 MinCurrent,
235
236 MaxPower,
238
239 MinPower,
241
242 ParkingTime,
244
245 ReservationTime,
247
248 Time,
250}
251
252#[derive(Clone)]
254pub struct Config {
255 pub timezone: chrono_tz::Tz,
257
258 pub end_date_time: DateTime<Utc>,
260
261 pub max_current_supply_amp: Decimal,
263
264 pub requested_kwh: Decimal,
269
270 pub max_power_supply_kw: Decimal,
279
280 pub start_date_time: DateTime<Utc>,
282}
283
284pub fn cdr_from_tariff(tariff_elem: &tariff::Versioned<'_>, config: &Config) -> Verdict<Report> {
286 let mut warnings = warning::Set::new();
287 let (metrics, timezone) = metrics(tariff_elem, config)?.gather_warnings_into(&mut warnings);
295
296 let tariff = tariff_elem.to_v221()?.gather_warnings_into(&mut warnings);
297
298 if !is_tariff_active(&metrics.start_date_time, &tariff) {
299 warnings.insert(tariff_elem.as_element(), tariff::Warning::NotActive.into());
300 }
301
302 let timeline = timeline(timezone, &metrics, &tariff);
303 let charging_periods = charge_periods(&metrics, timeline);
304
305 let report = price::periods(metrics.end_date_time, timezone, &tariff, charging_periods)
306 .with_element(tariff_elem.as_element())?
307 .gather_warnings_into(&mut warnings);
308
309 let price::PeriodsReport {
310 billable: _,
311 periods,
312 totals,
313 total_costs,
314 } = report;
315
316 let charging_periods = periods
317 .into_iter()
318 .map(|period| {
319 let price::PeriodReport {
320 start_date_time,
321 end_date_time: _,
322 dimensions,
323 } = period;
324 let duration_charging = dimensions.duration_charging.as_ref().map(|dim| Dimension {
325 dimension_type: DimensionType::Time,
326 volume: ToHoursDecimal::to_hours_dec_in_ocpi_precision(&dim.volume),
327 });
328 let duration_idle = dimensions.duration_idle.as_ref().map(|dim| Dimension {
329 dimension_type: DimensionType::ParkingTime,
330 volume: ToHoursDecimal::to_hours_dec_in_ocpi_precision(&dim.volume),
331 });
332 let energy = dimensions.energy.as_ref().map(|dim| Dimension {
333 dimension_type: DimensionType::Energy,
334 volume: dim.volume.into(),
335 });
336 let dimensions = vec![energy, duration_idle, duration_charging]
337 .into_iter()
338 .flatten()
339 .collect();
340
341 ChargingPeriod {
342 start_date_time,
343 dimensions,
344 tariff_id: Some(tariff.id.to_string()),
345 }
346 })
347 .collect();
348
349 let mut total_cost = total_costs.total();
350
351 if let Some(total_cost) = total_cost.as_mut() {
352 if let Some(min_price) = tariff.min_price {
353 if *total_cost < min_price {
354 *total_cost = min_price;
355 warnings.insert(
356 tariff_elem.as_element(),
357 tariff::Warning::TotalCostClampedToMin.into(),
358 );
359 }
360 }
361
362 if let Some(max_price) = tariff.max_price {
363 if *total_cost > max_price {
364 *total_cost = max_price;
365 warnings.insert(
366 tariff_elem.as_element(),
367 tariff::Warning::TotalCostClampedToMax.into(),
368 );
369 }
370 }
371 }
372
373 let report = Report {
374 tariff_id: tariff.id.to_string(),
375 tariff_currency_code: tariff.currency,
376 partial_cdr: PartialCdr {
377 party_id: tariff.party_id.map(CpoId::from),
378 start_date_time: metrics.start_date_time,
379 end_date_time: metrics.end_date_time,
380 currency_code: tariff.currency,
381 total_energy: totals.energy.round_to_ocpi_scale(),
382 total_charging_duration: totals.duration_charging,
383 total_idle_duration: totals.duration_idle,
384 total_cost: total_cost.round_to_ocpi_scale(),
385 total_energy_cost: total_costs.energy.round_to_ocpi_scale(),
386 total_fixed_cost: total_costs.fixed.round_to_ocpi_scale(),
387 total_idle_duration_cost: total_costs.duration_idle.round_to_ocpi_scale(),
388 total_charging_duration_cost: total_costs.duration_charging.round_to_ocpi_scale(),
389 charging_periods,
390 },
391 };
392
393 Ok(report.into_caveat(warnings))
394}
395
396struct EventCollector {
398 session_duration: TimeDelta,
400
401 events: Vec<Event>,
403}
404
405impl EventCollector {
406 fn with_session_duration(session_duration: TimeDelta) -> Self {
408 Self {
409 session_duration,
410 events: vec![],
411 }
412 }
413
414 fn push(&mut self, duration_from_start: TimeDelta, event_kind: EventKind) {
416 if duration_from_start <= self.session_duration {
417 self.events.push(Event {
418 duration_from_start,
419 kind: event_kind,
420 });
421 }
422 }
423
424 fn into_inner(self) -> Vec<Event> {
426 self.events
427 }
428}
429
430fn timeline(
432 timezone: chrono_tz::Tz,
433 metrics: &Metrics,
434 tariff: &tariff::v221::Tariff<'_>,
435) -> Timeline {
436 let Metrics {
437 start_date_time: cdr_start,
438 end_date_time: cdr_end,
439 duration_charging,
440 duration_parking,
441 max_power_supply,
442 max_current_supply,
443
444 energy_supplied: _,
445 } = metrics;
446
447 let mut events = {
448 let session_duration = duration_parking.map(|d| duration_charging.saturating_add(d));
449 let mut events =
450 EventCollector::with_session_duration(session_duration.unwrap_or(*duration_charging));
451
452 events.push(TimeDelta::seconds(0), EventKind::SessionStart);
453 events.push(*duration_charging, EventKind::ChargingEnd);
454
455 if let Some(dt) = session_duration {
456 events.push(
457 dt,
458 EventKind::ParkingEnd {
459 start: *duration_charging,
460 },
461 );
462 }
463
464 events
465 };
466
467 let mut emit_current = false;
470
471 let mut emit_power = false;
474
475 for elem in &tariff.elements {
476 if elem
479 .restrictions
480 .as_ref()
481 .is_some_and(|r| r.reservation.is_some())
482 {
483 continue;
484 }
485
486 if let Some((time_restrictions, energy_restrictions)) = elem
487 .restrictions
488 .as_ref()
489 .map(tariff::v221::Restrictions::restrictions_by_category)
490 {
491 generate_time_events(
492 &mut events,
493 timezone,
494 *cdr_start..*cdr_end,
495 time_restrictions,
496 );
497
498 let v2x::EnergyRestrictions {
499 min_kwh,
500 max_kwh,
501 min_current,
502 max_current,
503 min_power,
504 max_power,
505 } = energy_restrictions;
506
507 if !emit_current {
508 emit_current = (min_current..=max_current).contains(&Some(*max_current_supply));
513 }
514
515 if !emit_power {
516 emit_power = (min_power..=max_power).contains(&Some(*max_power_supply));
521 }
522
523 generate_energy_events(
524 &mut events,
525 metrics.duration_charging,
526 metrics.energy_supplied,
527 min_kwh,
528 max_kwh,
529 );
530 }
531 }
532
533 let events = events.into_inner();
534
535 Timeline {
536 events,
537 emit_current,
538 emit_power,
539 }
540}
541
542fn generate_time_events(
544 events: &mut EventCollector,
545 timezone: chrono_tz::Tz,
546 cdr_span: DateTimeSpan,
547 restrictions: v2x::TimeRestrictions,
548) {
549 const MIDNIGHT: NaiveTime = NaiveTime::from_hms_opt(0, 0, 0)
550 .expect("The hour, minute and second values are correct and hardcoded");
551 const ONE_DAY: TimeDelta = TimeDelta::days(1);
552
553 let v2x::TimeRestrictions {
554 start_time,
555 end_time,
556 start_date,
557 end_date,
558 min_duration,
559 max_duration,
560 weekdays,
561 } = restrictions;
562
563 let cdr_duration = cdr_span.end.signed_duration_since(cdr_span.start);
564
565 if let Some(dt) = min_duration {
566 if cdr_duration > dt {
567 events.push(dt, EventKind::MinDuration);
568 }
569 }
570
571 if let Some(dt) = max_duration {
572 if cdr_duration > dt {
573 events.push(dt, EventKind::MaxDuration);
574 }
575 }
576
577 let (start_date_time, end_date_time) =
587 if let (Some(start_time), Some(end_time)) = (start_time, end_time) {
588 if end_time < start_time {
589 (
590 start_date.map(|d| d.and_time(start_time)),
591 end_date.map(|d| {
592 let (end_time, _) = end_time.overflowing_add_signed(ONE_DAY);
593 d.and_time(end_time)
594 }),
595 )
596 } else {
597 (
598 start_date.map(|d| d.and_time(start_time)),
599 end_date.map(|d| d.and_time(end_time)),
600 )
601 }
602 } else {
603 (
604 start_date.map(|d| d.and_time(start_time.unwrap_or(MIDNIGHT))),
605 end_date.map(|d| d.and_time(end_time.unwrap_or(MIDNIGHT))),
606 )
607 };
608
609 let event_span = clamp_date_time_span(
612 start_date_time.and_then(|d| local_to_utc(timezone, d)),
613 end_date_time.and_then(|d| local_to_utc(timezone, d)),
614 cdr_span,
615 );
616
617 if let Some(start_time) = start_time {
618 gen_naive_time_events(
619 events,
620 &event_span,
621 timezone,
622 start_time,
623 &weekdays,
624 EventKind::StartTime,
625 );
626 }
627
628 if let Some(end_time) = end_time {
629 gen_naive_time_events(
630 events,
631 &event_span,
632 timezone,
633 end_time,
634 &weekdays,
635 EventKind::EndTime,
636 );
637 }
638}
639
640fn local_to_utc(timezone: chrono_tz::Tz, date_time: NaiveDateTime) -> Option<DateTime<Utc>> {
646 use chrono::offset::LocalResult;
647
648 let result = date_time.and_local_timezone(timezone);
649
650 let local_date_time = match result {
651 LocalResult::Single(d) => d,
652 LocalResult::Ambiguous(earliest, _latest) => earliest,
653 LocalResult::None => return None,
654 };
655
656 Some(local_date_time.to_utc())
657}
658
659fn gen_naive_time_events(
661 events: &mut EventCollector,
662 event_span: &Range<DateTime<Utc>>,
663 timezone: chrono_tz::Tz,
664 time: NaiveTime,
665 weekdays: &v2x::WeekdaySet,
666 kind: EventKind,
667) {
668 let local_start_time = event_span.start.with_timezone(&timezone).time();
669 let time_delta = time.signed_duration_since(local_start_time);
670 let cdr_duration = event_span.end.signed_duration_since(event_span.start);
671
672 let time_delta = if time_delta.num_seconds().is_negative() {
674 time_delta.saturating_add(TimeDelta::days(1))
675 } else {
676 time_delta
677 };
678
679 if time_delta.num_seconds().is_negative() {
681 return;
682 }
683
684 let Some(remainder) = cdr_duration.checked_sub(&time_delta) else {
686 warn!("TimeDelta overflow");
687 return;
688 };
689
690 if remainder.num_seconds().is_positive() {
691 let duration_from_start = time_delta;
692 let Some(date) = event_span.start.checked_add_signed(duration_from_start) else {
693 warn!("Date out of range");
694 return;
695 };
696
697 if weekdays.contains(date.weekday()) {
698 events.push(time_delta, kind);
700 }
701
702 for day in 1..=remainder.num_days() {
703 let Some(duration_from_start) = time_delta.checked_add(&TimeDelta::days(day)) else {
704 warn!("Date out of range");
705 break;
706 };
707 let Some(date) = event_span.start.checked_add_signed(duration_from_start) else {
708 warn!("Date out of range");
709 break;
710 };
711
712 if weekdays.contains(date.weekday()) {
713 events.push(duration_from_start, kind);
714 }
715 }
716 }
717}
718
719fn generate_energy_events(
721 events: &mut EventCollector,
722 duration_charging: TimeDelta,
723 energy_supplied: Kwh,
724 min_kwh: Option<Kwh>,
725 max_kwh: Option<Kwh>,
726) {
727 if let Some(dt) = min_kwh.and_then(|kwh| power_to_time(kwh, energy_supplied, duration_charging))
728 {
729 events.push(dt, EventKind::MinKwh);
730 }
731
732 if let Some(dt) = max_kwh.and_then(|kwh| power_to_time(kwh, energy_supplied, duration_charging))
733 {
734 events.push(dt, EventKind::MaxKwh);
735 }
736}
737
738#[instrument]
740fn power_to_time(power: Kwh, power_total: Kwh, duration_total: TimeDelta) -> Option<TimeDelta> {
741 if power == power_total {
744 return Some(duration_total);
745 }
746
747 let power = Decimal::from(power);
750 let power_total = Decimal::from(power_total);
752
753 let Some(factor) = power.checked_div(power_total) else {
755 return Some(TimeDelta::zero());
756 };
757
758 if factor.is_sign_negative() || factor > dec!(1.0) {
759 return None;
760 }
761
762 let hours_dec = duration_total.to_hours_dec();
763 let duration_from_start = factor.checked_mul(hours_dec)?;
764 Some(duration_from_start.to_duration())
765}
766
767fn charge_periods(metrics: &Metrics, timeline: Timeline) -> Vec<price::Period> {
769 enum ChargingPhase {
771 Charging,
772 Parking,
773 }
774
775 let Metrics {
776 start_date_time: cdr_start,
777 max_power_supply,
778 max_current_supply,
779
780 end_date_time: _,
781 duration_charging: _,
782 duration_parking: _,
783 energy_supplied: _,
784 } = metrics;
785
786 let Timeline {
787 mut events,
788 emit_current,
789 emit_power,
790 } = timeline;
791
792 events.sort_unstable_by_key(|e| e.duration_from_start);
793
794 let mut periods = vec![];
795 let emit_current = emit_current.then_some(*max_current_supply);
796 let emit_power = emit_power.then_some(*max_power_supply);
797 let mut charging_phase = ChargingPhase::Charging;
799
800 for items in events.windows(2) {
801 let [event, event_next] = items else {
802 unreachable!("The window size is 2");
803 };
804
805 let Event {
806 duration_from_start,
807 kind,
808 } = event;
809
810 if let EventKind::ChargingEnd = kind {
811 charging_phase = ChargingPhase::Parking;
812 }
813
814 let Some(duration) = event_next
815 .duration_from_start
816 .checked_sub(duration_from_start)
817 else {
818 warn!("TimeDelta overflow");
819 break;
820 };
821
822 let Some(start_date_time) = cdr_start.checked_add_signed(*duration_from_start) else {
823 warn!("TimeDelta overflow");
824 break;
825 };
826
827 let consumed = if let ChargingPhase::Charging = charging_phase {
828 let Some(energy) =
829 Decimal::from(*max_power_supply).checked_mul(duration.to_hours_dec())
830 else {
831 warn!("Decimal overflow");
832 break;
833 };
834 price::Consumed {
835 duration_charging: Some(duration),
836 duration_idle: None,
837 energy: Some(Kwh::from_decimal(energy)),
838 current_max: emit_current,
839 current_min: emit_current,
840 power_max: emit_power,
841 power_min: emit_power,
842 }
843 } else {
844 price::Consumed {
845 duration_charging: None,
846 duration_idle: Some(duration),
847 energy: None,
848 current_max: None,
849 current_min: None,
850 power_max: None,
851 power_min: None,
852 }
853 };
854
855 let period = price::Period {
856 start_date_time,
857 consumed,
858 };
859
860 periods.push(period);
861 }
862
863 periods
864}
865
866fn clamp_date_time_span(
872 min_date: Option<DateTime<Utc>>,
873 max_date: Option<DateTime<Utc>>,
874 span: DateTimeSpan,
875) -> DateTimeSpan {
876 let (min_date, max_date) = (min(min_date, max_date), max(min_date, max_date));
878
879 let start = min_date.filter(|d| &span.start < d).unwrap_or(span.start);
880 let end = max_date.filter(|d| &span.end > d).unwrap_or(span.end);
881
882 DateTimeSpan { start, end }
883}
884
885struct Timeline {
887 events: Vec<Event>,
889
890 emit_current: bool,
892
893 emit_power: bool,
895}
896
897struct Event {
899 duration_from_start: TimeDelta,
901
902 kind: EventKind,
904}
905
906impl fmt::Debug for Event {
907 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
908 f.debug_struct("Event")
909 .field("duration_from_start", &self.duration_from_start.as_hms())
910 .field("kind", &self.kind)
911 .finish()
912 }
913}
914
915#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
917enum EventKind {
918 SessionStart,
924
925 ChargingEnd,
930
931 ParkingEnd {
936 start: TimeDelta,
938 },
939
940 StartTime,
941
942 EndTime,
943
944 MinDuration,
949
950 MaxDuration,
955
956 MinKwh,
958
959 MaxKwh,
961}
962
963impl fmt::Debug for EventKind {
964 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
965 match self {
966 Self::SessionStart => write!(f, "SessionStart"),
967 Self::ChargingEnd => write!(f, "ChargingEnd"),
968 Self::ParkingEnd { start } => f
969 .debug_struct("ParkingEnd")
970 .field("start", &start.as_hms())
971 .finish(),
972 Self::StartTime => write!(f, "StartTime"),
973 Self::EndTime => write!(f, "EndTime"),
974 Self::MinDuration => write!(f, "MinDuration"),
975 Self::MaxDuration => write!(f, "MaxDuration"),
976 Self::MinKwh => write!(f, "MinKwh"),
977 Self::MaxKwh => write!(f, "MaxKwh"),
978 }
979 }
980}
981
982#[derive(Debug)]
984struct Metrics {
985 end_date_time: DateTime<Utc>,
987
988 start_date_time: DateTime<Utc>,
990
991 duration_charging: TimeDelta,
996
997 duration_parking: Option<TimeDelta>,
1001
1002 energy_supplied: Kwh,
1004
1005 max_current_supply: Ampere,
1007
1008 max_power_supply: Kw,
1010}
1011
1012#[instrument(skip_all)]
1014fn metrics(elem: &tariff::Versioned<'_>, config: &Config) -> Verdict<(Metrics, chrono_tz::Tz)> {
1015 let warnings = warning::Set::new();
1016
1017 let Config {
1018 start_date_time,
1019 end_date_time,
1020 max_power_supply_kw,
1021 requested_kwh: max_energy_battery_kwh,
1022 max_current_supply_amp,
1023 timezone,
1024 } = config;
1025 let duration_session = end_date_time.signed_duration_since(start_date_time);
1026
1027 debug!("duration_session: {}", duration_session.as_hms());
1028
1029 if duration_session.abs() != duration_session {
1031 return warnings.bail(elem.as_element(), Warning::StartDateTimeIsAfterEndDateTime);
1032 }
1033
1034 if duration_session.num_seconds() < MIN_CS_DURATION_SECS {
1035 return warnings.bail(elem.as_element(), Warning::DurationBelowMinimum);
1036 }
1037
1038 if max_energy_battery_kwh.is_zero() {
1039 return warnings.bail(elem.as_element(), Warning::RequestedKwhIsZero);
1040 }
1041
1042 let duration_full_charge = some_dec_or_bail!(
1044 elem,
1045 max_energy_battery_kwh.checked_div(*max_power_supply_kw),
1046 warnings,
1047 "Unable to calculate charging time"
1048 )
1049 .to_duration();
1050 debug!("duration_full_charge: {}", duration_full_charge.as_hms());
1051
1052 let duration_charging = TimeDelta::min(duration_full_charge, duration_session);
1054
1055 let energy_supplied_kwh = some_dec_or_bail!(
1056 elem,
1057 max_power_supply_kw.checked_mul(duration_charging.to_hours_dec()),
1058 warnings,
1059 "Unable to calculate the energy supplied during the charging time"
1060 );
1061
1062 let duration_parking = some_time_delta_or_bail!(
1063 elem,
1064 duration_session.checked_sub(&duration_charging),
1065 warnings,
1066 "Unable to calculate `idle_duration`"
1067 );
1068
1069 debug!(
1070 "duration_charging: {}, duration_parking: {}",
1071 duration_charging.as_hms(),
1072 duration_parking.as_hms()
1073 );
1074
1075 let metrics = Metrics {
1076 end_date_time: *end_date_time,
1077 start_date_time: *start_date_time,
1078 duration_charging,
1079 duration_parking: Some(duration_parking).filter(|dt| dt.num_seconds().is_positive()),
1080 energy_supplied: Kwh::from_decimal(energy_supplied_kwh),
1081 max_current_supply: Ampere::from_decimal(*max_current_supply_amp),
1082 max_power_supply: Kw::from_decimal(*max_power_supply_kw),
1083 };
1084
1085 Ok((metrics, *timezone).into_caveat(warnings))
1086}
1087
1088fn is_tariff_active(cdr_start: &DateTime<Utc>, tariff: &tariff::v221::Tariff<'_>) -> bool {
1089 match (tariff.start_date_time, tariff.end_date_time) {
1090 (None, None) => true,
1091 (None, Some(end)) => (..end).contains(cdr_start),
1092 (Some(start), None) => (start..).contains(cdr_start),
1093 (Some(start), Some(end)) => (start..end).contains(cdr_start),
1094 }
1095}
1096
1097#[derive(Debug)]
1098pub enum Warning {
1099 Decimal(&'static str),
1101
1102 DurationBelowMinimum,
1104
1105 Price(price::Warning),
1106
1107 StartDateTimeIsAfterEndDateTime,
1109
1110 RequestedKwhIsZero,
1112
1113 Tariff(tariff::Warning),
1114
1115 TimeDelta(&'static str),
1117}
1118
1119impl crate::Warning for Warning {
1120 fn id(&self) -> warning::Id {
1121 match self {
1122 Self::Decimal(_) => warning::Id::from_static("decimal_error"),
1123 Self::DurationBelowMinimum => warning::Id::from_static("duration_below_minimum"),
1124 Self::Price(kind) => kind.id(),
1125 Self::StartDateTimeIsAfterEndDateTime => {
1126 warning::Id::from_static("start_time_after_end_time")
1127 }
1128 Self::RequestedKwhIsZero => warning::Id::from_static("requested_kwh_is_zero"),
1129 Self::TimeDelta(_) => warning::Id::from_static("timedelta_error"),
1130 Self::Tariff(kind) => kind.id(),
1131 }
1132 }
1133}
1134
1135impl fmt::Display for Warning {
1136 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1137 match self {
1138 Self::Decimal(msg) | Self::TimeDelta(msg) => f.write_str(msg),
1139 Self::DurationBelowMinimum => write!(
1140 f,
1141 "The duration of the chargesession is below the minimum: {MIN_CS_DURATION_SECS}"
1142 ),
1143 Self::Price(warnings) => {
1144 write!(f, "Price warnings: {warnings:?}")
1145 }
1146 Self::StartDateTimeIsAfterEndDateTime => {
1147 write!(f, "The `start_date_time` is after the `end_date_time`")
1148 }
1149 Self::RequestedKwhIsZero => write!(f, "The `requested_kwh` in the `Config` is zero"),
1150 Self::Tariff(warnings) => {
1151 write!(f, "Tariff warnings: {warnings:?}")
1152 }
1153 }
1154 }
1155}
1156
1157from_warning_all!(
1158 tariff::Warning => Warning::Tariff,
1159 price::Warning => Warning::Price
1160);