Skip to main content

ocpi_tariffs/
generate.rs

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
25#[cfg(test)]
26mod test_popular_tariffs;
27
28mod v2x;
29
30use std::{
31    cmp::{max, min},
32    fmt,
33    ops::Range,
34};
35
36use chrono::{DateTime, Datelike as _, NaiveDateTime, NaiveTime, TimeDelta, Utc};
37use rust_decimal::{prelude::ToPrimitive, Decimal};
38use rust_decimal_macros::dec;
39use tracing::{debug, instrument, warn};
40
41use crate::{
42    country, currency,
43    duration::{AsHms, ToHoursDecimal},
44    energy::{Ampere, Kw, Kwh},
45    from_warning_all,
46    json::FromJson as _,
47    number::{FromDecimal as _, RoundDecimal},
48    price, tariff,
49    warning::{self, GatherWarnings as _, IntoCaveat, WithElement as _},
50    Price, SaturatingAdd, Version, Versioned,
51};
52
53/// The minimum duration of a CDR. Anything below this will result in an Error.
54const MIN_CS_DURATION_SECS: i64 = 120;
55
56type DateTimeSpan = Range<DateTime<Utc>>;
57type Verdict<T> = crate::Verdict<T, Warning>;
58pub type Caveat<T> = warning::Caveat<T, Warning>;
59
60/// Return the value if `Some`. Otherwise, bail(return) with an `Error::Internal` containing the giving message.
61macro_rules! some_dec_or_bail {
62    ($elem:expr, $opt:expr, $warnings:expr, $msg:literal) => {
63        match $opt {
64            Some(v) => v,
65            None => {
66                return $warnings.bail(Warning::Decimal($msg), $elem.as_element());
67            }
68        }
69    };
70}
71
72/// The outcome of calling [`crate::cdr::generate_from_tariff`].
73#[derive(Debug)]
74pub struct Report {
75    /// The ID of the parsed tariff.
76    pub tariff_id: String,
77
78    // The currency code of the parsed tariff.
79    pub tariff_currency_code: currency::Code,
80
81    /// A partial CDR that can be fleshed out by the caller.
82    ///
83    /// The CDR is partial as not all required fields are set as the `cdr_from_tariff` function
84    /// does not know anything about the EVSE location or the token used to authenticate the chargesession.
85    ///
86    /// * See: [OCPI spec 2.2.1: CDR](<https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_cdrs.asciidoc>)
87    pub partial_cdr: PartialCdr,
88}
89
90/// A partial CDR generated by the `cdr_from_tariff` function.
91///
92/// The CDR is partial as not all required fields are set as the `cdr_from_tariff` function
93/// does not know anything about the EVSE location or the token used to authenticate the chargesession.
94///
95/// * See: [OCPI spec 2.2.1: CDR](<https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_cdrs.asciidoc>)
96/// * See: [OCPI spec 2.1.1: Tariff](https://github.com/ocpi/ocpi/blob/release-2.1.1-bugfixes/mod_tariffs.md)
97#[derive(Debug)]
98pub struct PartialCdr {
99    /// ISO-3166 alpha-2 country code of the CPO that 'owns' this CDR.
100    pub currency_code: currency::Code,
101
102    /// The five character ID of the CPO that 'owns' this CDR.
103    ///
104    /// The first two characters are the ISO-3166 alpha-2 country code of the CPO. The remaining three
105    /// characters are the ISO-15118 ID of the CPO.
106    ///
107    /// None if a v211 tariff was used to generate the CDR.
108    /// The v211 tariff does not define a country code or `party_id` field.
109    pub party_id: Option<CpoId>,
110
111    /// Start timestamp of the charging session.
112    pub start_date_time: DateTime<Utc>,
113
114    /// End timestamp of the charging session.
115    pub end_date_time: DateTime<Utc>,
116
117    /// Total energy charged, in kWh.
118    pub total_energy: Option<Kwh>,
119
120    /// Total time charging.
121    ///
122    /// Some if the charging happened during the session.
123    pub total_charging_duration: Option<TimeDelta>,
124
125    /// Total time not charging.
126    ///
127    /// Some if there was idle time during the session.
128    pub total_parking_duration: Option<TimeDelta>,
129
130    /// Total cost of this transaction.
131    pub total_cost: Option<Price>,
132
133    /// Total cost related to the energy dimension.
134    pub total_energy_cost: Option<Price>,
135
136    /// Total cost of the flat dimension.
137    pub total_fixed_cost: Option<Price>,
138
139    /// Total cost related to the parking time dimension.
140    pub total_parking_duration_cost: Option<Price>,
141
142    /// Total cost related to the charging time dimension.
143    pub total_charging_duration_cost: Option<Price>,
144
145    /// List of charging periods that make up this charging session. A session should consist of 1 or
146    /// more periods, where each period has a different relevant Tariff.
147    pub charging_periods: Vec<ChargingPeriod>,
148}
149
150/// The five character ID of the CPO.
151///
152/// The first two characters are the ISO-3166 alpha-2 country code of the CPO.
153/// The remaining three characters are the ISO-15118 ID of the CPO.
154#[derive(Clone, Debug)]
155pub struct CpoId {
156    /// The ISO-3166 alpha-2 country code.
157    pub country_code: country::Code,
158
159    /// The ISO-15118 ID.
160    pub id: String,
161}
162
163impl<'buf> From<tariff::CpoId<'buf>> for CpoId {
164    fn from(value: tariff::CpoId<'buf>) -> Self {
165        let tariff::CpoId { country_code, id } = value;
166        CpoId {
167            country_code,
168            id: id.to_string(),
169        }
170    }
171}
172
173/// Display the CPO ID formatted like `NLENE`.
174impl fmt::Display for CpoId {
175    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
176        write!(f, "{}{}", self.country_code.into_alpha_2_str(), self.id)
177    }
178}
179
180/// A single charging period, containing a nonempty list of charge dimensions.
181///
182/// * See: [OCPI spec 2.2.1: CDR ChargingPeriod](<https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_cdrs.asciidoc#146-chargingperiod-class>)
183#[derive(Debug)]
184pub struct ChargingPeriod {
185    /// Start timestamp of the charging period. This period ends when a next period starts, the
186    /// last period ends when the session ends
187    pub start_date_time: DateTime<Utc>,
188
189    /// List of relevant values for this charging period.
190    pub dimensions: Vec<Dimension>,
191
192    /// Unique identifier of the Tariff that is relevant for this Charging Period.
193    /// In the OCPI spec the `tariff_id` field is optional but, we always know the tariff ID
194    /// when generating a CDR.
195    pub tariff_id: Option<String>,
196}
197
198/// The volume that has been consumed for a specific dimension during a charging period.
199///
200/// * See: [OCPI spec 2.2.1: CDR Dimension](<https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_cdrs.asciidoc#mod_cdrs_cdrdimension_class>)
201#[derive(Debug)]
202pub struct Dimension {
203    pub dimension_type: DimensionType,
204
205    /// Volume of the dimension consumed, measured according to the dimension type.
206    pub volume: Decimal,
207}
208
209/// The volume that has been consumed for a specific dimension during a charging period.
210///
211/// * See: [OCPI spec 2.2.1 CDR DimensionType](<https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_cdrs.asciidoc#mod_cdrs_cdrdimension_class>)
212#[derive(Debug, Clone, PartialEq, Eq)]
213pub enum DimensionType {
214    /// Consumed energy in `kWh`.
215    Energy,
216
217    /// The peak current, in 'A', during this period.
218    MaxCurrent,
219
220    /// The lowest current, in `A`, during this period.
221    MinCurrent,
222
223    /// The maximum power, in 'kW', reached during this period.
224    MaxPower,
225
226    /// The minimum power, in 'kW', reached during this period.
227    MinPower,
228
229    /// The parking time, in hours, consumed in this period.
230    ParkingTime,
231
232    /// The reservation time, in hours, consumed in this period.
233    ReservationTime,
234
235    /// The charging time, in hours, consumed in this period.
236    Time,
237}
238
239/// The config for generating a CDR from a tariff.
240#[derive(Clone)]
241pub struct Config {
242    /// The timezone of the EVSE: The timezone where the chargesession took place.
243    pub timezone: chrono_tz::Tz,
244
245    /// The end date of the generated CDR.
246    pub end_date_time: DateTime<Utc>,
247
248    /// The maximum DC current that can be delivered to the battery.
249    pub max_current_supply_amp: Decimal,
250
251    /// The amount of energy(kWh) the requested to be delivered.
252    ///
253    /// We don't model charging curves for the battery, so we don't care about the existing change of
254    /// the battery.
255    pub requested_kwh: Decimal,
256
257    /// The maximum DC power(kw) that can be delivered to the battery.
258    ///
259    /// This is modeled as a DC system as we don't care if the delivery medium is DC or one of the
260    /// various AC forms. We only care what the effective DC power is. The caller of `cdr_from_tariff`
261    /// should convert the delivery medium into a DC kw power by using a power factor.
262    ///
263    /// In practice the maximum power bottleneck is either the EVSE, the cable or the battery itself.
264    /// But whatever the bottleneck is, the caller should work that out and set the maximum expected.
265    pub max_power_supply_kw: Decimal,
266
267    /// The start date of the generated CDR.
268    pub start_date_time: DateTime<Utc>,
269}
270
271/// Generate a CDR from a given tariff.
272pub fn cdr_from_tariff(tariff_elem: &tariff::Versioned<'_>, config: &Config) -> Verdict<Report> {
273    let mut warnings = warning::Set::new();
274    // To generate a CDR from a tariff first, the tariff is parsed into structured data.
275    // Then some broad metrics are calculated that define limits on the chargesession.
276    //
277    // A Timeline of Events is then constructed by generating Events for each Element and each restriction.
278    // Some restrictions are periodic and can result in many `Event`s.
279    //
280    // The `Timeline` of `Event`s are then sorted by time and converted into a list of `ChargePeriods`.
281    let (metrics, timezone) = metrics(tariff_elem, config)?.gather_warnings_into(&mut warnings);
282
283    let tariff = match tariff_elem.version() {
284        Version::V211 => {
285            let tariff = tariff::v211::Tariff::from_json(tariff_elem.as_element())?
286                .gather_warnings_into(&mut warnings);
287
288            tariff::v221::Tariff::from(tariff)
289        }
290        Version::V221 => tariff::v221::Tariff::from_json(tariff_elem.as_element())?
291            .gather_warnings_into(&mut warnings),
292    };
293
294    if !is_tariff_active(&metrics.start_date_time, &tariff) {
295        warnings.insert(tariff::Warning::NotActive.into(), tariff_elem.as_element());
296    }
297
298    let timeline = timeline(timezone, &metrics, &tariff);
299    let charging_periods = charge_periods(&metrics, timeline);
300
301    let report = price::periods(metrics.end_date_time, timezone, &tariff, charging_periods)
302        .with_element(tariff_elem.as_element())?
303        .gather_warnings_into(&mut warnings);
304
305    let price::PeriodsReport {
306        billable: _,
307        periods,
308        totals,
309        total_costs,
310    } = report;
311
312    let charging_periods = periods
313        .into_iter()
314        .map(|period| {
315            let price::PeriodReport {
316                start_date_time,
317                end_date_time: _,
318                dimensions,
319            } = period;
320            let time = dimensions
321                .duration_charging
322                .volume
323                .as_ref()
324                .map(|dt| Dimension {
325                    dimension_type: DimensionType::Time,
326                    volume: ToHoursDecimal::to_hours_dec(dt),
327                });
328            let parking_time = dimensions
329                .duration_parking
330                .volume
331                .as_ref()
332                .map(|dt| Dimension {
333                    dimension_type: DimensionType::ParkingTime,
334                    volume: ToHoursDecimal::to_hours_dec(dt),
335                });
336            let energy = dimensions.energy.volume.as_ref().map(|kwh| Dimension {
337                dimension_type: DimensionType::Energy,
338                volume: (*kwh).into(),
339            });
340            let dimensions = vec![energy, parking_time, time]
341                .into_iter()
342                .flatten()
343                .collect();
344
345            ChargingPeriod {
346                start_date_time,
347                dimensions,
348                tariff_id: Some(tariff.id.to_string()),
349            }
350        })
351        .collect();
352
353    let mut total_cost = total_costs.total();
354
355    if let Some(total_cost) = total_cost.as_mut() {
356        if let Some(min_price) = tariff.min_price {
357            if *total_cost < min_price {
358                *total_cost = min_price;
359                warnings.insert(
360                    tariff::Warning::TotalCostClampedToMin.into(),
361                    tariff_elem.as_element(),
362                );
363            }
364        }
365
366        if let Some(max_price) = tariff.max_price {
367            if *total_cost > max_price {
368                *total_cost = max_price;
369                warnings.insert(
370                    tariff::Warning::TotalCostClampedToMin.into(),
371                    tariff_elem.as_element(),
372                );
373            }
374        }
375    }
376
377    let report = Report {
378        tariff_id: tariff.id.to_string(),
379        tariff_currency_code: tariff.currency,
380        partial_cdr: PartialCdr {
381            party_id: tariff.party_id.map(CpoId::from),
382            start_date_time: metrics.start_date_time,
383            end_date_time: metrics.end_date_time,
384            currency_code: tariff.currency,
385            total_energy: totals.energy.round_to_ocpi_scale(),
386            total_charging_duration: totals.duration_charging,
387            total_parking_duration: totals.duration_parking,
388            total_cost: total_cost.round_to_ocpi_scale(),
389            total_energy_cost: total_costs.energy.round_to_ocpi_scale(),
390            total_fixed_cost: total_costs.fixed.round_to_ocpi_scale(),
391            total_parking_duration_cost: total_costs.duration_parking.round_to_ocpi_scale(),
392            total_charging_duration_cost: total_costs.duration_charging.round_to_ocpi_scale(),
393            charging_periods,
394        },
395    };
396
397    Ok(report.into_caveat(warnings))
398}
399
400/// An `Event` collector that filters any `Event`s that are after the `session_end_time`.
401struct EventCollector {
402    /// The duration of the session, there is no point in adding events that occur after this.
403    session_duration: TimeDelta,
404
405    /// The list of `Event`s generated from the tariff.
406    events: Vec<Event>,
407}
408
409impl EventCollector {
410    /// Create a new `Event` collector from the duration of a session.
411    fn with_session_duration(session_duration: TimeDelta) -> Self {
412        Self {
413            session_duration,
414            events: vec![],
415        }
416    }
417
418    /// Add an `Event` to the list if the start duration is within the session duration.
419    fn push(&mut self, duration_from_start: TimeDelta, event_kind: EventKind) {
420        if duration_from_start <= self.session_duration {
421            self.events.push(Event {
422                duration_from_start,
423                kind: event_kind,
424            });
425        }
426    }
427
428    /// Return a function (curry) that only requires a `TimeDelta` to call `push`.
429    fn push_with(&mut self, event_kind: EventKind) -> impl FnOnce(TimeDelta) + use<'_> {
430        move |dt| {
431            self.push(dt, event_kind);
432        }
433    }
434
435    /// Consume the collector and return the list of `Event`s.
436    fn into_inner(self) -> Vec<Event> {
437        self.events
438    }
439}
440
441/// Make a `Timeline` of `Event`s using the `Metric`s and `Tariff`.
442fn timeline(
443    timezone: chrono_tz::Tz,
444    metrics: &Metrics,
445    tariff: &tariff::v221::Tariff<'_>,
446) -> Timeline {
447    let Metrics {
448        start_date_time: cdr_start,
449        end_date_time: cdr_end,
450        duration_charging,
451        duration_parking,
452        max_power_supply,
453        max_current_supply,
454
455        energy_supplied: _,
456    } = metrics;
457
458    let mut events = {
459        let session_duration = duration_parking.map(|d| duration_charging.saturating_add(d));
460        let mut events =
461            EventCollector::with_session_duration(session_duration.unwrap_or(*duration_charging));
462
463        events.push(TimeDelta::seconds(0), EventKind::SessionStart);
464        events.push(*duration_charging, EventKind::ChargingEnd);
465        session_duration.map(events.push_with(EventKind::ParkingEnd {
466            start: *duration_charging,
467        }));
468
469        events
470    };
471
472    // True if `min_current` or `max_current` restrictions are defined.
473    // Then we set current to be consumed for each period.
474    let mut emit_current = false;
475
476    // True if `min_power` or `max_power` restrictions are defined.
477    // Then we set power to be consumed for each period.
478    let mut emit_power = false;
479
480    for elem in &tariff.elements {
481        if let Some((time_restrictions, energy_restrictions)) = elem
482            .restrictions
483            .as_ref()
484            .map(tariff::v221::Restrictions::restrictions_by_category)
485        {
486            generate_time_events(
487                &mut events,
488                timezone,
489                *cdr_start..*cdr_end,
490                time_restrictions,
491            );
492
493            let v2x::EnergyRestrictions {
494                min_kwh,
495                max_kwh,
496                min_current,
497                max_current,
498                min_power,
499                max_power,
500            } = energy_restrictions;
501
502            if !emit_current {
503                // If the generator current is contained within the restriction, then we set
504                // an amount of current to be consumed for each period.
505                //
506                // Note: The generator supplies maximum current.
507                emit_current = (min_current..=max_current).contains(&Some(*max_current_supply));
508            }
509
510            if !emit_power {
511                // If the generator power is contained within the restriction, then we set
512                // an amount of power to be consumed for each period.
513                //
514                // Note: The generator supplies maximum power.
515                emit_power = (min_power..=max_power).contains(&Some(*max_power_supply));
516            }
517
518            generate_energy_events(
519                &mut events,
520                metrics.duration_charging,
521                metrics.energy_supplied,
522                min_kwh,
523                max_kwh,
524            );
525        }
526    }
527
528    let events = events.into_inner();
529
530    Timeline {
531        events,
532        emit_current,
533        emit_power,
534    }
535}
536
537/// Generate a list of `Event`s based on the `TimeRestrictions` an `Element` has.
538fn generate_time_events(
539    events: &mut EventCollector,
540    timezone: chrono_tz::Tz,
541    cdr_span: DateTimeSpan,
542    restrictions: v2x::TimeRestrictions,
543) {
544    const MIDNIGHT: NaiveTime = NaiveTime::from_hms_opt(0, 0, 0)
545        .expect("The hour, minute and second values are correct and hardcoded");
546    const ONE_DAY: TimeDelta = TimeDelta::days(1);
547
548    let v2x::TimeRestrictions {
549        start_time,
550        end_time,
551        start_date,
552        end_date,
553        min_duration,
554        max_duration,
555        weekdays,
556    } = restrictions;
557
558    let cdr_duration = cdr_span.end.signed_duration_since(cdr_span.start);
559
560    // If `min_duration` occur within the duration of the chargesession add an event.
561    min_duration
562        .filter(|dt| &cdr_duration < dt)
563        .map(events.push_with(EventKind::MinDuration));
564
565    // If `max_duration` occur within the duration of the chargesession add an event.
566    max_duration
567        .filter(|dt| &cdr_duration < dt)
568        .map(events.push_with(EventKind::MaxDuration));
569
570    // Here we create the `NaiveDateTime` range by combining the `start_date` (`NaiveDate`) and
571    // `start_time` (`NaiveTime`) and the associated `end_date` and `end_time`.
572    //
573    // If `start_time` or `end_time` are `None` then their respective `NaiveDate` is combined
574    // with the `NaiveTime` of `00:00:00` to form a `NaiveDateTime`.
575    //
576    // If the `end_time < start_time` then the period wraps around to the following day.
577    //
578    // See: <https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_tariffs.asciidoc#146-tariffrestrictions-class>
579    let (start_date_time, end_date_time) =
580        if let (Some(start_time), Some(end_time)) = (start_time, end_time) {
581            if end_time < start_time {
582                (
583                    start_date.map(|d| d.and_time(start_time)),
584                    end_date.map(|d| {
585                        let (end_time, _) = end_time.overflowing_add_signed(ONE_DAY);
586                        d.and_time(end_time)
587                    }),
588                )
589            } else {
590                (
591                    start_date.map(|d| d.and_time(start_time)),
592                    end_date.map(|d| d.and_time(end_time)),
593                )
594            }
595        } else {
596            (
597                start_date.map(|d| d.and_time(start_time.unwrap_or(MIDNIGHT))),
598                end_date.map(|d| d.and_time(end_time.unwrap_or(MIDNIGHT))),
599            )
600        };
601
602    // If `start_date` or `end_date` is set we clamp the cdr_span to those dates.
603    // As we are not going to produce any events before `start_date` or after `end_date`.
604    let event_span = clamp_date_time_span(
605        start_date_time.and_then(|d| local_to_utc(timezone, d)),
606        end_date_time.and_then(|d| local_to_utc(timezone, d)),
607        cdr_span,
608    );
609
610    if let Some(start_time) = start_time {
611        gen_naive_time_events(
612            events,
613            &event_span,
614            start_time,
615            &weekdays,
616            EventKind::StartTime,
617        );
618    }
619
620    if let Some(end_time) = end_time {
621        gen_naive_time_events(events, &event_span, end_time, &weekdays, EventKind::EndTime);
622    }
623}
624
625/// Convert a `NaiveDateTime` to a `DateTime<Utc>` using the local timezone.
626///
627/// Return Some `DateTime<Utc>` if the conversion from `NaiveDateTime` results in either a single
628/// or ambiguous `DateTime`. If the conversion is _ambiguous_ due to a _fold_ in the local time,
629/// then we return the earliest `DateTime`.
630fn local_to_utc(timezone: chrono_tz::Tz, date_time: NaiveDateTime) -> Option<DateTime<Utc>> {
631    use chrono::offset::LocalResult;
632
633    let result = date_time.and_local_timezone(timezone);
634
635    let local_date_time = match result {
636        LocalResult::Single(d) => d,
637        LocalResult::Ambiguous(earliest, _latest) => earliest,
638        LocalResult::None => return None,
639    };
640
641    Some(local_date_time.to_utc())
642}
643
644/// Generate `Event`s for the `start_time` or `end_time` restriction.
645fn gen_naive_time_events(
646    events: &mut EventCollector,
647    event_span: &Range<DateTime<Utc>>,
648    time: NaiveTime,
649    weekdays: &v2x::WeekdaySet,
650    kind: EventKind,
651) {
652    let time_delta = time.signed_duration_since(event_span.start.time());
653    let cdr_duration = event_span.end.signed_duration_since(event_span.start);
654
655    // If the start time is before the CDR start, we move it forward 24hours
656    // and test again.
657    let time_delta = if time_delta.num_seconds().is_negative() {
658        let (time_delta, _) = time.overflowing_add_signed(TimeDelta::days(1));
659        time_delta.signed_duration_since(event_span.start.time())
660    } else {
661        time_delta
662    };
663
664    // If the start delta is still negative after moving it forward 24 hours
665    if time_delta.num_seconds().is_negative() {
666        return;
667    }
668
669    // The time is after the CDR start.
670    let Some(remainder) = cdr_duration.checked_sub(&time_delta) else {
671        warn!("TimeDelta overflow");
672        return;
673    };
674
675    if remainder.num_seconds().is_positive() {
676        let duration_from_start = time_delta;
677        let Some(date) = event_span.start.checked_add_signed(duration_from_start) else {
678            warn!("Date out of range");
679            return;
680        };
681
682        if weekdays.contains(date.weekday()) {
683            // The time is before the CDR end.
684            events.push(time_delta, kind);
685        }
686
687        for day in 1..=remainder.num_days() {
688            let Some(duration_from_start) = time_delta.checked_add(&TimeDelta::days(day)) else {
689                warn!("Date out of range");
690                break;
691            };
692            let Some(date) = event_span.start.checked_add_signed(duration_from_start) else {
693                warn!("Date out of range");
694                break;
695            };
696
697            if weekdays.contains(date.weekday()) {
698                events.push(duration_from_start, kind);
699            }
700        }
701    }
702}
703
704/// Generate a list of `Event`s based on the `TimeRestrictions` an `Element` has.
705fn generate_energy_events(
706    events: &mut EventCollector,
707    duration_charging: TimeDelta,
708    energy_supplied: Kwh,
709    min_kwh: Option<Kwh>,
710    max_kwh: Option<Kwh>,
711) {
712    min_kwh
713        .and_then(|kwh| power_to_time(kwh, energy_supplied, duration_charging))
714        .map(events.push_with(EventKind::MinKwh));
715
716    max_kwh
717        .and_then(|kwh| power_to_time(kwh, energy_supplied, duration_charging))
718        .map(events.push_with(EventKind::MaxKwh));
719}
720
721/// Map power usage to time presuming a linear power consumption.
722fn power_to_time(power: Kwh, power_total: Kwh, duration_total: TimeDelta) -> Option<TimeDelta> {
723    use rust_decimal::prelude::ToPrimitive;
724
725    // Find the time that the `min_kwh` amount of power was reached.
726    // It has to be within the charging time.
727    let power = Decimal::from(power);
728    // The total power supplied during the chargesession
729    let power_total = Decimal::from(power_total);
730    // The factor minimum of the total power supplied.
731
732    let Some(factor) = power.checked_div(power_total) else {
733        return Some(TimeDelta::zero());
734    };
735
736    if factor.is_sign_negative() || factor > dec!(1.0) {
737        return None;
738    }
739
740    let duration_from_start = factor.checked_mul(Decimal::from(duration_total.num_seconds()))?;
741    duration_from_start.to_i64().map(TimeDelta::seconds)
742}
743
744/// Generate a list of charging periods for the given tariffs timeline.
745fn charge_periods(metrics: &Metrics, timeline: Timeline) -> Vec<price::Period> {
746    /// Keep track of the whether we are charging or parking.
747    enum ChargingPhase {
748        Charging,
749        Parking,
750    }
751
752    let Metrics {
753        start_date_time: cdr_start,
754        max_power_supply,
755        max_current_supply,
756
757        end_date_time: _,
758        duration_charging: _,
759        duration_parking: _,
760        energy_supplied: _,
761    } = metrics;
762
763    let Timeline {
764        mut events,
765        emit_current,
766        emit_power,
767    } = timeline;
768
769    events.sort_unstable_by_key(|e| e.duration_from_start);
770
771    let mut periods = vec![];
772    let emit_current = emit_current.then_some(*max_current_supply);
773    let emit_power = emit_power.then_some(*max_power_supply);
774    // Charging starts instantly in this model.
775    let mut charging_phase = ChargingPhase::Charging;
776
777    for items in events.windows(2) {
778        let [event, event_next] = items else {
779            unreachable!("The window size is 2");
780        };
781
782        let Event {
783            duration_from_start,
784            kind,
785        } = event;
786
787        if let EventKind::ChargingEnd = kind {
788            charging_phase = ChargingPhase::Parking;
789        }
790
791        let Some(duration) = event_next
792            .duration_from_start
793            .checked_sub(duration_from_start)
794        else {
795            warn!("TimeDelta overflow");
796            break;
797        };
798
799        let Some(start_date_time) = cdr_start.checked_add_signed(*duration_from_start) else {
800            warn!("TimeDelta overflow");
801            break;
802        };
803
804        let consumed = if let ChargingPhase::Charging = charging_phase {
805            let Some(energy) =
806                Decimal::from(*max_power_supply).checked_mul(duration.to_hours_dec())
807            else {
808                warn!("Decimal overflow");
809                break;
810            };
811            price::Consumed {
812                duration_charging: Some(duration),
813                duration_parking: None,
814                energy: Some(Kwh::from_decimal(energy)),
815                current_max: emit_current,
816                current_min: emit_current,
817                power_max: emit_power,
818                power_min: emit_power,
819            }
820        } else {
821            price::Consumed {
822                duration_charging: None,
823                duration_parking: Some(duration),
824                energy: None,
825                current_max: None,
826                current_min: None,
827                power_max: None,
828                power_min: None,
829            }
830        };
831
832        let period = price::Period {
833            start_date_time,
834            consumed,
835        };
836
837        periods.push(period);
838    }
839
840    periods
841}
842
843/// A `DateTimeSpan` bounded by a minimum and a maximum
844///
845/// If the input `DateTimeSpan` is less than `min_date` then this returns `min_date`.
846/// If input is greater than `max_date` then this returns `max_date`.
847/// Otherwise, this returns input `DateTimeSpan`.
848fn clamp_date_time_span(
849    min_date: Option<DateTime<Utc>>,
850    max_date: Option<DateTime<Utc>>,
851    span: DateTimeSpan,
852) -> DateTimeSpan {
853    // Make sure the `min_date` is the earlier of the `min`, max pair.
854    let (min_date, max_date) = (min(min_date, max_date), max(min_date, max_date));
855
856    let start = min_date.filter(|d| &span.start < d).unwrap_or(span.start);
857    let end = max_date.filter(|d| &span.end > d).unwrap_or(span.end);
858
859    DateTimeSpan { start, end }
860}
861
862/// A timeline of events that are used to generate the `ChargePeriods` of the CDR.
863struct Timeline {
864    /// The list of `Event`s generated from the tariff.
865    events: Vec<Event>,
866
867    /// The current is within the \[`min_current`..`max_current`\] range.
868    emit_current: bool,
869
870    /// The power is within the \[`min_power`..`max_power`\] range.
871    emit_power: bool,
872}
873
874/// An event at a time along the timeline.
875struct Event {
876    /// The duration of the Event from the start of the timeline/chargesession.
877    duration_from_start: TimeDelta,
878
879    /// The kind of Event.
880    kind: EventKind,
881}
882
883impl fmt::Debug for Event {
884    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
885        f.debug_struct("Event")
886            .field("duration_from_start", &self.duration_from_start.as_hms())
887            .field("kind", &self.kind)
888            .finish()
889    }
890}
891
892/// The kind of `Event`.
893#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
894enum EventKind {
895    /// The moment a session starts.
896    ///
897    /// This is added to the list of `Event`s so that the algorithm to generate the `ChargingPeriods`
898    /// can iterate over the `Event`s using a window of size 2. The first iteration will always have
899    /// `SessionStart` as the first window element and the `Event` of interest as the second.
900    SessionStart,
901
902    /// The moment charging ends.
903    ///
904    /// Charging starts at time 0. When `ChargingEnd`s, parking starts.
905    /// This could also be the last `Event` of the chargesession.
906    ChargingEnd,
907
908    /// The moment Parking ends
909    ///
910    /// This could also be the last `Event` of the chargesession.
911    /// If a `ParkingEnd` `Event` is present in the `Timeline` then a `ChargingEnd` `Event` will precede it.
912    ParkingEnd {
913        /// The parking started when `ChargingEnd`ed.
914        start: TimeDelta,
915    },
916
917    StartTime,
918
919    EndTime,
920
921    /// Minimum duration in seconds the Charging Session MUST last (inclusive).
922    ///
923    /// When the duration of a Charging Session is longer than the defined value, this `TariffElement` is or becomes active.
924    /// Before that moment, this `TariffElement` is not yet active.
925    MinDuration,
926
927    /// Maximum duration in seconds the Charging Session MUST last (exclusive).
928    ///
929    /// When the duration of a Charging Session is shorter than the defined value, this `TariffElement` is or becomes active.
930    /// After that moment, this `TariffElement` is no longer active.
931    MaxDuration,
932
933    /// Minimum consumed energy in kWh, for example 20, valid from this amount of energy (inclusive) being used.
934    MinKwh,
935
936    /// Maximum consumed energy in kWh, for example 50, valid until this amount of energy (exclusive) being used.
937    MaxKwh,
938}
939
940impl fmt::Debug for EventKind {
941    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
942        match self {
943            Self::SessionStart => write!(f, "SessionStart"),
944            Self::ChargingEnd => write!(f, "ChargingEnd"),
945            Self::ParkingEnd { start } => f
946                .debug_struct("ParkingEnd")
947                .field("start", &start.as_hms())
948                .finish(),
949            Self::StartTime => write!(f, "StartTime"),
950            Self::EndTime => write!(f, "EndTime"),
951            Self::MinDuration => write!(f, "MinDuration"),
952            Self::MaxDuration => write!(f, "MaxDuration"),
953            Self::MinKwh => write!(f, "MinKwh"),
954            Self::MaxKwh => write!(f, "MaxKwh"),
955        }
956    }
957}
958
959/// Broad metrics calculated about the chargesession which is given as input for generating a `Timeline` of `Event`s.
960#[derive(Debug)]
961struct Metrics {
962    /// The end date the generated CDR.
963    end_date_time: DateTime<Utc>,
964
965    /// The start date the generated CDR.
966    start_date_time: DateTime<Utc>,
967
968    /// The time spent charging the battery.
969    ///
970    /// Charging begins instantly and continues without interruption until the battery is full or the
971    /// session time has elapsed.
972    duration_charging: TimeDelta,
973
974    /// The time spent parking after charging the battery.
975    ///
976    /// This duration may be `None` if the battery did not finish charging within the session time.
977    duration_parking: Option<TimeDelta>,
978
979    /// The energy that's supplied during the charging period.
980    energy_supplied: Kwh,
981
982    /// The maximum DC current that can be delivered to the battery.
983    max_current_supply: Ampere,
984
985    /// The maximum DC power(kw) that can be delivered to the battery.
986    max_power_supply: Kw,
987}
988
989/// Validate the `Config` and compute various `Metrics` based on the `Config`s fields.
990#[instrument(skip_all)]
991fn metrics(elem: &tariff::Versioned<'_>, config: &Config) -> Verdict<(Metrics, chrono_tz::Tz)> {
992    const SECS_IN_HOUR: Decimal = dec!(3600);
993
994    let warnings = warning::Set::new();
995
996    let Config {
997        start_date_time,
998        end_date_time,
999        max_power_supply_kw,
1000        requested_kwh: max_energy_battery_kwh,
1001        max_current_supply_amp,
1002        timezone,
1003    } = config;
1004    let duration_session = end_date_time.signed_duration_since(start_date_time);
1005
1006    debug!("duration_session: {}", duration_session.as_hms());
1007
1008    // Std Duration must be positive, if the end time is before the start the conversion will fail.
1009    if duration_session.num_seconds().is_negative() {
1010        return warnings.bail(Warning::StartDateTimeIsAfterEndDateTime, elem.as_element());
1011    }
1012
1013    if duration_session.num_seconds() < MIN_CS_DURATION_SECS {
1014        return warnings.bail(Warning::DurationBelowMinimum, elem.as_element());
1015    }
1016
1017    // The time needed to charge the battery = battery_capacity(kWh) / power(kw)
1018    let duration_full_charge_hours = some_dec_or_bail!(
1019        elem,
1020        max_energy_battery_kwh.checked_div(*max_power_supply_kw),
1021        warnings,
1022        "Unable to calculate changing time"
1023    );
1024    debug!(
1025        "duration_full_charge: {}",
1026        duration_full_charge_hours.as_hms()
1027    );
1028
1029    // The charge duration taking into account that the end of the session can occur before the battery is fully charged.
1030    let charging_duration_hours =
1031        Decimal::min(duration_full_charge_hours, duration_session.to_hours_dec());
1032
1033    let power_supplied_kwh = some_dec_or_bail!(
1034        elem,
1035        max_energy_battery_kwh.checked_div(charging_duration_hours),
1036        warnings,
1037        "Unable to calculate the power supplied during the charging time"
1038    );
1039
1040    // Convert duration from hours to seconds as we work with seconds as the unit of time.
1041    let charging_duration_secs = some_dec_or_bail!(
1042        elem,
1043        charging_duration_hours.checked_mul(SECS_IN_HOUR),
1044        warnings,
1045        "Unable to convert charging time from hours to seconds"
1046    );
1047
1048    let charging_duration_secs = some_dec_or_bail!(
1049        elem,
1050        charging_duration_secs.round().to_i64(),
1051        warnings,
1052        "Unable to convert charging duration Decimal to i64"
1053    );
1054    let duration_charging = TimeDelta::seconds(charging_duration_secs);
1055
1056    let duration_parking = some_dec_or_bail!(
1057        elem,
1058        duration_session.checked_sub(&duration_charging),
1059        warnings,
1060        "Unable to calculate `idle_duration`"
1061    );
1062
1063    debug!(
1064        "duration_charging: {}, duration_parking: {}",
1065        duration_charging.as_hms(),
1066        duration_parking.as_hms()
1067    );
1068
1069    let metrics = Metrics {
1070        end_date_time: *end_date_time,
1071        start_date_time: *start_date_time,
1072        duration_charging,
1073        duration_parking: Some(duration_parking).filter(|dt| dt.num_seconds().is_positive()),
1074        energy_supplied: Kwh::from_decimal(power_supplied_kwh),
1075        max_current_supply: Ampere::from_decimal(*max_current_supply_amp),
1076        max_power_supply: Kw::from_decimal(*max_power_supply_kw),
1077    };
1078
1079    Ok((metrics, *timezone).into_caveat(warnings))
1080}
1081
1082fn is_tariff_active(cdr_start: &DateTime<Utc>, tariff: &tariff::v221::Tariff<'_>) -> bool {
1083    match (tariff.start_date_time, tariff.end_date_time) {
1084        (None, None) => true,
1085        (None, Some(end)) => (..end).contains(cdr_start),
1086        (Some(start), None) => (start..).contains(cdr_start),
1087        (Some(start), Some(end)) => (start..end).contains(cdr_start),
1088    }
1089}
1090
1091#[derive(Debug)]
1092pub enum Warning {
1093    /// A Decimal operation failed.
1094    Decimal(&'static str),
1095
1096    /// The duration of the chargesession is below the minimum allowed.
1097    DurationBelowMinimum,
1098
1099    Price(price::Warning),
1100
1101    /// The `start_date_time` is after the `end_date_time`.
1102    StartDateTimeIsAfterEndDateTime,
1103
1104    Tariff(tariff::Warning),
1105}
1106
1107impl crate::Warning for Warning {
1108    fn id(&self) -> warning::Id {
1109        match self {
1110            Self::Decimal(_) => warning::Id::from_static("decimal_error"),
1111            Self::DurationBelowMinimum => warning::Id::from_static("duration_below_minimum"),
1112            Self::Price(kind) => kind.id(),
1113            Self::StartDateTimeIsAfterEndDateTime => {
1114                warning::Id::from_static("start_time_after_end_time")
1115            }
1116            Self::Tariff(kind) => kind.id(),
1117        }
1118    }
1119}
1120
1121impl fmt::Display for Warning {
1122    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1123        match self {
1124            Self::Decimal(msg) => f.write_str(msg),
1125            Self::DurationBelowMinimum => write!(
1126                f,
1127                "The duration of the chargesession is below the minimum: {MIN_CS_DURATION_SECS}"
1128            ),
1129            Self::Price(warnings) => {
1130                write!(f, "Price warnings: {warnings:?}")
1131            }
1132            Self::StartDateTimeIsAfterEndDateTime => {
1133                write!(f, "The `start_date_time` is after the `end_date_time`")
1134            }
1135            Self::Tariff(warnings) => {
1136                write!(f, "Tariff warnings: {warnings:?}")
1137            }
1138        }
1139    }
1140}
1141
1142from_warning_all!(
1143    tariff::Warning => Warning::Tariff,
1144    price::Warning => Warning::Price
1145);