use chrono::TimeDelta;
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
use crate::{
assert_approx_eq, currency,
duration::{test::FromHms as _, ToHoursDecimal as _},
generate::{self, ChargingPeriod, Dimension, DimensionType, PartialCdr},
json::FromJson as _,
price, tariff, Ampere, Kw, Kwh, Money, Price,
};
use super::test;
const DATE: &str = "2025-11-10";
fn config_ten_mins() -> generate::Config {
generate::Config {
timezone: chrono_tz::Europe::Amsterdam,
start_date_time: test::datetime_utc(DATE, "15:02:12"),
end_date_time: test::datetime_utc(DATE, "15:12:12"),
max_power_supply_kw: Decimal::from(24),
requested_kwh: Decimal::from(80),
max_current_supply_amp: Decimal::from(4),
}
}
fn config_two_mins() -> generate::Config {
generate::Config {
timezone: chrono_tz::Europe::Amsterdam,
start_date_time: test::datetime_utc(DATE, "15:02:12"),
end_date_time: test::datetime_utc(DATE, "15:04:12"),
max_power_supply_kw: dec!(276),
requested_kwh: dec!(0.2),
max_current_supply_amp: Decimal::from(300),
}
}
fn config_six_hours() -> generate::Config {
generate::Config {
timezone: chrono_tz::Europe::Amsterdam,
start_date_time: test::datetime_utc(DATE, "15:02:12"),
end_date_time: test::datetime_utc(DATE, "19:02:12"),
max_power_supply_kw: Decimal::from(24),
requested_kwh: Decimal::from(80),
max_current_supply_amp: Decimal::from(4),
}
}
#[track_caller]
fn periods(tariff_json: &str, config: &generate::Config) -> Vec<price::Period> {
let tariff = tariff::parse(tariff_json).unwrap().unwrap_certain();
let (metrics, _tz) = generate::metrics(&tariff, config).unwrap().unwrap();
let tariff = tariff::v221::Tariff::from_json(tariff.as_element())
.unwrap()
.unwrap();
let timeline = super::timeline(chrono_tz::Tz::Europe__Amsterdam, &metrics, &tariff);
super::charge_periods(&metrics, timeline)
}
#[test]
fn should_partially_charge_battery() {
const TARIFF_JSON: &str = r#"{
"country_code": "DE",
"party_id": "ALL",
"id": "1",
"currency": "EUR",
"type": "REGULAR",
"elements": [
{
"price_components": [{
"type": "ENERGY",
"price": 0.50,
"vat": 20.0,
"step_size": 1
}]
}
],
"last_updated": "2018-12-05T12:01:09Z"
}
"#;
crate::test::setup();
let config = config_ten_mins();
let periods = periods(TARIFF_JSON, &config);
let [period] = periods
.try_into()
.expect("There are no restrictions so there should be one big period");
let price::Period {
start_date_time,
consumed,
} = period;
assert_eq!(start_date_time, test::datetime_utc(DATE, "15:02:12"));
let price::Consumed {
duration_charging,
duration_parking,
energy,
current_max,
current_min,
power_max,
power_min,
} = consumed;
assert_eq!(
duration_charging,
Some(TimeDelta::minutes(10)),
"The battery is charged for 10 mins and the plug is pulled"
);
let max_power_kw_input = config.max_power_supply_kw;
let avg_power = Decimal::from(energy.unwrap())
.checked_div(duration_charging.unwrap().to_hours_dec())
.unwrap();
assert!(avg_power <= max_power_kw_input);
assert_eq!(duration_parking, None, "The battery never fully charges");
assert_approx_eq!(
energy,
Some(Kwh::from(4)),
"The energy supplied is 24 Kwh from a session duration of 10 Mins (0.1666 Hours), so 4 Kwh should be consumed"
);
assert_approx_eq!(
current_max,
None,
"There is no `min_current` or `max_current` restriction defined"
);
assert_approx_eq!(
current_min,
None,
"There is no `min_current` or `max_current` defined"
);
assert_approx_eq!(
power_max,
None,
"There is no `min_power` or `max_power` defined"
);
assert_approx_eq!(
power_min,
None,
"There is no `min_power` or `max_power` defined"
);
}
#[test]
fn should_fully_charge_then_park() {
const TARIFF_JSON: &str = r#"{
"country_code": "DE",
"party_id": "ALL",
"id": "1",
"currency": "EUR",
"type": "REGULAR",
"elements": [
{
"price_components": [{
"type": "ENERGY",
"price": 0.50,
"vat": 20.0,
"step_size": 1
}]
}
],
"last_updated": "2018-12-05T12:01:09Z"
}
"#;
crate::test::setup();
let config = config_six_hours();
let periods = periods(TARIFF_JSON, &config);
let [period_charging, period_parking] = periods.try_into().expect(
"There are no restrictions so there should be period for charging and a second for parking",
);
{
let price::Period {
start_date_time,
consumed,
} = period_charging;
assert_eq!(start_date_time, test::datetime_utc(DATE, "15:02:12"));
let price::Consumed {
duration_charging,
duration_parking,
energy,
current_max,
current_min,
power_max,
power_min,
} = consumed;
assert_eq!(
duration_charging,
Some(TimeDelta::from_hms("03:20:00")),
"The battery is charged fully"
);
assert_eq!(duration_parking, None);
assert_approx_eq!(energy, Some(Kwh::from(80)));
let max_power_kw_input = config.max_power_supply_kw;
let avg_power = Decimal::from(energy.unwrap())
.checked_div(duration_charging.unwrap().to_hours_dec())
.unwrap();
assert!(avg_power <= max_power_kw_input);
assert_approx_eq!(
current_max,
None,
"There is no `min_current` or `max_current` restriction defined"
);
assert_approx_eq!(
current_min,
None,
"There is no `min_current` or `max_current` defined"
);
assert_approx_eq!(
power_max,
None,
"There is no `min_power` or `max_power` defined"
);
assert_approx_eq!(
power_min,
None,
"There is no `min_power` or `max_power` defined"
);
}
{
let price::Period {
start_date_time,
consumed,
} = period_parking;
assert_eq!(start_date_time, test::datetime_utc(DATE, "18:22:12"));
let price::Consumed {
duration_charging,
duration_parking,
energy,
current_max,
current_min,
power_max,
power_min,
} = consumed;
assert_eq!(
duration_charging, None,
"The battery is charged fully then parking begins"
);
assert_eq!(
duration_parking,
Some(TimeDelta::from_hms("00:40:00")),
"The battery is charged fully then parking begins"
);
assert_approx_eq!(energy, None);
assert_approx_eq!(
current_max,
None,
"There is no `min_current` or `max_current` restriction defined"
);
assert_approx_eq!(
current_min,
None,
"There is no `min_current` or `max_current` defined"
);
assert_approx_eq!(
power_max,
None,
"There is no `min_power` or `max_power` defined"
);
assert_approx_eq!(
power_min,
None,
"There is no `min_power` or `max_power` defined"
);
}
}
#[test]
fn should_generate_power() {
const TARIFF_JSON: &str = r#"{
"country_code": "DE",
"party_id": "ALL",
"id": "1",
"currency": "EUR",
"type": "REGULAR",
"elements": [
{
"price_components": [{
"type": "ENERGY",
"price": 0.60,
"vat": 20.0,
"step_size": 1
}],
"restrictions": {
"max_power": 16.00
}
},
{
"price_components": [{
"type": "ENERGY",
"price": 0.70,
"vat": 20.0,
"step_size": 1
}],
"restrictions": {
"max_power": 32.00
}
},
{
"price_components": [{
"type": "ENERGY",
"price": 0.50,
"vat": 20.0,
"step_size": 1
}]
}
],
"last_updated": "2018-12-05T12:01:09Z"
}
"#;
crate::test::setup();
let config = config_ten_mins();
let tariff_elem = tariff::parse(TARIFF_JSON).unwrap().unwrap_certain();
let (metrics, _tz) = generate::metrics(&tariff_elem, &config).unwrap().unwrap();
let tariff = tariff::v221::Tariff::from_json(tariff_elem.as_element())
.unwrap()
.unwrap();
let timeline = super::timeline(chrono_tz::Tz::Europe__Amsterdam, &metrics, &tariff);
let periods = super::charge_periods(&metrics, timeline);
let [ref period] = periods
.try_into()
.expect("There are no restrictions so there should be one big period");
let price::Period {
start_date_time,
consumed,
} = period;
assert_eq!(*start_date_time, test::datetime_utc(DATE, "15:02:12"));
let price::Consumed {
duration_charging,
duration_parking,
energy,
current_max,
current_min,
power_max,
power_min,
} = consumed;
assert_eq!(
*duration_charging,
Some(TimeDelta::minutes(10)),
"The battery is charged for 10 mins and the plug is pulled"
);
assert_eq!(*duration_parking, None, "The battery never fully charges");
assert_approx_eq!(
energy,
Some(Kwh::from(4)),
"The energy supplied is 24 Kwh from a session duration of 10 Mins (0.1666 Hours), so 4 Kwh should be consumed"
);
assert_approx_eq!(
current_max,
None,
"There is no `min_current` or `max_current` restriction defined"
);
assert_approx_eq!(
current_min,
None,
"There is no `min_current` or `max_current` defined"
);
assert_approx_eq!(
power_max,
Some(Kw::from(24)),
"There is a `max_power` defined"
);
assert_approx_eq!(
power_min,
Some(Kw::from(24)),
"There is a `min_power` defined"
);
let report = generate::cdr_from_tariff(&tariff_elem, &config).unwrap();
let (report, warnings) = report.into_parts();
assert!(warnings.is_empty(), "{:#?}", warnings.path_id_map());
let PartialCdr {
party_id,
start_date_time,
end_date_time,
currency_code,
total_energy,
total_charging_duration,
total_parking_duration,
total_cost,
total_energy_cost,
total_fixed_cost,
total_parking_duration_cost,
total_charging_duration_cost,
charging_periods,
} = report.partial_cdr;
assert_eq!(
party_id.as_ref().map(ToString::to_string).as_deref(),
Some("DEALL")
);
assert_eq!(currency_code, currency::Code::Eur);
assert_eq!(start_date_time, test::datetime_utc(DATE, "15:02:12"));
assert_eq!(end_date_time, test::datetime_utc(DATE, "15:12:12"));
assert_approx_eq!(
total_cost,
Some(Price {
excl_vat: Money::from(2.80),
incl_vat: Some(Money::from(3.36))
}),
"The power input is 24 Kw and the second tariff element with a price per KwH or 0.70 should be used."
);
assert_eq!(
total_charging_duration,
Some(TimeDelta::minutes(10)),
"The charging session is 10 min and is stopped before the battery is fully charged."
);
assert_eq!(
total_parking_duration, None,
"There is no parking time since the battery never fully charged."
);
assert_approx_eq!(total_energy, Some(Kwh::from(4)));
assert_approx_eq!(
total_energy_cost,
Some(Price {
excl_vat: Money::from(2.80),
incl_vat: Some(Money::from(3.36))
}),
"The cost per KwH is 70 cents and the VAT is 20%."
);
assert_eq!(total_fixed_cost, None, "There are no fixed costs.");
assert_eq!(
total_parking_duration_cost, None,
"There is no parking cost as there is no parking time."
);
assert_eq!(
total_charging_duration_cost, None,
"There are no time costs defined in the tariff."
);
let [period] = charging_periods
.try_into()
.expect("There should be one period.");
let ChargingPeriod {
start_date_time,
dimensions,
tariff_id,
} = period;
assert_eq!(start_date_time, test::datetime_utc(DATE, "15:02:12"));
assert_eq!(tariff_id.as_deref(), Some("1"));
let [energy, time] = dimensions
.try_into()
.expect("There should be an energy dimension");
let Dimension {
dimension_type,
volume: energy_volume,
} = energy;
assert_eq!(dimension_type, DimensionType::Energy);
assert_approx_eq!(energy_volume, dec!(4.0));
let Dimension {
dimension_type,
volume,
} = time;
assert_eq!(dimension_type, DimensionType::Time);
assert_approx_eq!(volume, TimeDelta::minutes(10).to_hours_dec());
let max_power_kw_input = config.max_power_supply_kw;
let avg_power = energy_volume.checked_div(volume).unwrap();
assert!(avg_power <= max_power_kw_input);
}
#[test]
fn should_generate_current() {
const TARIFF_JSON: &str = r#"{
"country_code": "DE",
"party_id": "ALL",
"id": "1",
"currency": "EUR",
"type": "REGULAR",
"elements": [
{
"price_components": [{
"type": "ENERGY",
"price": 0.60,
"vat": 20.0,
"step_size": 1
}],
"restrictions": {
"max_current": 2
}
},
{
"price_components": [{
"type": "ENERGY",
"price": 0.70,
"vat": 20.0,
"step_size": 1
}],
"restrictions": {
"max_current": 4
}
},
{
"price_components": [{
"type": "ENERGY",
"price": 0.50,
"vat": 20.0,
"step_size": 1
}]
}
],
"last_updated": "2018-12-05T12:01:09Z"
}
"#;
crate::test::setup();
let config = config_ten_mins();
let tariff_elem = tariff::parse(TARIFF_JSON).unwrap().unwrap_certain();
let (metrics, _tz) = generate::metrics(&tariff_elem, &config).unwrap().unwrap();
let tariff = tariff::v221::Tariff::from_json(tariff_elem.as_element())
.unwrap()
.unwrap();
let timeline = super::timeline(chrono_tz::Tz::Europe__Amsterdam, &metrics, &tariff);
let periods = super::charge_periods(&metrics, timeline);
let [ref period] = periods
.try_into()
.expect("There are no restrictions so there should be one big period");
let price::Period {
start_date_time,
consumed,
} = period;
assert_eq!(*start_date_time, test::datetime_utc(DATE, "15:02:12"));
let price::Consumed {
duration_charging,
duration_parking,
current_max,
current_min,
energy,
power_max,
power_min,
} = consumed;
assert_eq!(
*duration_charging,
Some(TimeDelta::minutes(10)),
"The battery is charged for 10 mins and the plug is pulled"
);
assert_eq!(*duration_parking, None, "The battery never fully charges");
assert_approx_eq!(
energy,
Some(Kwh::from(4)),
"The energy supplied is 24 Kwh from a session duration of 10 Mins (0.1666 Hours), so 4 Kwh should be consumed"
);
assert_approx_eq!(
current_max,
Some(Ampere::from(4)),
"There is a `max_current` restriction defined"
);
assert_approx_eq!(
current_min,
Some(Ampere::from(4)),
"There is a `max_current` restriction defined"
);
assert_approx_eq!(
power_max,
None,
"There is no `min_power` or `max_power` defined"
);
assert_approx_eq!(
power_min,
None,
"There is no `min_power` or `max_power` defined"
);
let report = generate::cdr_from_tariff(&tariff_elem, &config).unwrap();
let (report, warnings) = report.into_parts();
assert!(warnings.is_empty(), "{:#?}", warnings.path_id_map());
let PartialCdr {
party_id,
start_date_time,
end_date_time,
currency_code,
total_energy,
total_charging_duration,
total_parking_duration,
total_cost,
total_energy_cost,
total_fixed_cost,
total_parking_duration_cost,
total_charging_duration_cost,
charging_periods,
} = report.partial_cdr;
assert_eq!(
party_id.as_ref().map(ToString::to_string).as_deref(),
Some("DEALL")
);
assert_eq!(currency_code, currency::Code::Eur);
assert_eq!(start_date_time, test::datetime_utc(DATE, "15:02:12"));
assert_eq!(end_date_time, test::datetime_utc(DATE, "15:12:12"));
assert_approx_eq!(
total_cost,
Some(Price {
excl_vat: Money::from(2.00),
incl_vat: Some(Money::from(2.40))
}),
"The power input is 24 Kw and the second tariff element with a price per KwH or 0.70 should be used."
);
assert_eq!(
total_charging_duration,
Some(TimeDelta::minutes(10)),
"The charging session is 10 min and is stopped before the battery is fully charged."
);
assert_eq!(
total_parking_duration, None,
"There is no parking time since the battery never fully charged."
);
assert_approx_eq!(total_energy, Some(Kwh::from(4)));
assert_approx_eq!(
total_energy_cost,
Some(Price {
excl_vat: Money::from(2.00),
incl_vat: Some(Money::from(2.40))
}),
"The cost per KwH is 70 cents and the VAT is 20%."
);
assert_eq!(total_fixed_cost, None, "There are no fixed costs.");
assert_eq!(
total_parking_duration_cost, None,
"There is no parking cost as there is no parking time."
);
assert_eq!(
total_charging_duration_cost, None,
"There are no time costs defined in the tariff."
);
let [period] = charging_periods
.try_into()
.expect("There should be one period.");
let ChargingPeriod {
start_date_time,
dimensions,
tariff_id,
} = period;
assert_eq!(start_date_time, test::datetime_utc(DATE, "15:02:12"));
assert_eq!(tariff_id.as_deref(), Some("1"));
let [energy, time] = dimensions
.try_into()
.expect("There should be an energy dimension");
let Dimension {
dimension_type,
volume: energy_volume,
} = energy;
assert_eq!(dimension_type, DimensionType::Energy);
assert_approx_eq!(energy_volume, dec!(4.0));
let Dimension {
dimension_type,
volume,
} = time;
assert_eq!(dimension_type, DimensionType::Time);
assert_approx_eq!(volume, TimeDelta::minutes(10).to_hours_dec());
let max_power_kw_input = config.max_power_supply_kw;
let avg_power = energy_volume.checked_div(volume).unwrap();
assert!(avg_power <= max_power_kw_input);
}
#[test]
fn should_partially_charge_battery_and_not_exceed_max_power() {
const TARIFF_JSON: &str = r#"{
"id": "22",
"currency": "EUR",
"elements": [
{
"price_components": [
{
"type": "ENERGY",
"price": "2.0000",
"step_size": 8000
}
]
},
{
"price_components": [
{
"type": "ENERGY",
"price": "2.0000",
"step_size": 5000
}
]
},
{
"price_components": [
{
"type": "ENERGY",
"price": "4.0000",
"step_size": 2000
}
]
}
],
"party_id": "ALL",
"country_code": "DE",
"last_updated": "2018-12-18T17:07:11Z"
}
"#;
crate::test::setup();
let config = config_two_mins();
let periods = periods(TARIFF_JSON, &config);
let [charging_period, _parking_period] = periods
.try_into()
.expect("The amount of energy consumed can be reached in small, time window");
let price::Period {
start_date_time,
consumed,
} = charging_period;
assert_eq!(start_date_time, test::datetime_utc(DATE, "15:02:12"));
let price::Consumed {
duration_charging,
duration_parking: _,
energy,
current_max,
current_min,
power_max,
power_min,
} = consumed;
let charge_secs = duration_charging.unwrap().num_seconds();
assert_eq!(
charge_secs, 2,
"The battery is charged for 2.61 seconds to reach the requested charge"
);
let max_power_kw_input = config.max_power_supply_kw;
let avg_power = Decimal::from(energy.unwrap())
.checked_div(duration_charging.unwrap().to_hours_dec())
.unwrap();
assert!(avg_power <= max_power_kw_input);
assert_approx_eq!(
energy,
Some(Kwh::from(0.2)),
"The energy supplied is 0.2 Kwh from a session duration of 2 Mins (0.0333 Hours), so 0.2 Kwh should be consumed"
);
assert_approx_eq!(
current_max,
None,
"There is no `min_current` or `max_current` restriction defined"
);
assert_approx_eq!(
current_min,
None,
"There is no `min_current` or `max_current` defined"
);
assert_approx_eq!(
power_max,
None,
"There is no `min_power` or `max_power` defined"
);
assert_approx_eq!(
power_min,
None,
"There is no `min_power` or `max_power` defined"
);
}