1use hems_core::prelude::*;
14use s2energy::common::{ControlType as S2ControlType, RoleType};
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
21pub enum ControlType {
22 Frbc,
24 Pebc,
26 Ombc,
28 Ppbc,
30 Ddbc,
32 NotControllable,
34}
35
36impl From<ControlType> for S2ControlType {
37 fn from(value: ControlType) -> Self {
38 match value {
39 ControlType::Frbc => S2ControlType::FillRateBasedControl,
40 ControlType::Pebc => S2ControlType::PowerEnvelopeBasedControl,
41 ControlType::Ombc => S2ControlType::OperationModeBasedControl,
42 ControlType::Ppbc => S2ControlType::PowerProfileBasedControl,
43 ControlType::Ddbc => S2ControlType::DemandDrivenBasedControl,
44 ControlType::NotControllable => S2ControlType::NotControlable,
45 }
46 }
47}
48
49#[must_use]
55pub fn control_type_for(asset: &Asset, has_deadline: bool) -> ControlType {
56 match asset {
57 Asset::Battery(_) | Asset::Dhw(_) => ControlType::Frbc,
59
60 Asset::Evse(_) => {
63 if has_deadline {
64 ControlType::Frbc
65 } else {
66 ControlType::Pebc
67 }
68 }
69
70 Asset::HeatPump(hp) => match hp.control {
75 HeatPumpControl::SgReady | HeatPumpControl::OperationModes => ControlType::Ombc,
76 HeatPumpControl::PowerCeiling => ControlType::Pebc,
77 },
78
79 Asset::Pv(_) => ControlType::Pebc,
81
82 Asset::Load(load) => match load.kind {
83 LoadKind::Shiftable => ControlType::Ppbc,
86 LoadKind::Interruptible => ControlType::Ombc,
87 LoadKind::Fixed => ControlType::NotControllable,
88 },
89
90 Asset::Relay(_) => ControlType::Ombc,
91 Asset::Meter(_) => ControlType::NotControllable,
92 }
93}
94
95#[must_use]
101pub fn roles_for(asset: &Asset) -> Vec<RoleType> {
102 match asset {
103 Asset::Battery(_) => {
104 vec![
105 RoleType::EnergyStorage,
106 RoleType::EnergyConsumer,
107 RoleType::EnergyProducer,
108 ]
109 }
110 Asset::Evse(evse) => {
111 if evse.bidirectional {
112 vec![
113 RoleType::EnergyStorage,
114 RoleType::EnergyConsumer,
115 RoleType::EnergyProducer,
116 ]
117 } else {
118 vec![RoleType::EnergyConsumer]
119 }
120 }
121 Asset::Pv(_) => vec![RoleType::EnergyProducer],
122 Asset::Dhw(_) => vec![RoleType::EnergyStorage, RoleType::EnergyConsumer],
123 Asset::HeatPump(_) | Asset::Load(_) | Asset::Relay(_) => vec![RoleType::EnergyConsumer],
124 Asset::Meter(_) => Vec::new(),
125 }
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131 use hems_core::asset::{AssetMeta, Battery, Chemistry, Evse, FlexibleLoad, HeatPump, PvArray};
132
133 fn meta(id: &str, kw: f64) -> AssetMeta {
134 AssetMeta::new(
135 AssetId::new(id).unwrap(),
136 CircuitId::new("main").unwrap(),
137 PhaseConnection::Three,
138 Power::from_kw(kw),
139 )
140 }
141
142 fn evse(bidirectional: bool) -> Asset {
143 Asset::Evse(Evse {
144 meta: meta("wallbox", 11.0),
145 min_current: Current::new(6.0),
146 max_current: Current::new(16.0),
147 bidirectional,
148 public: false,
149 })
150 }
151
152 fn battery() -> Asset {
153 Asset::Battery(Battery {
154 meta: meta("battery", 5.0),
155 capacity: Energy::from_kwh(10.0),
156 max_charge: Power::from_kw(5.0),
157 max_discharge: Power::from_kw(5.0),
158 efficiency_charge: 0.95,
159 efficiency_discharge: 0.95,
160 soc_min: Soc::new(0.05).unwrap(),
161 soc_max: Soc::FULL,
162 reserve_soc: Soc::EMPTY,
163 chemistry: Chemistry::Lfp,
164 grid_charging_allowed: true,
165 })
166 }
167
168 #[test]
169 fn a_battery_is_a_store() {
170 assert_eq!(control_type_for(&battery(), false), ControlType::Frbc);
171 }
172
173 #[test]
174 fn a_charge_point_changes_control_type_when_the_car_has_a_deadline() {
175 assert_eq!(control_type_for(&evse(false), false), ControlType::Pebc);
178 assert_eq!(control_type_for(&evse(false), true), ControlType::Frbc);
179 }
180
181 #[test]
182 fn an_sg_ready_heat_pump_is_modes_and_a_modern_one_is_an_envelope() {
183 let hp = |control| {
184 Asset::HeatPump(HeatPump {
185 meta: meta("wp", 9.0),
186 electrical_nominal: Power::from_kw(5.0),
187 heating_rod: None,
188 control,
189 modulating: true,
190 })
191 };
192 assert_eq!(
193 control_type_for(&hp(HeatPumpControl::SgReady), false),
194 ControlType::Ombc
195 );
196 assert_eq!(
197 control_type_for(&hp(HeatPumpControl::PowerCeiling), false),
198 ControlType::Pebc
199 );
200 }
201
202 #[test]
203 fn a_washing_machine_is_a_profile_and_a_pool_pump_is_modes() {
204 let load = |kind| {
205 Asset::Load(FlexibleLoad {
206 meta: meta("geraet", 2.0),
207 nominal: Power::from_kw(2.0),
208 kind,
209 })
210 };
211 assert_eq!(
212 control_type_for(&load(LoadKind::Shiftable), false),
213 ControlType::Ppbc
214 );
215 assert_eq!(
216 control_type_for(&load(LoadKind::Interruptible), false),
217 ControlType::Ombc
218 );
219 assert_eq!(
220 control_type_for(&load(LoadKind::Fixed), false),
221 ControlType::NotControllable
222 );
223 }
224
225 #[test]
226 fn an_inverter_is_only_ever_an_envelope() {
227 let pv = Asset::Pv(PvArray {
228 meta: meta("pv", 9.8),
229 kwp_dc: Power::from_kw(9.8),
230 ac_nominal: Power::from_kw(8.0),
231 tilt_deg: 35.0,
232 azimuth_deg: 180.0,
233 cap_relief: CapRelief::None,
234 });
235 assert_eq!(control_type_for(&pv, false), ControlType::Pebc);
236 assert_eq!(roles_for(&pv), vec![RoleType::EnergyProducer]);
237 }
238
239 #[test]
240 fn a_battery_declares_three_roles_not_one() {
241 let roles = roles_for(&battery());
243 assert!(roles.contains(&RoleType::EnergyStorage));
244 assert!(roles.contains(&RoleType::EnergyProducer));
245 assert!(roles.contains(&RoleType::EnergyConsumer));
246 }
247
248 #[test]
249 fn a_bidirectional_charge_point_can_produce_and_an_ordinary_one_cannot() {
250 assert!(roles_for(&evse(true)).contains(&RoleType::EnergyProducer));
251 assert_eq!(roles_for(&evse(false)), vec![RoleType::EnergyConsumer]);
252 }
253
254 #[test]
255 fn every_control_type_maps_onto_the_standards_own_enum() {
256 for ct in [
257 ControlType::Frbc,
258 ControlType::Pebc,
259 ControlType::Ombc,
260 ControlType::Ppbc,
261 ControlType::Ddbc,
262 ControlType::NotControllable,
263 ] {
264 let _: S2ControlType = ct.into();
265 }
266 }
267}