frequenz_microgrid/microgrid/
pv_pool.rs1use tokio::sync::broadcast;
16
17use std::collections::{BTreeSet, HashSet};
18use std::time::Duration;
19
20use crate::{
21 Bounds, Error, Formula, LogicalMeterHandle, MicrogridClientHandle,
22 client::proto::common::microgrid::electrical_components::ElectricalComponentStateCode,
23 metric,
24 metric::Metric,
25 microgrid::{
26 caching_sender::{CachingSender, WeakCachingSender},
27 pool_bounds,
28 pool_bounds_tracker::PoolBoundsTracker,
29 pool_validation::validate_pool_ids,
30 telemetry_tracker::pv_pool_telemetry_tracker::{PvPoolSnapshot, PvPoolTelemetryTracker},
31 },
32 quantity::Power,
33};
34
35pub struct PvPool {
79 component_ids: Option<BTreeSet<u64>>,
80 client: MicrogridClientHandle,
81 logical_meter: LogicalMeterHandle,
82 snapshot_tx: Option<WeakCachingSender<PvPoolSnapshot>>,
83 bounds_tx: Option<WeakCachingSender<Vec<Bounds<Power>>>>,
84}
85
86impl PvPool {
87 pub(crate) fn try_new(
94 component_ids: Option<BTreeSet<u64>>,
95 client: MicrogridClientHandle,
96 logical_meter: LogicalMeterHandle,
97 ) -> Result<Self, Error> {
98 let this = Self {
99 component_ids,
100 client,
101 logical_meter,
102 snapshot_tx: None,
103 bounds_tx: None,
104 };
105 validate_pool_ids(
106 &this.component_ids,
107 &this.get_all_pv_inverter_ids(),
108 "PV inverters",
109 )
110 .inspect_err(|e| tracing::error!("{e}"))?;
111 Ok(this)
112 }
113
114 fn get_all_pv_inverter_ids(&self) -> BTreeSet<u64> {
115 self.logical_meter
116 .graph()
117 .components()
118 .filter(|c| c.is_pv_inverter())
119 .map(|c| c.id)
120 .collect()
121 }
122
123 pub(crate) fn get_pv_inverter_ids(&self) -> BTreeSet<u64> {
124 if let Some(ids) = &self.component_ids {
125 ids.clone()
126 } else {
127 self.get_all_pv_inverter_ids()
128 }
129 }
130
131 pub fn power(&mut self) -> Result<Formula<Power>, Error> {
133 self.logical_meter
134 .pv::<metric::AcPowerActive>(self.component_ids.clone())
135 }
136
137 pub fn power_bounds(&mut self) -> broadcast::Receiver<Vec<Bounds<Power>>> {
144 if let Some(tx) = self.bounds_tx.as_ref().and_then(WeakCachingSender::upgrade)
145 && tx.receiver_count() > 0
146 {
147 return tx.subscribe_with_current();
148 }
149 let snapshot_rx = self.telemetry_snapshots();
150 let tx = CachingSender::<Vec<Bounds<Power>>>::new();
151 let rx = tx.subscribe_with_current();
154 let tracker = PoolBoundsTracker::new(
155 snapshot_rx,
156 tx.clone(),
157 pool_bounds::compute_pv_pool_bounds::<metric::AcPowerActive>,
158 format!("{} PV", metric::AcPowerActive::str_name()),
159 );
160 tokio::spawn(tracker.run());
161 self.bounds_tx = Some(tx.downgrade());
162 rx
163 }
164
165 pub fn telemetry_snapshots(&mut self) -> broadcast::Receiver<PvPoolSnapshot> {
172 if let Some(tx) = self
173 .snapshot_tx
174 .as_ref()
175 .and_then(WeakCachingSender::upgrade)
176 && tx.receiver_count() > 0
177 {
178 return tx.subscribe_with_current();
179 }
180 let tx = CachingSender::<PvPoolSnapshot>::new();
181 let rx = tx.subscribe_with_current();
184 let tracker = PvPoolTelemetryTracker::new(
185 self.get_pv_inverter_ids(),
186 Duration::from_secs(10),
187 HashSet::from([
191 ElectricalComponentStateCode::Ready,
192 ElectricalComponentStateCode::Standby,
193 ElectricalComponentStateCode::Discharging,
194 ]),
195 self.client.clone(),
196 tx.clone(),
197 );
198 tokio::spawn(tracker.run());
199 self.snapshot_tx = Some(tx.downgrade());
200 rx
201 }
202}
203
204#[cfg(test)]
205mod tests {
206 use std::collections::BTreeSet;
207
208 use super::PvPool;
209 use crate::client::test_utils::MockComponent;
210 use crate::microgrid::test_utils::{handles, last_snapshot};
211
212 fn graph() -> MockComponent {
215 MockComponent::grid(1).with_children(vec![MockComponent::meter(2).with_children(vec![
216 MockComponent::meter(3).with_children(vec![
217 MockComponent::pv_inverter(4),
218 MockComponent::pv_inverter(5),
219 ]),
220 MockComponent::meter(6).with_children(vec![
221 MockComponent::battery_inverter(7).with_children(vec![MockComponent::battery(8)]),
222 ]),
223 ])])
224 }
225
226 #[tokio::test]
227 async fn try_new_accepts_empty_component_ids() {
228 let (client, lm) = handles(graph()).await;
229 let mut pool = PvPool::try_new(Some(BTreeSet::new()), client, lm)
231 .expect("an empty component_ids set should yield an empty pool");
232 pool.power().expect("empty pool power formula");
233 }
234
235 #[tokio::test(start_paused = true)]
236 async fn empty_pool_emits_empty_snapshot_and_bounds() {
237 let (client, lm) =
239 handles(MockComponent::grid(1).with_children(vec![MockComponent::meter(2)])).await;
240 let mut pool = PvPool::try_new(None, client, lm).unwrap();
241
242 let mut snapshots = pool.telemetry_snapshots();
243 let mut bounds = pool.power_bounds();
244
245 let snapshot = last_snapshot(&mut snapshots, 5).await;
246 assert!(
247 snapshot.inverters.healthy.is_empty() && snapshot.inverters.unhealthy.is_empty(),
248 "empty pool snapshot should have no inverters, got {snapshot:?}"
249 );
250
251 let bounds = last_snapshot(&mut bounds, 5).await;
252 assert!(
253 bounds.is_empty(),
254 "empty pool should have empty power bounds"
255 );
256 }
257
258 #[tokio::test(start_paused = true)]
259 async fn late_subscriber_sees_latest_snapshot() {
260 let (client, lm) = handles(graph()).await;
261 let mut pool = PvPool::try_new(None, client, lm).unwrap();
262
263 let mut early = pool.telemetry_snapshots();
267 let early_snap = last_snapshot(&mut early, 10).await;
268 assert_eq!(
269 early_snap.inverters.unhealthy.len(),
270 2,
271 "precondition: both inverters tracked"
272 );
273
274 let mut late = pool.telemetry_snapshots();
278 let late_snap = late
279 .try_recv()
280 .expect("late subscriber should be sent the cached snapshot at once");
281 assert_eq!(
282 late_snap, early_snap,
283 "late subscriber should see the latest snapshot, not an empty stream"
284 );
285 }
286
287 #[tokio::test(start_paused = true)]
288 async fn resubscribing_after_teardown_yields_a_current_valued_stream() {
289 let (client, lm) = handles(graph()).await;
290 let mut pool = PvPool::try_new(None, client, lm).unwrap();
291
292 let mut rx = pool.telemetry_snapshots();
295 assert_eq!(
296 last_snapshot(&mut rx, 10).await.inverters.unhealthy.len(),
297 2
298 );
299 drop(rx);
300 tokio::time::advance(std::time::Duration::from_secs(1)).await;
301
302 let mut rx = pool.telemetry_snapshots();
306 assert_eq!(
307 last_snapshot(&mut rx, 10).await.inverters.unhealthy.len(),
308 2,
309 "resubscribed stream should yield the pool's current snapshot"
310 );
311 }
312
313 #[tokio::test(start_paused = true)]
314 async fn calling_power_bounds_twice_reuses_the_tracker() {
315 let (client, lm) = handles(graph()).await;
316 let mut pool = PvPool::try_new(None, client, lm).unwrap();
317
318 let mut rx1 = pool.power_bounds();
320 let bounds1 = last_snapshot(&mut rx1, 10).await;
321
322 let mut rx2 = pool.power_bounds();
327 let bounds2 = rx2
328 .try_recv()
329 .expect("reused tracker should re-send its cached bounds immediately");
330 assert_eq!(bounds1, bounds2, "reused tracker shares the same bounds");
331 }
332
333 #[tokio::test]
334 async fn try_new_rejects_non_pv_component_ids() {
335 let (client, lm) = handles(graph()).await;
336 let err = PvPool::try_new(Some([4, 7, 8].into()), client, lm)
338 .err()
339 .expect("non-PV component_ids should be rejected");
340 assert!(
341 err.to_string().contains("must be PV inverters"),
342 "unexpected error: {err}"
343 );
344 }
345
346 #[tokio::test]
347 async fn power_formula_for_explicit_pv_inverters() {
348 let (client, lm) = handles(graph()).await;
349 let mut pool = PvPool::try_new(Some([4, 5].into()), client, lm).unwrap();
350 let formula = pool.power().unwrap();
351 assert_eq!(
352 formula.to_string(),
353 "METRIC_AC_POWER_ACTIVE::(COALESCE(#5 + #4, #3, COALESCE(#5, 0.0) + COALESCE(#4, 0.0)))"
354 );
355 }
356
357 #[tokio::test]
358 async fn power_formula_for_all_pv_inverters() {
359 let (client, lm) = handles(graph()).await;
360 let mut pool = PvPool::try_new(None, client, lm).unwrap();
361 let formula = pool.power().unwrap();
362 assert_eq!(
363 formula.to_string(),
364 "METRIC_AC_POWER_ACTIVE::(COALESCE(#5 + #4, #3, COALESCE(#5, 0.0) + COALESCE(#4, 0.0)))"
365 );
366 }
367}