1#[cfg(test)]
2mod test;
3
4#[cfg(test)]
5mod test_clamp_date_time_span;
6
7#[cfg(test)]
8mod test_gen_time_events;
9
10#[cfg(test)]
11mod test_generate;
12
13#[cfg(test)]
14mod test_generate_from_single_elem_tariff;
15
16#[cfg(test)]
17mod test_local_to_utc;
18
19#[cfg(test)]
20mod test_periods;
21
22#[cfg(test)]
23mod test_power_to_time;
24
25mod v2x;
26
27use std::{
28 cmp::{max, min},
29 fmt,
30 ops::Range,
31};
32
33use chrono::{DateTime, Datelike as _, NaiveDateTime, NaiveTime, TimeDelta, Utc};
34use rust_decimal::{prelude::ToPrimitive, Decimal};
35use rust_decimal_macros::dec;
36use tracing::warn;
37
38use crate::{
39 country, currency,
40 duration::ToHoursDecimal,
41 energy::{Ampere, Kw, Kwh},
42 from_warning_all, into_caveat, into_caveat_all,
43 json::FromJson as _,
44 number::{FromDecimal as _, RoundDecimal},
45 price, tariff,
46 warning::{self, GatherWarnings as _, IntoCaveat, WithElement as _},
47 Price, Version, Versioned,
48};
49
50const MIN_CS_DURATION_SECS: i64 = 120;
52
53type DateTimeSpan = Range<DateTime<Utc>>;
54type Verdict<T> = crate::Verdict<T, Warning>;
55pub type Caveat<T> = warning::Caveat<T, Warning>;
56
57macro_rules! some_dec_or_bail {
59 ($elem:expr, $opt:expr, $warnings:expr, $msg:literal) => {
60 match $opt {
61 Some(v) => v,
62 None => {
63 return $warnings.bail(Warning::Decimal($msg), $elem.as_element());
64 }
65 }
66 };
67}
68
69#[derive(Debug)]
71pub struct Report {
72 pub tariff_id: String,
74
75 pub tariff_currency_code: currency::Code,
77
78 pub partial_cdr: PartialCdr,
85}
86
87#[derive(Debug)]
95pub struct PartialCdr {
96 pub cpo_country_code: Option<country::Code>,
98
99 pub cpo_currency_code: currency::Code,
101
102 pub party_id: Option<String>,
104
105 pub start_date_time: DateTime<Utc>,
107
108 pub end_date_time: DateTime<Utc>,
110
111 pub total_energy: Option<Kwh>,
113
114 pub total_charging_duration: Option<TimeDelta>,
118
119 pub total_parking_duration: Option<TimeDelta>,
123
124 pub total_cost: Option<Price>,
126
127 pub total_energy_cost: Option<Price>,
129
130 pub total_fixed_cost: Option<Price>,
132
133 pub total_parking_duration_cost: Option<Price>,
135
136 pub total_charging_duration_cost: Option<Price>,
138
139 pub charging_periods: Vec<ChargingPeriod>,
142}
143
144#[derive(Debug)]
148pub struct ChargingPeriod {
149 pub start_date_time: DateTime<Utc>,
152
153 pub dimensions: Vec<Dimension>,
155
156 pub tariff_id: Option<String>,
160}
161
162#[derive(Debug)]
166pub struct Dimension {
167 pub dimension_type: DimensionType,
168
169 pub volume: Decimal,
171}
172
173#[derive(Debug, Clone, PartialEq, Eq)]
177pub enum DimensionType {
178 Energy,
180 MaxCurrent,
182 MinCurrent,
184 MaxPower,
186 MinPower,
188 ParkingTime,
190 ReservationTime,
192 Time,
194}
195
196into_caveat_all!(Report, Timeline);
197
198pub fn cdr_from_tariff(tariff_elem: &tariff::Versioned<'_>, config: Config) -> Verdict<Report> {
200 let mut warnings = warning::Set::new();
201 let (metrics, timezone) = metrics(tariff_elem, config)?.gather_warnings_into(&mut warnings);
209
210 let tariff = match tariff_elem.version() {
211 Version::V211 => {
212 let tariff = tariff::v211::Tariff::from_json(tariff_elem.as_element())?
213 .gather_warnings_into(&mut warnings);
214
215 tariff::v221::Tariff::from(tariff)
216 }
217 Version::V221 => tariff::v221::Tariff::from_json(tariff_elem.as_element())?
218 .gather_warnings_into(&mut warnings),
219 };
220
221 if !is_tariff_active(&metrics.start_date_time, &tariff) {
222 warnings.insert(tariff::Warning::NotActive.into(), tariff_elem.as_element());
223 }
224
225 let timeline = timeline(timezone, &metrics, &tariff);
226 let charging_periods = charge_periods(&metrics, timeline);
227
228 let report = price::periods(metrics.end_date_time, timezone, &tariff, charging_periods)
229 .with_element(tariff_elem.as_element())?
230 .gather_warnings_into(&mut warnings);
231
232 let price::PeriodsReport {
233 billable: _,
234 periods,
235 totals,
236 total_costs,
237 } = report;
238
239 let charging_periods = periods
240 .into_iter()
241 .map(|period| {
242 let price::PeriodReport {
243 start_date_time,
244 end_date_time: _,
245 dimensions,
246 } = period;
247 let time = dimensions
248 .duration_charging
249 .volume
250 .as_ref()
251 .map(|dt| Dimension {
252 dimension_type: DimensionType::Time,
253 volume: ToHoursDecimal::to_hours_dec(dt),
254 });
255 let parking_time = dimensions
256 .duration_parking
257 .volume
258 .as_ref()
259 .map(|dt| Dimension {
260 dimension_type: DimensionType::ParkingTime,
261 volume: ToHoursDecimal::to_hours_dec(dt),
262 });
263 let energy = dimensions.energy.volume.as_ref().map(|kwh| Dimension {
264 dimension_type: DimensionType::Energy,
265 volume: (*kwh).into(),
266 });
267 let dimensions = vec![energy, parking_time, time]
268 .into_iter()
269 .flatten()
270 .collect();
271
272 ChargingPeriod {
273 start_date_time,
274 dimensions,
275 tariff_id: Some(tariff.id.to_string()),
276 }
277 })
278 .collect();
279
280 let mut total_cost = total_costs.total();
281
282 if let Some(total_cost) = total_cost.as_mut() {
283 if let Some(min_price) = tariff.min_price {
284 if *total_cost < min_price {
285 *total_cost = min_price;
286 warnings.insert(
287 tariff::Warning::TotalCostClampedToMin.into(),
288 tariff_elem.as_element(),
289 );
290 }
291 }
292
293 if let Some(max_price) = tariff.max_price {
294 if *total_cost > max_price {
295 *total_cost = max_price;
296 warnings.insert(
297 tariff::Warning::TotalCostClampedToMin.into(),
298 tariff_elem.as_element(),
299 );
300 }
301 }
302 }
303
304 let report = Report {
305 tariff_id: tariff.id.to_string(),
306 tariff_currency_code: tariff.currency,
307 partial_cdr: PartialCdr {
308 cpo_country_code: tariff.country_code,
309 party_id: tariff.party_id.as_ref().map(ToString::to_string),
310 start_date_time: metrics.start_date_time,
311 end_date_time: metrics.end_date_time,
312 cpo_currency_code: tariff.currency,
313 total_energy: totals.energy.round_to_ocpi_scale(),
314 total_charging_duration: totals.duration_charging,
315 total_parking_duration: totals.duration_parking,
316 total_cost: total_cost.round_to_ocpi_scale(),
317 total_energy_cost: total_costs.energy.round_to_ocpi_scale(),
318 total_fixed_cost: total_costs.fixed.round_to_ocpi_scale(),
319 total_parking_duration_cost: total_costs.duration_parking.round_to_ocpi_scale(),
320 total_charging_duration_cost: total_costs.duration_charging.round_to_ocpi_scale(),
321 charging_periods,
322 },
323 };
324
325 Ok(report.into_caveat(warnings))
326}
327
328fn timeline(
330 timezone: chrono_tz::Tz,
331 metrics: &Metrics,
332 tariff: &tariff::v221::Tariff<'_>,
333) -> Timeline {
334 let mut events = vec![];
335
336 let Metrics {
337 start_date_time: cdr_start,
338 end_date_time: cdr_end,
339 duration_charging,
340 duration_parking,
341 max_power_supply,
342 max_current_supply,
343
344 energy_supplied: _,
345 } = metrics;
346
347 events.push(Event {
348 duration_from_start: TimeDelta::seconds(0),
349 kind: EventKind::SessionStart,
350 });
351
352 events.push(Event {
353 duration_from_start: *duration_charging,
354 kind: EventKind::ChargingEnd,
355 });
356
357 if let Some(duration_parking) = duration_parking {
358 events.push(Event {
359 duration_from_start: *duration_parking,
360 kind: EventKind::ParkingEnd {
361 start: metrics.duration_charging,
362 },
363 });
364 }
365
366 let mut emit_current = false;
369
370 let mut emit_power = false;
373
374 for elem in &tariff.elements {
375 if let Some((time_restrictions, energy_restrictions)) = elem
376 .restrictions
377 .as_ref()
378 .map(tariff::v221::Restrictions::restrictions_by_category)
379 {
380 let mut time_events =
381 generate_time_events(timezone, *cdr_start..*cdr_end, time_restrictions);
382
383 let v2x::EnergyRestrictions {
384 min_kwh,
385 max_kwh,
386 min_current,
387 max_current,
388 min_power,
389 max_power,
390 } = energy_restrictions;
391
392 if !emit_current {
393 emit_current = (min_current..=max_current).contains(&Some(*max_current_supply));
398 }
399
400 if !emit_power {
401 emit_power = (min_power..=max_power).contains(&Some(*max_power_supply));
406 }
407
408 let mut energy_events = generate_energy_events(
409 metrics.duration_charging,
410 metrics.energy_supplied,
411 min_kwh,
412 max_kwh,
413 );
414
415 events.append(&mut time_events);
416 events.append(&mut energy_events);
417 }
418 }
419
420 Timeline {
421 events,
422 emit_current,
423 emit_power,
424 }
425}
426
427fn generate_time_events(
429 timezone: chrono_tz::Tz,
430 cdr_span: DateTimeSpan,
431 restrictions: v2x::TimeRestrictions,
432) -> Vec<Event> {
433 const MIDNIGHT: NaiveTime = NaiveTime::from_hms_opt(0, 0, 0)
434 .expect("The hour, minute and second values are correct and hardcoded");
435 const ONE_DAY: TimeDelta = TimeDelta::days(1);
436
437 let v2x::TimeRestrictions {
438 start_time,
439 end_time,
440 start_date,
441 end_date,
442 min_duration,
443 max_duration,
444 weekdays,
445 } = restrictions;
446 let mut events = vec![];
447
448 let cdr_duration = cdr_span.end.signed_duration_since(cdr_span.start);
449
450 if let Some(min_duration) = min_duration.filter(|dt| &cdr_duration < dt) {
452 events.push(Event {
453 duration_from_start: min_duration,
454 kind: EventKind::MinDuration,
455 });
456 }
457
458 if let Some(max_duration) = max_duration.filter(|dt| &cdr_duration < dt) {
460 events.push(Event {
461 duration_from_start: max_duration,
462 kind: EventKind::MaxDuration,
463 });
464 }
465
466 let (start_date_time, end_date_time) =
476 if let (Some(start_time), Some(end_time)) = (start_time, end_time) {
477 if end_time < start_time {
478 (
479 start_date.map(|d| d.and_time(start_time)),
480 end_date.map(|d| {
481 let (end_time, _) = end_time.overflowing_add_signed(ONE_DAY);
482 d.and_time(end_time)
483 }),
484 )
485 } else {
486 (
487 start_date.map(|d| d.and_time(start_time)),
488 end_date.map(|d| d.and_time(end_time)),
489 )
490 }
491 } else {
492 (
493 start_date.map(|d| d.and_time(start_time.unwrap_or(MIDNIGHT))),
494 end_date.map(|d| d.and_time(end_time.unwrap_or(MIDNIGHT))),
495 )
496 };
497
498 let event_span = clamp_date_time_span(
501 start_date_time.and_then(|d| local_to_utc(timezone, d)),
502 end_date_time.and_then(|d| local_to_utc(timezone, d)),
503 cdr_span,
504 );
505
506 if let Some(start_time) = start_time {
507 let mut start_events =
508 gen_naive_time_events(&event_span, start_time, &weekdays, EventKind::StartTime);
509 events.append(&mut start_events);
510 }
511
512 if let Some(end_time) = end_time {
513 let mut end_events =
514 gen_naive_time_events(&event_span, end_time, &weekdays, EventKind::EndTime);
515 events.append(&mut end_events);
516 }
517
518 events
519}
520
521fn local_to_utc(timezone: chrono_tz::Tz, date_time: NaiveDateTime) -> Option<DateTime<Utc>> {
527 use chrono::offset::LocalResult;
528
529 let result = date_time.and_local_timezone(timezone);
530
531 let local_date_time = match result {
532 LocalResult::Single(d) => d,
533 LocalResult::Ambiguous(earliest, _latest) => earliest,
534 LocalResult::None => return None,
535 };
536
537 Some(local_date_time.to_utc())
538}
539
540fn gen_naive_time_events(
542 event_span: &Range<DateTime<Utc>>,
543 time: NaiveTime,
544 weekdays: &v2x::WeekdaySet,
545 kind: EventKind,
546) -> Vec<Event> {
547 let mut events = vec![];
548 let time_delta = time.signed_duration_since(event_span.start.time());
549 let cdr_duration = event_span.end.signed_duration_since(event_span.start);
550
551 let time_delta = if time_delta.num_seconds().is_negative() {
554 let (time_delta, _) = time.overflowing_add_signed(TimeDelta::days(1));
555 time_delta.signed_duration_since(event_span.start.time())
556 } else {
557 time_delta
558 };
559
560 if time_delta.num_seconds().is_negative() {
562 return vec![];
563 }
564
565 let Some(remainder) = cdr_duration.checked_sub(&time_delta) else {
567 warn!("TimeDelta overflow");
568 return events;
569 };
570
571 if remainder.num_seconds().is_positive() {
572 let duration_from_start = time_delta;
573 let Some(date) = event_span.start.checked_add_signed(duration_from_start) else {
574 warn!("Date out of range");
575 return events;
576 };
577
578 if weekdays.contains(date.weekday()) {
579 events.push(Event {
581 duration_from_start: time_delta,
582 kind,
583 });
584 }
585
586 for day in 1..=remainder.num_days() {
587 let Some(duration_from_start) = time_delta.checked_add(&TimeDelta::days(day)) else {
588 warn!("Date out of range");
589 break;
590 };
591 let Some(date) = event_span.start.checked_add_signed(duration_from_start) else {
592 warn!("Date out of range");
593 break;
594 };
595
596 if weekdays.contains(date.weekday()) {
597 events.push(Event {
598 duration_from_start,
599 kind,
600 });
601 }
602 }
603 }
604
605 events
606}
607
608fn generate_energy_events(
610 duration_charging: TimeDelta,
611 energy_supplied: Kwh,
612 min_kwh: Option<Kwh>,
613 max_kwh: Option<Kwh>,
614) -> Vec<Event> {
615 let mut events = vec![];
616
617 if let Some(duration_from_start) =
618 min_kwh.and_then(|kwh| power_to_time(kwh, energy_supplied, duration_charging))
619 {
620 events.push(Event {
621 duration_from_start,
622 kind: EventKind::MinKwh,
623 });
624 }
625
626 if let Some(duration_from_start) =
627 max_kwh.and_then(|kwh| power_to_time(kwh, energy_supplied, duration_charging))
628 {
629 events.push(Event {
630 duration_from_start,
631 kind: EventKind::MaxKwh,
632 });
633 }
634
635 events
636}
637
638fn power_to_time(power: Kwh, power_total: Kwh, duration_total: TimeDelta) -> Option<TimeDelta> {
640 use rust_decimal::prelude::ToPrimitive;
641
642 let power = Decimal::from(power);
645 let power_total = Decimal::from(power_total);
647 let Some(factor) = power.checked_div(power_total) else {
650 return Some(TimeDelta::zero());
651 };
652
653 if factor.is_sign_negative() || factor > dec!(1.0) {
654 return None;
655 }
656
657 let duration_from_start = factor.checked_mul(Decimal::from(duration_total.num_seconds()))?;
658 duration_from_start.to_i64().map(TimeDelta::seconds)
659}
660
661fn charge_periods(metrics: &Metrics, timeline: Timeline) -> Vec<price::Period> {
663 enum ChargingPhase {
665 Charging,
666 Parking,
667 }
668
669 let Metrics {
670 start_date_time: cdr_start,
671 max_power_supply,
672 max_current_supply,
673
674 end_date_time: _,
675 duration_charging: _,
676 duration_parking: _,
677 energy_supplied: _,
678 } = metrics;
679
680 let Timeline {
681 mut events,
682 emit_current,
683 emit_power,
684 } = timeline;
685
686 events.sort_unstable_by_key(|e| e.duration_from_start);
687
688 let mut periods = vec![];
689 let emit_current = emit_current.then_some(*max_current_supply);
690 let emit_power = emit_power.then_some(*max_power_supply);
691 let mut charging_phase = ChargingPhase::Charging;
693
694 for items in events.windows(2) {
695 let [event, event_next] = items else {
696 unreachable!("The window size is 2");
697 };
698
699 let Event {
700 duration_from_start,
701 kind,
702 } = event;
703
704 if let EventKind::ChargingEnd = kind {
705 charging_phase = ChargingPhase::Parking;
706 }
707
708 let Some(duration) = event_next
709 .duration_from_start
710 .checked_sub(duration_from_start)
711 else {
712 warn!("TimeDelta overflow");
713 break;
714 };
715 let Some(start_date_time) = cdr_start.checked_add_signed(*duration_from_start) else {
716 warn!("TimeDelta overflow");
717 break;
718 };
719
720 let consumed = if let ChargingPhase::Charging = charging_phase {
721 let Some(energy) =
722 Decimal::from(*max_power_supply).checked_mul(duration.to_hours_dec())
723 else {
724 warn!("Decimal overflow");
725 break;
726 };
727 price::Consumed {
728 duration_charging: Some(duration),
729 duration_parking: None,
730 energy: Some(Kwh::from_decimal(energy)),
731 current_max: emit_current,
732 current_min: emit_current,
733 power_max: emit_power,
734 power_min: emit_power,
735 }
736 } else {
737 price::Consumed {
738 duration_charging: None,
739 duration_parking: Some(duration),
740 energy: None,
741 current_max: None,
742 current_min: None,
743 power_max: None,
744 power_min: None,
745 }
746 };
747
748 let period = price::Period {
749 start_date_time,
750 consumed,
751 };
752
753 periods.push(period);
754 }
755
756 periods
757}
758
759fn clamp_date_time_span(
765 min_date: Option<DateTime<Utc>>,
766 max_date: Option<DateTime<Utc>>,
767 span: DateTimeSpan,
768) -> DateTimeSpan {
769 let (min_date, max_date) = (min(min_date, max_date), max(min_date, max_date));
771
772 let start = min_date.filter(|d| &span.start < d).unwrap_or(span.start);
773 let end = max_date.filter(|d| &span.end > d).unwrap_or(span.end);
774
775 DateTimeSpan { start, end }
776}
777
778struct Timeline {
780 events: Vec<Event>,
782
783 emit_current: bool,
785
786 emit_power: bool,
788}
789
790#[derive(Debug)]
792struct Event {
793 duration_from_start: TimeDelta,
795
796 kind: EventKind,
798}
799
800#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
802enum EventKind {
803 SessionStart,
809
810 ChargingEnd,
815
816 ParkingEnd {
821 start: TimeDelta,
823 },
824
825 StartTime,
826
827 EndTime,
828
829 MinDuration,
834
835 MaxDuration,
840
841 MinKwh,
843
844 MaxKwh,
846}
847
848#[derive(Debug)]
850struct Metrics {
851 end_date_time: DateTime<Utc>,
853
854 start_date_time: DateTime<Utc>,
856
857 duration_charging: TimeDelta,
862
863 duration_parking: Option<TimeDelta>,
867
868 energy_supplied: Kwh,
870
871 max_current_supply: Ampere,
873
874 max_power_supply: Kw,
876}
877
878into_caveat!(Metrics);
879
880#[expect(
882 clippy::needless_pass_by_value,
883 reason = "Clippy is complaining that `Config` is not consumed by the function when it clearly is"
884)]
885fn metrics(elem: &tariff::Versioned<'_>, config: Config) -> Verdict<(Metrics, chrono_tz::Tz)> {
886 const SECS_IN_HOUR: Decimal = dec!(3600);
887
888 let warnings = warning::Set::new();
889
890 let Config {
891 start_date_time,
892 end_date_time,
893 max_power_supply_kw,
894 requested_kwh: max_energy_battery_kwh,
895 max_current_supply_amp,
896 timezone,
897 } = config;
898 let duration_session = end_date_time.signed_duration_since(start_date_time);
899
900 if duration_session.num_seconds().is_negative() {
902 return warnings.bail(Warning::StartDateTimeIsAfterEndDateTime, elem.as_element());
903 }
904
905 if duration_session.num_seconds() < MIN_CS_DURATION_SECS {
906 return warnings.bail(Warning::DurationBelowMinimum, elem.as_element());
907 }
908
909 let duration_full_charge_hours = some_dec_or_bail!(
911 elem,
912 max_energy_battery_kwh.checked_div(max_power_supply_kw),
913 warnings,
914 "Unable to calculate changing time"
915 );
916
917 let charge_duration_hours =
919 Decimal::min(duration_full_charge_hours, duration_session.to_hours_dec());
920
921 let power_supplied_kwh = some_dec_or_bail!(
922 elem,
923 max_energy_battery_kwh.checked_div(charge_duration_hours),
924 warnings,
925 "Unable to calculate the power supplied during the charging time"
926 );
927
928 let charging_duration_secs = some_dec_or_bail!(
930 elem,
931 charge_duration_hours.checked_mul(SECS_IN_HOUR),
932 warnings,
933 "Unable to convert charging time from hours to seconds"
934 );
935
936 let charging_duration_secs = some_dec_or_bail!(
937 elem,
938 charging_duration_secs.to_i64(),
939 warnings,
940 "Unable to convert charging duration Decimal to i64"
941 );
942 let duration_charging = TimeDelta::seconds(charging_duration_secs);
943
944 let duration_parking = some_dec_or_bail!(
945 elem,
946 duration_session.checked_sub(&duration_charging),
947 warnings,
948 "Unable to calculate `idle_duration`"
949 );
950
951 let metrics = Metrics {
952 end_date_time,
953 start_date_time,
954 duration_charging,
955 duration_parking: Some(duration_parking).filter(|dt| dt.num_seconds().is_positive()),
956 energy_supplied: Kwh::from_decimal(power_supplied_kwh),
957 max_current_supply: Ampere::from_decimal(max_current_supply_amp),
958 max_power_supply: Kw::from_decimal(max_power_supply_kw),
959 };
960
961 Ok((metrics, timezone).into_caveat(warnings))
962}
963
964fn is_tariff_active(cdr_start: &DateTime<Utc>, tariff: &tariff::v221::Tariff<'_>) -> bool {
965 match (tariff.start_date_time, tariff.end_date_time) {
966 (None, None) => true,
967 (None, Some(end)) => (..end).contains(cdr_start),
968 (Some(start), None) => (start..).contains(cdr_start),
969 (Some(start), Some(end)) => (start..end).contains(cdr_start),
970 }
971}
972
973#[derive(Debug)]
974pub enum Warning {
975 Decimal(&'static str),
977
978 DurationBelowMinimum,
980
981 Price(price::Warning),
982
983 StartDateTimeIsAfterEndDateTime,
985
986 Tariff(tariff::Warning),
987}
988
989impl crate::Warning for Warning {
990 fn id(&self) -> warning::Id {
991 match self {
992 Self::Decimal(_) => warning::Id::from_static("decimal_error"),
993 Self::DurationBelowMinimum => warning::Id::from_static("duration_below_minimum"),
994 Self::Price(kind) => kind.id(),
995 Self::StartDateTimeIsAfterEndDateTime => {
996 warning::Id::from_static("start_time_after_end_time")
997 }
998 Self::Tariff(kind) => kind.id(),
999 }
1000 }
1001}
1002
1003impl fmt::Display for Warning {
1004 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1005 match self {
1006 Self::Decimal(msg) => f.write_str(msg),
1007 Self::DurationBelowMinimum => write!(
1008 f,
1009 "The duration of the chargesession is below the minimum: {MIN_CS_DURATION_SECS}"
1010 ),
1011 Self::Price(warnings) => {
1012 write!(f, "Price warnings: {warnings:?}")
1013 }
1014 Self::StartDateTimeIsAfterEndDateTime => {
1015 write!(f, "The `start_date_time` is after the `end_date_time`")
1016 }
1017 Self::Tariff(warnings) => {
1018 write!(f, "Tariff warnings: {warnings:?}")
1019 }
1020 }
1021 }
1022}
1023
1024from_warning_all!(
1025 tariff::Warning => Warning::Tariff,
1026 price::Warning => Warning::Price
1027);
1028
1029#[derive(Clone)]
1031pub struct Config {
1032 pub timezone: chrono_tz::Tz,
1034
1035 pub end_date_time: DateTime<Utc>,
1037
1038 pub max_current_supply_amp: Decimal,
1040
1041 pub requested_kwh: Decimal,
1046
1047 pub max_power_supply_kw: Decimal,
1056
1057 pub start_date_time: DateTime<Utc>,
1059}