use std::collections::BTreeSet;
use ocpp_core::DateTime;
use serde::Serialize;
use super::model::{Kind, Profile, Purpose, Unit};
pub struct Params {
pub start: DateTime,
pub duration: i64,
pub unit: Unit,
pub voltage: f64,
pub default_phases: i64,
pub tx_start: Option<DateTime>,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct CompositePeriod {
pub start_period: i64,
pub limit: Option<f64>,
pub number_phases: Option<i64>,
}
const MICROS: i64 = 1_000_000;
fn occurrence(p: &Profile, t: DateTime, params: &Params) -> Option<(DateTime, Option<DateTime>)> {
let s = &p.schedule;
let bounded = |start: DateTime, dur: Option<i64>| (start, dur.map(|d| start.add_seconds(d)));
let (start, end) = match p.kind {
Kind::Absolute => bounded(s.start.unwrap_or(params.start), s.duration),
Kind::Relative => bounded(params.tx_start.unwrap_or(params.start), s.duration),
Kind::Dynamic => {
let since = p.dyn_updated_at.or(p.valid_from).unwrap_or(params.start);
(
DateTime::UNIX_EPOCH,
s.duration.map(|d| since.add_seconds(d)),
)
}
Kind::Recurring => {
let anchor = s.start?;
let period = p.recurrency?.seconds();
if t < anchor {
return None;
}
let k = (t.unix_seconds() - anchor.unix_seconds()).div_euclid(period);
let occ = anchor.add_seconds(k * period);
let dur = s.duration.map_or(period, |d| d.min(period));
(occ, Some(occ.add_seconds(dur)))
}
};
(t >= start && end.is_none_or(|e| t < e)).then_some((start, end))
}
fn period_at(p: &Profile, t: DateTime, params: &Params) -> Option<(Option<f64>, Option<i64>)> {
if !p.valid_at(t) {
return None;
}
let (start, _) = occurrence(p, t, params)?;
let offset = (t.unix_micros() - start.unix_micros()) / MICROS;
let offset = if p.kind == Kind::Dynamic { 0 } else { offset };
p.schedule
.periods
.iter()
.rev()
.find(|per| per.start <= offset)
.map(|per| (per.limit, per.phases))
}
fn convert(v: f64, from: Unit, to: Unit, phases: i64, voltage: f64) -> f64 {
let factor = voltage * phases.max(1) as f64;
match (from, to) {
(Unit::A, Unit::W) => v * factor,
(Unit::W, Unit::A) => v / factor,
_ => v,
}
}
fn breakpoints(profiles: &[&Profile], params: &Params) -> Vec<DateTime> {
let end = params.start.add_seconds(params.duration);
let mut set = BTreeSet::new();
set.insert(params.start);
let mut add = |t: DateTime| {
if t > params.start && t < end {
set.insert(t);
}
};
for p in profiles {
p.valid_from
.into_iter()
.chain(p.valid_to)
.for_each(&mut add);
let s = &p.schedule;
let mut occurrence_starts = Vec::new();
match p.kind {
Kind::Absolute => occurrence_starts.push(s.start.unwrap_or(params.start)),
Kind::Relative => occurrence_starts.push(params.tx_start.unwrap_or(params.start)),
Kind::Dynamic => {
if let (Some(d), Some(since)) = (s.duration, p.dyn_updated_at.or(p.valid_from)) {
add(since.add_seconds(d));
}
}
Kind::Recurring => {
if let (Some(anchor), Some(rec)) = (s.start, p.recurrency) {
let period = rec.seconds();
let first = ((params.start.unix_seconds() - anchor.unix_seconds())
.div_euclid(period))
.max(0);
let last = (end.unix_seconds() - anchor.unix_seconds()).div_euclid(period);
for k in first..=last.max(first) {
occurrence_starts.push(anchor.add_seconds(k * period));
}
}
}
}
for occ in occurrence_starts {
for per in &s.periods {
add(occ.add_seconds(per.start));
}
let dur = match p.kind {
Kind::Recurring => Some(
s.duration
.map_or(p.recurrency.map_or(0, |r| r.seconds()), |d| {
d.min(p.recurrency.map_or(d, |r| r.seconds()))
}),
),
_ => s.duration,
};
if let Some(d) = dur {
add(occ.add_seconds(d));
}
}
}
set.into_iter().collect()
}
fn leading<'a>(
profiles: &[&'a Profile],
purpose: Purpose,
t: DateTime,
params: &Params,
) -> Option<(&'a Profile, Option<f64>, Option<i64>)> {
profiles
.iter()
.filter(|p| p.purpose == purpose)
.filter_map(|p| period_at(p, t, params).map(|(l, ph)| (*p, l, ph)))
.max_by_key(|(p, _, _)| (p.stack_level, p.evse))
}
pub fn composite(profiles: &[Profile], evse: i64, params: &Params) -> Vec<CompositePeriod> {
let relevant: Vec<&Profile> = profiles
.iter()
.filter(|p| p.evse == evse || p.evse == 0)
.collect();
let mut out: Vec<CompositePeriod> = Vec::new();
for t in breakpoints(&relevant, params) {
let get = |purpose| leading(&relevant, purpose, t, params);
let tx_side = get(Purpose::PriorityCharging)
.or_else(|| get(Purpose::Tx))
.or_else(|| get(Purpose::TxDefault));
let mut limit: Option<f64> = None;
let mut phases: Option<i64> = None;
for (p, l, ph) in [
get(Purpose::StationMax),
tx_side,
get(Purpose::ExternalConstraints),
]
.into_iter()
.flatten()
{
if let Some(ph) = ph {
phases = Some(phases.map_or(ph, |x: i64| x.min(ph)));
}
if let Some(l) = l {
let v = convert(
l,
p.schedule.unit,
params.unit,
ph.unwrap_or(params.default_phases),
params.voltage,
);
limit = Some(limit.map_or(v, |x: f64| x.min(v)));
}
}
if let (Some((p, Some(l), ph)), Some(current)) = (get(Purpose::LocalGeneration), limit) {
limit = Some(
current
+ convert(
l,
p.schedule.unit,
params.unit,
ph.unwrap_or(params.default_phases),
params.voltage,
),
);
}
let limit = limit.map(|v| (v * 10.0).round() / 10.0);
let start_period = (t.unix_micros() - params.start.unix_micros()) / MICROS;
if out
.last()
.is_none_or(|last| last.limit != limit || last.number_phases != phases)
{
out.push(CompositePeriod {
start_period,
limit,
number_phases: phases,
});
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::smart::model::{Period, Recurrency, Schedule};
fn t(s: &str) -> DateTime {
DateTime::parse(s).unwrap()
}
#[allow(clippy::too_many_arguments)]
fn profile(
id: i64,
evse: i64,
purpose: Purpose,
stack: i64,
kind: Kind,
start: Option<&str>,
duration: Option<i64>,
unit: Unit,
periods: &[(i64, f64)],
) -> Profile {
Profile {
id,
evse,
purpose,
stack_level: stack,
kind,
recurrency: None,
valid_from: None,
valid_to: None,
transaction: None,
schedule: Schedule {
start: start.map(t),
duration,
unit,
periods: periods
.iter()
.map(|&(s, l)| Period {
start: s,
limit: Some(l),
phases: None,
})
.collect(),
},
dyn_updated_at: None,
}
}
fn params(start: &str, duration: i64, unit: Unit) -> Params {
Params {
start: t(start),
duration,
unit,
voltage: 230.0,
default_phases: 3,
tx_start: None,
}
}
fn view(c: &[CompositePeriod]) -> Vec<(i64, Option<f64>)> {
c.iter().map(|p| (p.start_period, p.limit)).collect()
}
#[test]
fn spec_example_daily_recurring_profile() {
let mut p = profile(
100,
0,
Purpose::TxDefault,
0,
Kind::Recurring,
Some("2013-01-01T00:00:00Z"),
Some(86_400),
Unit::W,
&[(0, 11000.0), (28_800, 6000.0), (72_000, 11000.0)],
);
p.recurrency = Some(Recurrency::Daily);
let c = composite(&[p], 1, ¶ms("2024-03-10T06:00:00Z", 86_400, Unit::W));
assert_eq!(
view(&c),
vec![
(0, Some(11000.0)),
(7_200, Some(6000.0)),
(50_400, Some(11000.0))
]
);
}
#[test]
fn stacking_and_combining() {
let mut daily = profile(
10,
0,
Purpose::TxDefault,
1,
Kind::Recurring,
Some("2020-01-09T17:00:00Z"),
Some(10_800),
Unit::W,
&[(0, 2000.0)],
);
daily.recurrency = Some(Recurrency::Daily);
let mut sunday = profile(
11,
0,
Purpose::TxDefault,
2,
Kind::Recurring,
Some("2020-01-05T00:00:00Z"),
Some(86_400),
Unit::W,
&[(0, 999_999.0)],
);
sunday.recurrency = Some(Recurrency::Weekly);
let max = profile(
1,
0,
Purpose::StationMax,
0,
Kind::Absolute,
Some("2020-01-01T00:00:00Z"),
None,
Unit::W,
&[(0, 7000.0)],
);
let all = [daily, sunday, max];
let c = composite(&all, 1, ¶ms("2024-03-09T12:00:00Z", 12 * 3600, Unit::W));
assert_eq!(
view(&c),
vec![
(0, Some(7000.0)),
(5 * 3600, Some(2000.0)),
(8 * 3600, Some(7000.0))
]
);
let c = composite(&all, 1, ¶ms("2024-03-10T12:00:00Z", 12 * 3600, Unit::W));
assert_eq!(view(&c), vec![(0, Some(7000.0))]);
}
#[test]
fn tx_profile_overrules_default_and_units_convert() {
let def = profile(
1,
1,
Purpose::TxDefault,
0,
Kind::Absolute,
Some("2024-01-01T00:00:00Z"),
None,
Unit::A,
&[(0, 32.0)],
);
let mut tx = profile(
2,
1,
Purpose::Tx,
0,
Kind::Relative,
None,
Some(1800),
Unit::A,
&[(0, 10.0), (600, 16.0)],
);
tx.transaction = Some("T1".into());
let mut p = params("2024-01-01T10:00:00Z", 3600, Unit::A);
p.tx_start = Some(t("2024-01-01T10:00:00Z"));
let c = composite(&[def.clone(), tx.clone()], 1, &p);
assert_eq!(
view(&c),
vec![(0, Some(10.0)), (600, Some(16.0)), (1800, Some(32.0))]
);
p.unit = Unit::W;
let c = composite(&[def, tx], 1, &p);
assert_eq!(c[1].limit, Some(11040.0));
assert_eq!(view(&composite(&[], 2, &p)), vec![(0, None)]);
}
#[test]
fn validity_and_local_generation() {
let mut max = profile(
1,
0,
Purpose::StationMax,
0,
Kind::Absolute,
Some("2024-01-01T00:00:00Z"),
None,
Unit::W,
&[(0, 10000.0)],
);
max.valid_to = Some(t("2024-01-01T01:00:00Z"));
let gen_ = profile(
2,
0,
Purpose::LocalGeneration,
0,
Kind::Absolute,
Some("2024-01-01T00:30:00Z"),
Some(3600),
Unit::W,
&[(0, 4000.0)],
);
let c = composite(
&[max, gen_],
1,
¶ms("2024-01-01T00:00:00Z", 7200, Unit::W),
);
assert_eq!(
view(&c),
vec![(0, Some(10000.0)), (1800, Some(14000.0)), (3600, None)]
);
}
}