Skip to content

Commit ee8dd2d

Browse files
committed
Add tests for the logical meter actor
Signed-off-by: Sahas Subramanian <sahas.subramanian@proton.me>
1 parent ebc4600 commit ee8dd2d

2 files changed

Lines changed: 205 additions & 1 deletion

File tree

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ tracing-subscriber = { version = "0.3" }
2424
[dev-dependencies]
2525
tracing-subscriber = { version = "0.3", features = ["std", "env-filter"] }
2626
tokio = { version = "1.45", features = ["test-util"] }
27-
tokio-stream = "0.1.17"
27+
tokio-stream = { version = "0.1.17", features = ["sync"] }
2828

2929

3030
[build-dependencies]

src/logical_meter/logical_meter_handle.rs

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,3 +181,207 @@ impl LogicalMeterHandle {
181181
)?)))
182182
}
183183
}
184+
185+
#[cfg(test)]
186+
mod tests {
187+
use chrono::TimeDelta;
188+
use tokio_stream::{StreamExt, wrappers::BroadcastStream};
189+
190+
use crate::{
191+
LogicalMeterConfig, LogicalMeterHandle, MicrogridClientHandle, Sample,
192+
client::test_utils::{
193+
MockComponent,
194+
MockMicrogridApiClient, //
195+
},
196+
logical_meter::formula::Formula,
197+
quantity::Quantity,
198+
};
199+
200+
async fn new_logical_meter_handle() -> LogicalMeterHandle {
201+
let api_client = MockMicrogridApiClient::new(
202+
// Grid connection point
203+
MockComponent::grid(1).with_children(vec![
204+
// Main meter
205+
MockComponent::meter(2)
206+
.with_power(vec![4.0, 5.0, 6.0, 7.0, 7.0, 7.0])
207+
.with_current(vec![1.0, 1.5, 2.0, 2.5, 2.0, 1.5])
208+
.with_children(vec![
209+
// PV meter
210+
MockComponent::meter(3)
211+
.with_reactive_power(vec![-2.0, -5.0, -4.0, 1.0, 3.0, 4.0])
212+
.with_children(vec![
213+
// PV inverter
214+
MockComponent::pv_inverter(4),
215+
]),
216+
// Battery meter
217+
MockComponent::meter(5).with_children(vec![
218+
// Battery inverter
219+
MockComponent::battery_inverter(6)
220+
.with_voltage(vec![400.0, 400.0, 398.0, 396.0, 396.0, 396.0])
221+
.with_children(vec![
222+
// Battery
223+
MockComponent::battery(7),
224+
]),
225+
]),
226+
// Consumer meter
227+
MockComponent::meter(8)
228+
.with_current(vec![14.5, 15.0, 16.0, 15.5, 14.0, 13.5]),
229+
]),
230+
]),
231+
);
232+
233+
LogicalMeterHandle::try_new(
234+
MicrogridClientHandle::new_from_client(api_client),
235+
LogicalMeterConfig {
236+
resampling_interval: TimeDelta::try_seconds(1).unwrap(),
237+
},
238+
)
239+
.await
240+
.unwrap()
241+
}
242+
243+
#[tokio::test(start_paused = true)]
244+
async fn test_grid_power_formula() {
245+
let formula = new_logical_meter_handle()
246+
.await
247+
.grid(crate::metric::AcPowerActive)
248+
.unwrap();
249+
250+
let samples = fetch_samples(formula, 10).await;
251+
252+
check_samples(
253+
samples,
254+
|q| q.as_watts(),
255+
vec![
256+
Some(5.8),
257+
Some(6.0),
258+
Some(6.0),
259+
Some(7.0),
260+
Some(5.8),
261+
Some(6.0),
262+
Some(6.0),
263+
Some(7.0),
264+
Some(5.8),
265+
Some(6.0),
266+
],
267+
)
268+
}
269+
270+
#[tokio::test(start_paused = true)]
271+
async fn test_pv_reactive_power_formula() {
272+
let formula = new_logical_meter_handle()
273+
.await
274+
.pv(None, crate::metric::AcPowerReactive)
275+
.unwrap();
276+
277+
let samples = fetch_samples(formula, 10).await;
278+
279+
check_samples(
280+
samples,
281+
|q| q.as_volt_amperes_reactive(),
282+
vec![
283+
Some(-1.4),
284+
Some(-0.5),
285+
Some(-0.5),
286+
Some(4.0),
287+
Some(-1.4),
288+
Some(-0.5),
289+
Some(-0.5),
290+
Some(4.0),
291+
Some(-1.4),
292+
Some(-0.5),
293+
],
294+
)
295+
}
296+
297+
#[tokio::test(start_paused = true)]
298+
async fn test_battery_voltage_formula() {
299+
let formula = new_logical_meter_handle()
300+
.await
301+
.battery(None, crate::metric::AcVoltage)
302+
.unwrap();
303+
304+
let samples = fetch_samples(formula, 10).await;
305+
check_samples(
306+
samples,
307+
|q| q.as_volts(),
308+
vec![
309+
Some(398.0),
310+
Some(397.67),
311+
Some(397.67),
312+
Some(396.0),
313+
Some(398.0),
314+
Some(397.67),
315+
Some(397.67),
316+
Some(396.0),
317+
Some(398.0),
318+
Some(397.67),
319+
],
320+
)
321+
}
322+
323+
#[tokio::test(start_paused = true)]
324+
async fn test_consumer_current_formula() {
325+
let formula = new_logical_meter_handle()
326+
.await
327+
.consumer(crate::metric::AcCurrent)
328+
.unwrap();
329+
330+
let samples = fetch_samples(formula, 10).await;
331+
check_samples(
332+
samples,
333+
|q| q.as_amperes(),
334+
vec![
335+
Some(15.0),
336+
Some(14.75),
337+
Some(14.75),
338+
Some(13.5),
339+
Some(15.0),
340+
Some(14.75),
341+
Some(14.75),
342+
Some(13.5),
343+
Some(15.0),
344+
Some(14.75),
345+
],
346+
)
347+
}
348+
349+
async fn fetch_samples<Q: Quantity>(formula: Formula<Q>, num_values: usize) -> Vec<Sample<Q>> {
350+
let rx = formula.subscribe().await.unwrap();
351+
352+
BroadcastStream::new(rx)
353+
.take(num_values)
354+
.map(|x| x.unwrap())
355+
.collect()
356+
.await
357+
}
358+
359+
#[track_caller]
360+
fn check_samples<Q: Quantity>(
361+
samples: Vec<Sample<Q>>,
362+
extractor: impl Fn(Q) -> f32,
363+
expected_values: Vec<Option<f32>>,
364+
) {
365+
let values = samples
366+
.iter()
367+
.map(|res| res.value().map(|v| extractor(v)))
368+
.collect::<Vec<_>>();
369+
370+
let one_second = TimeDelta::try_seconds(1).unwrap();
371+
372+
samples.as_slice().windows(2).for_each(|w| {
373+
assert_eq!(w[1].timestamp() - w[0].timestamp(), one_second);
374+
});
375+
376+
for (v, ev) in values.iter().zip(expected_values.iter()) {
377+
match (v, ev) {
378+
(Some(v), Some(ev)) => assert!(
379+
(v - ev).abs() < 0.01,
380+
"expected value {ev:?}, got value {v:?}"
381+
),
382+
(None, None) => {}
383+
_ => panic!("expected value {ev:?}, got value {v:?}"),
384+
}
385+
}
386+
}
387+
}

0 commit comments

Comments
 (0)