@@ -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