use alloc::vec::Vec;
use core::fmt;
use crate::iso2::{
ChargingProfile, PMaxScheduleEntryChoice, PhysicalValue, PowerDeliveryReq, ResponseCode,
SAScheduleList, SAScheduleTuple, UnitSymbol,
};
pub type Milliwatts = i64;
pub fn power_mw(value: &PhysicalValue) -> Result<Milliwatts, ProfileError> {
if value.unit != UnitSymbol::W {
return Err(ProfileError::NotWatts { unit: value.unit });
}
let exponent = i32::from(value.multiplier) + 3;
let Some(scale) = u32::try_from(exponent).ok().filter(|e| *e <= 6).map(|e| 10i64.pow(e)) else {
return Err(ProfileError::MultiplierOutOfRange { multiplier: value.multiplier });
};
Ok(i64::from(value.value) * scale)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Step {
start: u32,
end: Option<u32>,
power: Milliwatts,
}
impl Step {
const fn overlaps(&self, other: &Self) -> bool {
let a_end = match self.end {
Some(e) => e,
None => u32::MAX,
};
let b_end = match other.end {
Some(e) => e,
None => u32::MAX,
};
self.start < b_end && other.start < a_end
}
const fn overlap_start(&self, other: &Self) -> u32 {
if self.start > other.start { self.start } else { other.start }
}
}
fn schedule_steps(tuple: &SAScheduleTuple) -> Result<Vec<Step>, ProfileError> {
let entries = &tuple.p_max_schedule.p_max_schedule_entry;
let mut steps: Vec<Step> = Vec::with_capacity(entries.len());
for (index, entry) in entries.iter().enumerate() {
let PMaxScheduleEntryChoice::RelativeTimeInterval(interval) = &entry.choice else {
return Err(ProfileError::ScheduleNotRelative { index });
};
let power = power_mw(&entry.p_max)?;
if let Some(previous) = steps.last_mut() {
if interval.start <= previous.start {
return Err(ProfileError::ScheduleOutOfOrder { index });
}
previous.end = Some(interval.start);
}
steps.push(Step { start: interval.start, end: None, power });
}
if let Some(last) = steps.last_mut()
&& let Some(PMaxScheduleEntryChoice::RelativeTimeInterval(interval)) =
entries.last().map(|e| &e.choice)
&& let Some(duration) = interval.duration
{
last.end = Some(last.start.saturating_add(duration));
}
Ok(steps)
}
fn profile_steps(profile: &ChargingProfile) -> Result<Vec<Step>, ProfileError> {
let mut steps: Vec<Step> = Vec::with_capacity(profile.profile_entry.len());
for (index, entry) in profile.profile_entry.iter().enumerate() {
let power = power_mw(&entry.charging_profile_entry_max_power)?;
if let Some(previous) = steps.last_mut() {
if entry.charging_profile_entry_start <= previous.start {
return Err(ProfileError::ProfileOutOfOrder { index });
}
previous.end = Some(entry.charging_profile_entry_start);
}
steps.push(Step { start: entry.charging_profile_entry_start, end: None, power });
}
Ok(steps)
}
pub fn pmax_at(tuple: &SAScheduleTuple, offset: u32) -> Result<Option<Milliwatts>, ProfileError> {
let steps = schedule_steps(tuple)?;
Ok(steps
.iter()
.find(|s| s.start <= offset && s.end.is_none_or(|end| offset < end))
.map(|s| s.power))
}
#[must_use]
pub fn coverage(tuple: &SAScheduleTuple) -> Option<u32> {
let steps = schedule_steps(tuple).ok()?;
steps.last()?.end
}
pub fn check_power_delivery(
offered: &SAScheduleList,
req: &PowerDeliveryReq,
) -> Result<(), ProfileError> {
let tuple = offered
.sa_schedule_tuple
.iter()
.find(|t| t.sa_schedule_tuple_id == req.sa_schedule_tuple_id)
.ok_or(ProfileError::UnknownScheduleTuple { requested: req.sa_schedule_tuple_id })?;
let Some(profile) = req.charging_profile.as_ref() else { return Ok(()) };
check_profile(tuple, profile)
}
pub fn check_profile(
tuple: &SAScheduleTuple,
profile: &ChargingProfile,
) -> Result<(), ProfileError> {
let limits = schedule_steps(tuple)?;
let wanted = profile_steps(profile)?;
for want in &wanted {
for limit in &limits {
if want.overlaps(limit) && want.power > limit.power {
return Err(ProfileError::ExceedsPMax {
at: want.overlap_start(limit),
requested: want.power,
permitted: limit.power,
});
}
}
}
Ok(())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ProfileError {
UnknownScheduleTuple {
requested: u8,
},
ExceedsPMax {
at: u32,
requested: Milliwatts,
permitted: Milliwatts,
},
NotWatts {
unit: UnitSymbol,
},
MultiplierOutOfRange {
multiplier: i8,
},
ProfileOutOfOrder {
index: usize,
},
ScheduleOutOfOrder {
index: usize,
},
ScheduleNotRelative {
index: usize,
},
}
impl ProfileError {
#[must_use]
pub const fn response_code(self) -> ResponseCode {
match self {
Self::UnknownScheduleTuple { .. } => ResponseCode::FAILEDTariffSelectionInvalid,
_ => ResponseCode::FAILEDChargingProfileInvalid,
}
}
#[must_use]
pub const fn is_local_fault(self) -> bool {
matches!(self, Self::ScheduleOutOfOrder { .. } | Self::ScheduleNotRelative { .. })
}
}
impl fmt::Display for ProfileError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::UnknownScheduleTuple { requested } => {
write!(f, "SAScheduleTupleID {requested} was never offered")
}
Self::ExceedsPMax { at, requested, permitted } => write!(
f,
"the charging profile asks for {requested} mW at t+{at}s, \
where the schedule permits {permitted} mW"
),
Self::NotWatts { unit } => write!(f, "a power was given in {unit:?}, not watts"),
Self::MultiplierOutOfRange { multiplier } => {
write!(f, "multiplier {multiplier} is outside the schema's -3..=3")
}
Self::ProfileOutOfOrder { index } => {
write!(f, "charging profile entry {index} does not start after the one before it")
}
Self::ScheduleOutOfOrder { index } => {
write!(f, "offered schedule entry {index} does not start after the one before it")
}
Self::ScheduleNotRelative { index } => {
write!(f, "offered schedule entry {index} has no RelativeTimeInterval")
}
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for ProfileError {}
#[cfg(test)]
mod tests {
use alloc::vec;
use super::*;
use crate::iso2::{
ChargeProgress, PMaxSchedule, PMaxScheduleEntry, ProfileEntry, RelativeTimeInterval,
};
const fn watts(multiplier: i8, value: i16) -> PhysicalValue {
PhysicalValue { multiplier, unit: UnitSymbol::W, value }
}
fn schedule(id: u8, entries: &[(u32, Option<u32>, PhysicalValue)]) -> SAScheduleTuple {
SAScheduleTuple {
sa_schedule_tuple_id: id,
p_max_schedule: PMaxSchedule {
p_max_schedule_entry: entries
.iter()
.map(|(start, duration, p_max)| PMaxScheduleEntry {
choice: PMaxScheduleEntryChoice::RelativeTimeInterval(
RelativeTimeInterval { start: *start, duration: *duration },
),
p_max: p_max.clone(),
})
.collect(),
},
sales_tariff: None,
}
}
fn profile(entries: &[(u32, PhysicalValue)]) -> ChargingProfile {
ChargingProfile {
profile_entry: entries
.iter()
.map(|(start, power)| ProfileEntry {
charging_profile_entry_start: *start,
charging_profile_entry_max_power: power.clone(),
charging_profile_entry_max_number_of_phases_in_use: None,
})
.collect(),
}
}
#[test]
fn a_multiplier_is_a_power_of_ten_not_decoration() {
assert_eq!(power_mw(&watts(1, 1100)).unwrap(), 11_000_000);
assert_eq!(power_mw(&watts(0, 11_000)).unwrap(), 11_000_000);
assert_eq!(power_mw(&watts(3, 11)).unwrap(), 11_000_000);
assert_eq!(power_mw(&watts(-3, 1)).unwrap(), 1);
assert_eq!(power_mw(&watts(-1, 5)).unwrap(), 500);
}
#[test]
fn a_power_that_is_not_a_power_is_refused() {
let amps = PhysicalValue { multiplier: 0, unit: UnitSymbol::A, value: 32 };
assert_eq!(power_mw(&s), Err(ProfileError::NotWatts { unit: UnitSymbol::A }));
assert_eq!(power_mw(&watts(0, 32)).unwrap(), 32_000);
}
#[test]
fn a_multiplier_outside_the_facet_is_refused_not_saturated() {
for bad in [-4i8, 4, i8::MIN, i8::MAX] {
assert_eq!(
power_mw(&watts(bad, 1)),
Err(ProfileError::MultiplierOutOfRange { multiplier: bad })
);
}
}
#[test]
fn a_profile_inside_the_schedule_is_accepted() {
let tuple = schedule(1, &[(0, Some(3600), watts(1, 1100))]);
assert!(check_profile(&tuple, &profile(&[(0, watts(0, 7400))])).is_ok());
assert!(check_profile(&tuple, &profile(&[(0, watts(1, 1100))])).is_ok());
}
#[test]
fn a_profile_over_the_limit_names_the_instant_it_goes_over() {
let tuple = schedule(1, &[(0, None, watts(1, 1100)), (1800, Some(1800), watts(0, 3700))]);
let err = check_profile(&tuple, &profile(&[(0, watts(0, 7400))])).unwrap_err();
assert_eq!(
err,
ProfileError::ExceedsPMax { at: 1800, requested: 7_400_000, permitted: 3_700_000 }
);
assert_eq!(err.response_code(), ResponseCode::FAILEDChargingProfileInvalid);
assert!(!err.is_local_fault());
assert!(
check_profile(&tuple, &profile(&[(0, watts(0, 7400)), (1800, watts(0, 3700))])).is_ok()
);
}
#[test]
fn a_start_is_also_the_stop_of_the_interval_before_it() {
let tuple = schedule(1, &[(0, None, watts(1, 1100)), (300, Some(300), watts(0, 1000))]);
assert_eq!(pmax_at(&tuple, 0).unwrap(), Some(11_000_000));
assert_eq!(pmax_at(&tuple, 299).unwrap(), Some(11_000_000));
assert_eq!(pmax_at(&tuple, 300).unwrap(), Some(1_000_000));
assert_eq!(pmax_at(&tuple, 599).unwrap(), Some(1_000_000));
assert_eq!(pmax_at(&tuple, 600).unwrap(), None);
assert_eq!(coverage(&tuple), Some(600));
}
#[test]
fn a_schedule_with_no_duration_states_no_end() {
let tuple = schedule(1, &[(0, None, watts(1, 1100))]);
assert_eq!(coverage(&tuple), None);
assert_eq!(pmax_at(&tuple, u32::MAX).unwrap(), Some(11_000_000));
}
#[test]
fn nothing_is_exceeded_where_the_schedule_says_nothing() {
let tuple = schedule(1, &[(0, Some(600), watts(0, 3700))]);
assert!(
check_profile(&tuple, &profile(&[(0, watts(0, 3700)), (600, watts(1, 2000))])).is_ok()
);
assert!(
check_profile(&tuple, &profile(&[(0, watts(0, 3700)), (599, watts(1, 2000))])).is_err()
);
}
#[test]
fn a_tuple_id_that_was_never_offered_is_a_tariff_selection_failure() {
let offered =
SAScheduleList { sa_schedule_tuple: vec![schedule(1, &[(0, None, watts(1, 1100))])] };
let req = PowerDeliveryReq {
charge_progress: ChargeProgress::Start,
sa_schedule_tuple_id: 7,
charging_profile: Some(profile(&[(0, watts(0, 100))])),
choice: None,
};
let err = check_power_delivery(&offered, &req).unwrap_err();
assert_eq!(err, ProfileError::UnknownScheduleTuple { requested: 7 });
assert_eq!(err.response_code(), ResponseCode::FAILEDTariffSelectionInvalid);
}
#[test]
fn the_right_tuple_of_several_is_the_one_checked() {
let offered = SAScheduleList {
sa_schedule_tuple: vec![
schedule(1, &[(0, None, watts(0, 3700))]),
schedule(2, &[(0, None, watts(1, 2200))]),
],
};
let mut req = PowerDeliveryReq {
charge_progress: ChargeProgress::Start,
sa_schedule_tuple_id: 2,
charging_profile: Some(profile(&[(0, watts(1, 1100))])),
choice: None,
};
assert!(check_power_delivery(&offered, &req).is_ok(), "11 kW fits tuple 2's 22 kW");
req.sa_schedule_tuple_id = 1;
assert!(check_power_delivery(&offered, &req).is_err(), "...and not tuple 1's 3.7 kW");
}
#[test]
fn no_profile_at_all_is_not_a_profile_that_exceeds_anything() {
let offered =
SAScheduleList { sa_schedule_tuple: vec![schedule(1, &[(0, None, watts(0, 0))])] };
let req = PowerDeliveryReq {
charge_progress: ChargeProgress::Stop,
sa_schedule_tuple_id: 1,
charging_profile: None,
choice: None,
};
assert!(check_power_delivery(&offered, &req).is_ok());
}
#[test]
fn an_out_of_order_list_describes_no_interval_at_all() {
let tuple = schedule(1, &[(0, None, watts(0, 100))]);
assert_eq!(
check_profile(&tuple, &profile(&[(600, watts(0, 10)), (300, watts(0, 10))])),
Err(ProfileError::ProfileOutOfOrder { index: 1 })
);
assert_eq!(
check_profile(&tuple, &profile(&[(300, watts(0, 10)), (300, watts(0, 10))])),
Err(ProfileError::ProfileOutOfOrder { index: 1 })
);
}
#[test]
fn a_malformed_offer_is_this_station_s_own_fault_and_says_so() {
let backwards = schedule(1, &[(600, None, watts(0, 10)), (0, None, watts(0, 10))]);
let err = check_profile(&backwards, &profile(&[(0, watts(0, 1))])).unwrap_err();
assert_eq!(err, ProfileError::ScheduleOutOfOrder { index: 1 });
assert!(err.is_local_fault(), "the vehicle did nothing wrong here");
let abstract_interval = SAScheduleTuple {
sa_schedule_tuple_id: 1,
p_max_schedule: PMaxSchedule {
p_max_schedule_entry: vec![PMaxScheduleEntry {
choice: PMaxScheduleEntryChoice::TimeInterval(crate::iso2::Interval),
p_max: watts(0, 100),
}],
},
sales_tariff: None,
};
let err = check_profile(&abstract_interval, &profile(&[(0, watts(0, 1))])).unwrap_err();
assert_eq!(err, ProfileError::ScheduleNotRelative { index: 0 });
assert!(err.is_local_fault());
}
#[test]
fn a_later_profile_entry_is_checked_against_an_earlier_schedule_entry_it_overlaps() {
let tuple = schedule(1, &[(0, None, watts(0, 1000)), (300, Some(300), watts(1, 1100))]);
let err = check_profile(&tuple, &profile(&[(0, watts(0, 500)), (100, watts(0, 5000))]))
.unwrap_err();
assert_eq!(
err,
ProfileError::ExceedsPMax { at: 100, requested: 5_000_000, permitted: 1_000_000 }
);
}
}