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 json::FromJson as _,
49 number::{FromDecimal as _, RoundDecimal as _},
50 price, tariff,
51 warning::{self, GatherWarnings as _, IntoCaveat as _, WithElement as _},
52 Price, SaturatingAdd as _, ToDuration as _, Version, Versioned as _,
53};
54
55const MIN_CS_DURATION_SECS: i64 = 120;
57
58type DateTimeSpan = Range<DateTime<Utc>>;
59pub type Verdict<T> = crate::Verdict<T, Warning>;
60pub type Caveat<T> = warning::Caveat<T, Warning>;
61
62macro_rules! some_dec_or_bail {
64 ($elem:expr, $opt:expr, $warnings:expr, $msg:literal) => {
65 match $opt {
66 Some(v) => v,
67 None => {
68 return $warnings.bail($elem.as_element(), Warning::Decimal($msg));
69 }
70 }
71 };
72}
73
74macro_rules! some_time_delta_or_bail {
76 ($elem:expr, $opt:expr, $warnings:expr, $msg:literal) => {
77 match $opt {
78 Some(v) => v,
79 None => {
80 return $warnings.bail($elem.as_element(), Warning::TimeDelta($msg));
81 }
82 }
83 };
84}
85
86#[derive(Debug)]
88pub struct Report {
89 pub tariff_id: String,
91
92 pub tariff_currency_code: currency::Code,
94
95 pub partial_cdr: PartialCdr,
102}
103
104#[derive(Debug)]
112pub struct PartialCdr {
113 pub currency_code: currency::Code,
115
116 pub party_id: Option<CpoId>,
124
125 pub start_date_time: DateTime<Utc>,
127
128 pub end_date_time: DateTime<Utc>,
130
131 pub total_energy: Option<Kwh>,
133
134 pub total_charging_duration: Option<TimeDelta>,
138
139 pub total_idle_duration: Option<TimeDelta>,
143
144 pub total_cost: Option<Price>,
146
147 pub total_energy_cost: Option<Price>,
149
150 pub total_fixed_cost: Option<Price>,
152
153 pub total_idle_duration_cost: Option<Price>,
155
156 pub total_charging_duration_cost: Option<Price>,
158
159 pub charging_periods: Vec<ChargingPeriod>,
162}
163
164#[derive(Clone, Debug)]
169pub struct CpoId {
170 pub country_code: country::Code,
172
173 pub id: String,
175}
176
177impl<'buf> From<tariff::CpoId<'buf>> for CpoId {
178 fn from(value: tariff::CpoId<'buf>) -> Self {
179 let tariff::CpoId { country_code, id } = value;
180 CpoId {
181 country_code,
182 id: id.to_string(),
183 }
184 }
185}
186
187impl fmt::Display for CpoId {
189 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
190 write!(f, "{}{}", self.country_code.into_alpha_2_str(), self.id)
191 }
192}
193
194#[derive(Debug)]
198pub struct ChargingPeriod {
199 pub start_date_time: DateTime<Utc>,
202
203 pub dimensions: Vec<Dimension>,
205
206 pub tariff_id: Option<String>,
210}
211
212#[derive(Debug)]
216pub struct Dimension {
217 pub dimension_type: DimensionType,
218
219 pub volume: Decimal,
221}
222
223#[derive(Debug, Clone, PartialEq, Eq)]
227pub enum DimensionType {
228 Energy,
230
231 MaxCurrent,
233
234 MinCurrent,
236
237 MaxPower,
239
240 MinPower,
242
243 ParkingTime,
245
246 ReservationTime,
248
249 Time,
251}
252
253#[derive(Clone)]
255pub struct Config {
256 pub timezone: chrono_tz::Tz,
258
259 pub end_date_time: DateTime<Utc>,
261
262 pub max_current_supply_amp: Decimal,
264
265 pub requested_kwh: Decimal,
270
271 pub max_power_supply_kw: Decimal,
280
281 pub start_date_time: DateTime<Utc>,
283}
284
285pub fn cdr_from_tariff(tariff_elem: &tariff::Versioned<'_>, config: &Config) -> Verdict<Report> {
287 let mut warnings = warning::Set::new();
288 let (metrics, timezone) = metrics(tariff_elem, config)?.gather_warnings_into(&mut warnings);
296
297 let tariff = match tariff_elem.version() {
298 Version::V211 => {
299 let tariff = tariff::v211::Tariff::from_json(tariff_elem.as_element())?
300 .gather_warnings_into(&mut warnings);
301
302 tariff::v221::Tariff::from(tariff)
303 }
304 Version::V221 => tariff::v221::Tariff::from_json(tariff_elem.as_element())?
305 .gather_warnings_into(&mut warnings),
306 };
307
308 if !is_tariff_active(&metrics.start_date_time, &tariff) {
309 warnings.insert(tariff_elem.as_element(), tariff::Warning::NotActive.into());
310 }
311
312 let timeline = timeline(timezone, &metrics, &tariff);
313 let charging_periods = charge_periods(&metrics, timeline);
314
315 let report = price::periods(metrics.end_date_time, timezone, &tariff, charging_periods)
316 .with_element(tariff_elem.as_element())?
317 .gather_warnings_into(&mut warnings);
318
319 let price::PeriodsReport {
320 billable: _,
321 periods,
322 totals,
323 total_costs,
324 } = report;
325
326 let charging_periods = periods
327 .into_iter()
328 .map(|period| {
329 let price::PeriodReport {
330 start_date_time,
331 end_date_time: _,
332 dimensions,
333 } = period;
334 let duration_charging = dimensions.duration_charging.as_ref().map(|dim| Dimension {
335 dimension_type: DimensionType::Time,
336 volume: ToHoursDecimal::to_hours_dec_in_ocpi_precision(&dim.volume),
337 });
338 let duration_idle = dimensions.duration_idle.as_ref().map(|dim| Dimension {
339 dimension_type: DimensionType::ParkingTime,
340 volume: ToHoursDecimal::to_hours_dec_in_ocpi_precision(&dim.volume),
341 });
342 let energy = dimensions.energy.as_ref().map(|dim| Dimension {
343 dimension_type: DimensionType::Energy,
344 volume: dim.volume.into(),
345 });
346 let dimensions = vec![energy, duration_idle, duration_charging]
347 .into_iter()
348 .flatten()
349 .collect();
350
351 ChargingPeriod {
352 start_date_time,
353 dimensions,
354 tariff_id: Some(tariff.id.to_string()),
355 }
356 })
357 .collect();
358
359 let mut total_cost = total_costs.total();
360
361 if let Some(total_cost) = total_cost.as_mut() {
362 if let Some(min_price) = tariff.min_price {
363 if *total_cost < min_price {
364 *total_cost = min_price;
365 warnings.insert(
366 tariff_elem.as_element(),
367 tariff::Warning::TotalCostClampedToMin.into(),
368 );
369 }
370 }
371
372 if let Some(max_price) = tariff.max_price {
373 if *total_cost > max_price {
374 *total_cost = max_price;
375 warnings.insert(
376 tariff_elem.as_element(),
377 tariff::Warning::TotalCostClampedToMax.into(),
378 );
379 }
380 }
381 }
382
383 let report = Report {
384 tariff_id: tariff.id.to_string(),
385 tariff_currency_code: tariff.currency,
386 partial_cdr: PartialCdr {
387 party_id: tariff.party_id.map(CpoId::from),
388 start_date_time: metrics.start_date_time,
389 end_date_time: metrics.end_date_time,
390 currency_code: tariff.currency,
391 total_energy: totals.energy.round_to_ocpi_scale(),
392 total_charging_duration: totals.duration_charging,
393 total_idle_duration: totals.duration_idle,
394 total_cost: total_cost.round_to_ocpi_scale(),
395 total_energy_cost: total_costs.energy.round_to_ocpi_scale(),
396 total_fixed_cost: total_costs.fixed.round_to_ocpi_scale(),
397 total_idle_duration_cost: total_costs.duration_idle.round_to_ocpi_scale(),
398 total_charging_duration_cost: total_costs.duration_charging.round_to_ocpi_scale(),
399 charging_periods,
400 },
401 };
402
403 Ok(report.into_caveat(warnings))
404}
405
406struct EventCollector {
408 session_duration: TimeDelta,
410
411 events: Vec<Event>,
413}
414
415impl EventCollector {
416 fn with_session_duration(session_duration: TimeDelta) -> Self {
418 Self {
419 session_duration,
420 events: vec![],
421 }
422 }
423
424 fn push(&mut self, duration_from_start: TimeDelta, event_kind: EventKind) {
426 if duration_from_start <= self.session_duration {
427 self.events.push(Event {
428 duration_from_start,
429 kind: event_kind,
430 });
431 }
432 }
433
434 fn into_inner(self) -> Vec<Event> {
436 self.events
437 }
438}
439
440fn timeline(
442 timezone: chrono_tz::Tz,
443 metrics: &Metrics,
444 tariff: &tariff::v221::Tariff<'_>,
445) -> Timeline {
446 let Metrics {
447 start_date_time: cdr_start,
448 end_date_time: cdr_end,
449 duration_charging,
450 duration_parking,
451 max_power_supply,
452 max_current_supply,
453
454 energy_supplied: _,
455 } = metrics;
456
457 let mut events = {
458 let session_duration = duration_parking.map(|d| duration_charging.saturating_add(d));
459 let mut events =
460 EventCollector::with_session_duration(session_duration.unwrap_or(*duration_charging));
461
462 events.push(TimeDelta::seconds(0), EventKind::SessionStart);
463 events.push(*duration_charging, EventKind::ChargingEnd);
464
465 if let Some(dt) = session_duration {
466 events.push(
467 dt,
468 EventKind::ParkingEnd {
469 start: *duration_charging,
470 },
471 );
472 }
473
474 events
475 };
476
477 let mut emit_current = false;
480
481 let mut emit_power = false;
484
485 for elem in &tariff.elements {
486 if elem
489 .restrictions
490 .as_ref()
491 .is_some_and(|r| r.reservation.is_some())
492 {
493 continue;
494 }
495
496 if let Some((time_restrictions, energy_restrictions)) = elem
497 .restrictions
498 .as_ref()
499 .map(tariff::v221::Restrictions::restrictions_by_category)
500 {
501 generate_time_events(
502 &mut events,
503 timezone,
504 *cdr_start..*cdr_end,
505 time_restrictions,
506 );
507
508 let v2x::EnergyRestrictions {
509 min_kwh,
510 max_kwh,
511 min_current,
512 max_current,
513 min_power,
514 max_power,
515 } = energy_restrictions;
516
517 if !emit_current {
518 emit_current = (min_current..=max_current).contains(&Some(*max_current_supply));
523 }
524
525 if !emit_power {
526 emit_power = (min_power..=max_power).contains(&Some(*max_power_supply));
531 }
532
533 generate_energy_events(
534 &mut events,
535 metrics.duration_charging,
536 metrics.energy_supplied,
537 min_kwh,
538 max_kwh,
539 );
540 }
541 }
542
543 let events = events.into_inner();
544
545 Timeline {
546 events,
547 emit_current,
548 emit_power,
549 }
550}
551
552fn generate_time_events(
554 events: &mut EventCollector,
555 timezone: chrono_tz::Tz,
556 cdr_span: DateTimeSpan,
557 restrictions: v2x::TimeRestrictions,
558) {
559 const MIDNIGHT: NaiveTime = NaiveTime::from_hms_opt(0, 0, 0)
560 .expect("The hour, minute and second values are correct and hardcoded");
561 const ONE_DAY: TimeDelta = TimeDelta::days(1);
562
563 let v2x::TimeRestrictions {
564 start_time,
565 end_time,
566 start_date,
567 end_date,
568 min_duration,
569 max_duration,
570 weekdays,
571 } = restrictions;
572
573 let cdr_duration = cdr_span.end.signed_duration_since(cdr_span.start);
574
575 if let Some(dt) = min_duration {
576 if cdr_duration > dt {
577 events.push(dt, EventKind::MinDuration);
578 }
579 }
580
581 if let Some(dt) = max_duration {
582 if cdr_duration > dt {
583 events.push(dt, EventKind::MaxDuration);
584 }
585 }
586
587 let (start_date_time, end_date_time) =
597 if let (Some(start_time), Some(end_time)) = (start_time, end_time) {
598 if end_time < start_time {
599 (
600 start_date.map(|d| d.and_time(start_time)),
601 end_date.map(|d| {
602 let (end_time, _) = end_time.overflowing_add_signed(ONE_DAY);
603 d.and_time(end_time)
604 }),
605 )
606 } else {
607 (
608 start_date.map(|d| d.and_time(start_time)),
609 end_date.map(|d| d.and_time(end_time)),
610 )
611 }
612 } else {
613 (
614 start_date.map(|d| d.and_time(start_time.unwrap_or(MIDNIGHT))),
615 end_date.map(|d| d.and_time(end_time.unwrap_or(MIDNIGHT))),
616 )
617 };
618
619 let event_span = clamp_date_time_span(
622 start_date_time.and_then(|d| local_to_utc(timezone, d)),
623 end_date_time.and_then(|d| local_to_utc(timezone, d)),
624 cdr_span,
625 );
626
627 if let Some(start_time) = start_time {
628 gen_naive_time_events(
629 events,
630 &event_span,
631 timezone,
632 start_time,
633 &weekdays,
634 EventKind::StartTime,
635 );
636 }
637
638 if let Some(end_time) = end_time {
639 gen_naive_time_events(
640 events,
641 &event_span,
642 timezone,
643 end_time,
644 &weekdays,
645 EventKind::EndTime,
646 );
647 }
648}
649
650fn local_to_utc(timezone: chrono_tz::Tz, date_time: NaiveDateTime) -> Option<DateTime<Utc>> {
656 use chrono::offset::LocalResult;
657
658 let result = date_time.and_local_timezone(timezone);
659
660 let local_date_time = match result {
661 LocalResult::Single(d) => d,
662 LocalResult::Ambiguous(earliest, _latest) => earliest,
663 LocalResult::None => return None,
664 };
665
666 Some(local_date_time.to_utc())
667}
668
669fn gen_naive_time_events(
671 events: &mut EventCollector,
672 event_span: &Range<DateTime<Utc>>,
673 timezone: chrono_tz::Tz,
674 time: NaiveTime,
675 weekdays: &v2x::WeekdaySet,
676 kind: EventKind,
677) {
678 let local_start_time = event_span.start.with_timezone(&timezone).time();
679 let time_delta = time.signed_duration_since(local_start_time);
680 let cdr_duration = event_span.end.signed_duration_since(event_span.start);
681
682 let time_delta = if time_delta.num_seconds().is_negative() {
684 time_delta.saturating_add(TimeDelta::days(1))
685 } else {
686 time_delta
687 };
688
689 if time_delta.num_seconds().is_negative() {
691 return;
692 }
693
694 let Some(remainder) = cdr_duration.checked_sub(&time_delta) else {
696 warn!("TimeDelta overflow");
697 return;
698 };
699
700 if remainder.num_seconds().is_positive() {
701 let duration_from_start = time_delta;
702 let Some(date) = event_span.start.checked_add_signed(duration_from_start) else {
703 warn!("Date out of range");
704 return;
705 };
706
707 if weekdays.contains(date.weekday()) {
708 events.push(time_delta, kind);
710 }
711
712 for day in 1..=remainder.num_days() {
713 let Some(duration_from_start) = time_delta.checked_add(&TimeDelta::days(day)) else {
714 warn!("Date out of range");
715 break;
716 };
717 let Some(date) = event_span.start.checked_add_signed(duration_from_start) else {
718 warn!("Date out of range");
719 break;
720 };
721
722 if weekdays.contains(date.weekday()) {
723 events.push(duration_from_start, kind);
724 }
725 }
726 }
727}
728
729fn generate_energy_events(
731 events: &mut EventCollector,
732 duration_charging: TimeDelta,
733 energy_supplied: Kwh,
734 min_kwh: Option<Kwh>,
735 max_kwh: Option<Kwh>,
736) {
737 if let Some(dt) = min_kwh.and_then(|kwh| power_to_time(kwh, energy_supplied, duration_charging))
738 {
739 events.push(dt, EventKind::MinKwh);
740 }
741
742 if let Some(dt) = max_kwh.and_then(|kwh| power_to_time(kwh, energy_supplied, duration_charging))
743 {
744 events.push(dt, EventKind::MaxKwh);
745 }
746}
747
748#[instrument]
750fn power_to_time(power: Kwh, power_total: Kwh, duration_total: TimeDelta) -> Option<TimeDelta> {
751 if power == power_total {
754 return Some(duration_total);
755 }
756
757 let power = Decimal::from(power);
760 let power_total = Decimal::from(power_total);
762
763 let Some(factor) = power.checked_div(power_total) else {
765 return Some(TimeDelta::zero());
766 };
767
768 if factor.is_sign_negative() || factor > dec!(1.0) {
769 return None;
770 }
771
772 let hours_dec = duration_total.to_hours_dec();
773 let duration_from_start = factor.checked_mul(hours_dec)?;
774 Some(duration_from_start.to_duration())
775}
776
777fn charge_periods(metrics: &Metrics, timeline: Timeline) -> Vec<price::Period> {
779 enum ChargingPhase {
781 Charging,
782 Parking,
783 }
784
785 let Metrics {
786 start_date_time: cdr_start,
787 max_power_supply,
788 max_current_supply,
789
790 end_date_time: _,
791 duration_charging: _,
792 duration_parking: _,
793 energy_supplied: _,
794 } = metrics;
795
796 let Timeline {
797 mut events,
798 emit_current,
799 emit_power,
800 } = timeline;
801
802 events.sort_unstable_by_key(|e| e.duration_from_start);
803
804 let mut periods = vec![];
805 let emit_current = emit_current.then_some(*max_current_supply);
806 let emit_power = emit_power.then_some(*max_power_supply);
807 let mut charging_phase = ChargingPhase::Charging;
809
810 for items in events.windows(2) {
811 let [event, event_next] = items else {
812 unreachable!("The window size is 2");
813 };
814
815 let Event {
816 duration_from_start,
817 kind,
818 } = event;
819
820 if let EventKind::ChargingEnd = kind {
821 charging_phase = ChargingPhase::Parking;
822 }
823
824 let Some(duration) = event_next
825 .duration_from_start
826 .checked_sub(duration_from_start)
827 else {
828 warn!("TimeDelta overflow");
829 break;
830 };
831
832 let Some(start_date_time) = cdr_start.checked_add_signed(*duration_from_start) else {
833 warn!("TimeDelta overflow");
834 break;
835 };
836
837 let consumed = if let ChargingPhase::Charging = charging_phase {
838 let Some(energy) =
839 Decimal::from(*max_power_supply).checked_mul(duration.to_hours_dec())
840 else {
841 warn!("Decimal overflow");
842 break;
843 };
844 price::Consumed {
845 duration_charging: Some(duration),
846 duration_idle: None,
847 energy: Some(Kwh::from_decimal(energy)),
848 current_max: emit_current,
849 current_min: emit_current,
850 power_max: emit_power,
851 power_min: emit_power,
852 }
853 } else {
854 price::Consumed {
855 duration_charging: None,
856 duration_idle: Some(duration),
857 energy: None,
858 current_max: None,
859 current_min: None,
860 power_max: None,
861 power_min: None,
862 }
863 };
864
865 let period = price::Period {
866 start_date_time,
867 consumed,
868 };
869
870 periods.push(period);
871 }
872
873 periods
874}
875
876fn clamp_date_time_span(
882 min_date: Option<DateTime<Utc>>,
883 max_date: Option<DateTime<Utc>>,
884 span: DateTimeSpan,
885) -> DateTimeSpan {
886 let (min_date, max_date) = (min(min_date, max_date), max(min_date, max_date));
888
889 let start = min_date.filter(|d| &span.start < d).unwrap_or(span.start);
890 let end = max_date.filter(|d| &span.end > d).unwrap_or(span.end);
891
892 DateTimeSpan { start, end }
893}
894
895struct Timeline {
897 events: Vec<Event>,
899
900 emit_current: bool,
902
903 emit_power: bool,
905}
906
907struct Event {
909 duration_from_start: TimeDelta,
911
912 kind: EventKind,
914}
915
916impl fmt::Debug for Event {
917 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
918 f.debug_struct("Event")
919 .field("duration_from_start", &self.duration_from_start.as_hms())
920 .field("kind", &self.kind)
921 .finish()
922 }
923}
924
925#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
927enum EventKind {
928 SessionStart,
934
935 ChargingEnd,
940
941 ParkingEnd {
946 start: TimeDelta,
948 },
949
950 StartTime,
951
952 EndTime,
953
954 MinDuration,
959
960 MaxDuration,
965
966 MinKwh,
968
969 MaxKwh,
971}
972
973impl fmt::Debug for EventKind {
974 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
975 match self {
976 Self::SessionStart => write!(f, "SessionStart"),
977 Self::ChargingEnd => write!(f, "ChargingEnd"),
978 Self::ParkingEnd { start } => f
979 .debug_struct("ParkingEnd")
980 .field("start", &start.as_hms())
981 .finish(),
982 Self::StartTime => write!(f, "StartTime"),
983 Self::EndTime => write!(f, "EndTime"),
984 Self::MinDuration => write!(f, "MinDuration"),
985 Self::MaxDuration => write!(f, "MaxDuration"),
986 Self::MinKwh => write!(f, "MinKwh"),
987 Self::MaxKwh => write!(f, "MaxKwh"),
988 }
989 }
990}
991
992#[derive(Debug)]
994struct Metrics {
995 end_date_time: DateTime<Utc>,
997
998 start_date_time: DateTime<Utc>,
1000
1001 duration_charging: TimeDelta,
1006
1007 duration_parking: Option<TimeDelta>,
1011
1012 energy_supplied: Kwh,
1014
1015 max_current_supply: Ampere,
1017
1018 max_power_supply: Kw,
1020}
1021
1022#[instrument(skip_all)]
1024fn metrics(elem: &tariff::Versioned<'_>, config: &Config) -> Verdict<(Metrics, chrono_tz::Tz)> {
1025 let warnings = warning::Set::new();
1026
1027 let Config {
1028 start_date_time,
1029 end_date_time,
1030 max_power_supply_kw,
1031 requested_kwh: max_energy_battery_kwh,
1032 max_current_supply_amp,
1033 timezone,
1034 } = config;
1035 let duration_session = end_date_time.signed_duration_since(start_date_time);
1036
1037 debug!("duration_session: {}", duration_session.as_hms());
1038
1039 if duration_session.abs() != duration_session {
1041 return warnings.bail(elem.as_element(), Warning::StartDateTimeIsAfterEndDateTime);
1042 }
1043
1044 if duration_session.num_seconds() < MIN_CS_DURATION_SECS {
1045 return warnings.bail(elem.as_element(), Warning::DurationBelowMinimum);
1046 }
1047
1048 if max_energy_battery_kwh.is_zero() {
1049 return warnings.bail(elem.as_element(), Warning::RequestedKwhIsZero);
1050 }
1051
1052 let duration_full_charge = some_dec_or_bail!(
1054 elem,
1055 max_energy_battery_kwh.checked_div(*max_power_supply_kw),
1056 warnings,
1057 "Unable to calculate charging time"
1058 )
1059 .to_duration();
1060 debug!("duration_full_charge: {}", duration_full_charge.as_hms());
1061
1062 let duration_charging = TimeDelta::min(duration_full_charge, duration_session);
1064
1065 let energy_supplied_kwh = some_dec_or_bail!(
1066 elem,
1067 max_power_supply_kw.checked_mul(duration_charging.to_hours_dec()),
1068 warnings,
1069 "Unable to calculate the energy supplied during the charging time"
1070 );
1071
1072 let duration_parking = some_time_delta_or_bail!(
1073 elem,
1074 duration_session.checked_sub(&duration_charging),
1075 warnings,
1076 "Unable to calculate `idle_duration`"
1077 );
1078
1079 debug!(
1080 "duration_charging: {}, duration_parking: {}",
1081 duration_charging.as_hms(),
1082 duration_parking.as_hms()
1083 );
1084
1085 let metrics = Metrics {
1086 end_date_time: *end_date_time,
1087 start_date_time: *start_date_time,
1088 duration_charging,
1089 duration_parking: Some(duration_parking).filter(|dt| dt.num_seconds().is_positive()),
1090 energy_supplied: Kwh::from_decimal(energy_supplied_kwh),
1091 max_current_supply: Ampere::from_decimal(*max_current_supply_amp),
1092 max_power_supply: Kw::from_decimal(*max_power_supply_kw),
1093 };
1094
1095 Ok((metrics, *timezone).into_caveat(warnings))
1096}
1097
1098fn is_tariff_active(cdr_start: &DateTime<Utc>, tariff: &tariff::v221::Tariff<'_>) -> bool {
1099 match (tariff.start_date_time, tariff.end_date_time) {
1100 (None, None) => true,
1101 (None, Some(end)) => (..end).contains(cdr_start),
1102 (Some(start), None) => (start..).contains(cdr_start),
1103 (Some(start), Some(end)) => (start..end).contains(cdr_start),
1104 }
1105}
1106
1107#[derive(Debug)]
1108pub enum Warning {
1109 Decimal(&'static str),
1111
1112 DurationBelowMinimum,
1114
1115 Price(price::Warning),
1116
1117 StartDateTimeIsAfterEndDateTime,
1119
1120 RequestedKwhIsZero,
1122
1123 Tariff(tariff::Warning),
1124
1125 TimeDelta(&'static str),
1127}
1128
1129impl crate::Warning for Warning {
1130 fn id(&self) -> warning::Id {
1131 match self {
1132 Self::Decimal(_) => warning::Id::from_static("decimal_error"),
1133 Self::DurationBelowMinimum => warning::Id::from_static("duration_below_minimum"),
1134 Self::Price(kind) => kind.id(),
1135 Self::StartDateTimeIsAfterEndDateTime => {
1136 warning::Id::from_static("start_time_after_end_time")
1137 }
1138 Self::RequestedKwhIsZero => warning::Id::from_static("requested_kwh_is_zero"),
1139 Self::TimeDelta(_) => warning::Id::from_static("timedelta_error"),
1140 Self::Tariff(kind) => kind.id(),
1141 }
1142 }
1143}
1144
1145impl fmt::Display for Warning {
1146 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1147 match self {
1148 Self::Decimal(msg) | Self::TimeDelta(msg) => f.write_str(msg),
1149 Self::DurationBelowMinimum => write!(
1150 f,
1151 "The duration of the chargesession is below the minimum: {MIN_CS_DURATION_SECS}"
1152 ),
1153 Self::Price(warnings) => {
1154 write!(f, "Price warnings: {warnings:?}")
1155 }
1156 Self::StartDateTimeIsAfterEndDateTime => {
1157 write!(f, "The `start_date_time` is after the `end_date_time`")
1158 }
1159 Self::RequestedKwhIsZero => write!(f, "The `requested_kwh` in the `Config` is zero"),
1160 Self::Tariff(warnings) => {
1161 write!(f, "Tariff warnings: {warnings:?}")
1162 }
1163 }
1164 }
1165}
1166
1167from_warning_all!(
1168 tariff::Warning => Warning::Tariff,
1169 price::Warning => Warning::Price
1170);