use hems_core::prelude::*;
use s2energy::common::{ControlType as S2ControlType, RoleType};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ControlType {
Frbc,
Pebc,
Ombc,
Ppbc,
Ddbc,
NotControllable,
}
impl From<ControlType> for S2ControlType {
fn from(value: ControlType) -> Self {
match value {
ControlType::Frbc => S2ControlType::FillRateBasedControl,
ControlType::Pebc => S2ControlType::PowerEnvelopeBasedControl,
ControlType::Ombc => S2ControlType::OperationModeBasedControl,
ControlType::Ppbc => S2ControlType::PowerProfileBasedControl,
ControlType::Ddbc => S2ControlType::DemandDrivenBasedControl,
ControlType::NotControllable => S2ControlType::NotControlable,
}
}
}
#[must_use]
pub fn control_type_for(asset: &Asset, has_deadline: bool) -> ControlType {
match asset {
Asset::Battery(_) | Asset::Dhw(_) => ControlType::Frbc,
Asset::Evse(_) => {
if has_deadline {
ControlType::Frbc
} else {
ControlType::Pebc
}
}
Asset::HeatPump(hp) => match hp.control {
HeatPumpControl::SgReady | HeatPumpControl::OperationModes => ControlType::Ombc,
HeatPumpControl::PowerCeiling => ControlType::Pebc,
},
Asset::Pv(_) => ControlType::Pebc,
Asset::Load(load) => match load.kind {
LoadKind::Shiftable => ControlType::Ppbc,
LoadKind::Interruptible => ControlType::Ombc,
LoadKind::Fixed => ControlType::NotControllable,
},
Asset::Relay(_) => ControlType::Ombc,
Asset::Meter(_) => ControlType::NotControllable,
}
}
#[must_use]
pub fn roles_for(asset: &Asset) -> Vec<RoleType> {
match asset {
Asset::Battery(_) => {
vec![
RoleType::EnergyStorage,
RoleType::EnergyConsumer,
RoleType::EnergyProducer,
]
}
Asset::Evse(evse) => {
if evse.bidirectional {
vec![
RoleType::EnergyStorage,
RoleType::EnergyConsumer,
RoleType::EnergyProducer,
]
} else {
vec![RoleType::EnergyConsumer]
}
}
Asset::Pv(_) => vec![RoleType::EnergyProducer],
Asset::Dhw(_) => vec![RoleType::EnergyStorage, RoleType::EnergyConsumer],
Asset::HeatPump(_) | Asset::Load(_) | Asset::Relay(_) => vec![RoleType::EnergyConsumer],
Asset::Meter(_) => Vec::new(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use hems_core::asset::{AssetMeta, Battery, Chemistry, Evse, FlexibleLoad, HeatPump, PvArray};
fn meta(id: &str, kw: f64) -> AssetMeta {
AssetMeta::new(
AssetId::new(id).unwrap(),
CircuitId::new("main").unwrap(),
PhaseConnection::Three,
Power::from_kw(kw),
)
}
fn evse(bidirectional: bool) -> Asset {
Asset::Evse(Evse {
meta: meta("wallbox", 11.0),
min_current: Current::new(6.0),
max_current: Current::new(16.0),
bidirectional,
public: false,
})
}
fn battery() -> Asset {
Asset::Battery(Battery {
meta: meta("battery", 5.0),
capacity: Energy::from_kwh(10.0),
max_charge: Power::from_kw(5.0),
max_discharge: Power::from_kw(5.0),
efficiency_charge: 0.95,
efficiency_discharge: 0.95,
soc_min: Soc::new(0.05).unwrap(),
soc_max: Soc::FULL,
reserve_soc: Soc::EMPTY,
chemistry: Chemistry::Lfp,
grid_charging_allowed: true,
})
}
#[test]
fn a_battery_is_a_store() {
assert_eq!(control_type_for(&battery(), false), ControlType::Frbc);
}
#[test]
fn a_charge_point_changes_control_type_when_the_car_has_a_deadline() {
assert_eq!(control_type_for(&evse(false), false), ControlType::Pebc);
assert_eq!(control_type_for(&evse(false), true), ControlType::Frbc);
}
#[test]
fn an_sg_ready_heat_pump_is_modes_and_a_modern_one_is_an_envelope() {
let hp = |control| {
Asset::HeatPump(HeatPump {
meta: meta("wp", 9.0),
electrical_nominal: Power::from_kw(5.0),
heating_rod: None,
control,
modulating: true,
})
};
assert_eq!(
control_type_for(&hp(HeatPumpControl::SgReady), false),
ControlType::Ombc
);
assert_eq!(
control_type_for(&hp(HeatPumpControl::PowerCeiling), false),
ControlType::Pebc
);
}
#[test]
fn a_washing_machine_is_a_profile_and_a_pool_pump_is_modes() {
let load = |kind| {
Asset::Load(FlexibleLoad {
meta: meta("geraet", 2.0),
nominal: Power::from_kw(2.0),
kind,
})
};
assert_eq!(
control_type_for(&load(LoadKind::Shiftable), false),
ControlType::Ppbc
);
assert_eq!(
control_type_for(&load(LoadKind::Interruptible), false),
ControlType::Ombc
);
assert_eq!(
control_type_for(&load(LoadKind::Fixed), false),
ControlType::NotControllable
);
}
#[test]
fn an_inverter_is_only_ever_an_envelope() {
let pv = Asset::Pv(PvArray {
meta: meta("pv", 9.8),
kwp_dc: Power::from_kw(9.8),
ac_nominal: Power::from_kw(8.0),
tilt_deg: 35.0,
azimuth_deg: 180.0,
cap_relief: CapRelief::None,
});
assert_eq!(control_type_for(&pv, false), ControlType::Pebc);
assert_eq!(roles_for(&pv), vec![RoleType::EnergyProducer]);
}
#[test]
fn a_battery_declares_three_roles_not_one() {
let roles = roles_for(&battery());
assert!(roles.contains(&RoleType::EnergyStorage));
assert!(roles.contains(&RoleType::EnergyProducer));
assert!(roles.contains(&RoleType::EnergyConsumer));
}
#[test]
fn a_bidirectional_charge_point_can_produce_and_an_ordinary_one_cannot() {
assert!(roles_for(&evse(true)).contains(&RoleType::EnergyProducer));
assert_eq!(roles_for(&evse(false)), vec![RoleType::EnergyConsumer]);
}
#[test]
fn every_control_type_maps_onto_the_standards_own_enum() {
for ct in [
ControlType::Frbc,
ControlType::Pebc,
ControlType::Ombc,
ControlType::Ppbc,
ControlType::Ddbc,
ControlType::NotControllable,
] {
let _: S2ControlType = ct.into();
}
}
}