1717from dataclasses import dataclass
1818
1919from frequenz .channels import Broadcast , Sender
20- from frequenz .client .microgrid import ComponentCategory
20+ from frequenz .client .microgrid import ComponentCategory , InverterType
2121
2222from ..actor ._actor import Actor
2323from ..timeseries ._grid_frequency import GridFrequency
4444 )
4545 from ..timeseries .logical_meter import LogicalMeter
4646 from ..timeseries .producer import Producer
47+ from ..timeseries .pv_pool import PVPool
48+ from ..timeseries .pv_pool ._pv_pool_reference_store import PVPoolReferenceStore
4749
4850_logger = logging .getLogger (__name__ )
4951
@@ -101,6 +103,9 @@ def __init__(
101103 self ._ev_power_wrapper = PowerWrapper (
102104 ComponentCategory .EV_CHARGER , None , self ._channel_registry
103105 )
106+ self ._pv_power_wrapper = PowerWrapper (
107+ ComponentCategory .INVERTER , InverterType .SOLAR , self ._channel_registry
108+ )
104109
105110 self ._logical_meter : LogicalMeter | None = None
106111 self ._consumer : Consumer | None = None
@@ -112,6 +117,7 @@ def __init__(
112117 self ._battery_pool_reference_stores : dict [
113118 frozenset [int ], BatteryPoolReferenceStore
114119 ] = {}
120+ self ._pv_pool_reference_stores : dict [frozenset [int ], PVPoolReferenceStore ] = {}
115121 self ._frequency_instance : GridFrequency | None = None
116122 self ._voltage_instance : VoltageStreamer | None = None
117123
@@ -245,6 +251,71 @@ def ev_charger_pool(
245251 self ._ev_charger_pool_reference_stores [ref_store_key ], name , priority
246252 )
247253
254+ def pv_pool (
255+ self ,
256+ pv_inverter_ids : abc .Set [int ] | None = None ,
257+ name : str | None = None ,
258+ priority : int = - sys .maxsize - 1 ,
259+ ) -> PVPool :
260+ """Return a new `PVPool` instance for the given ids.
261+
262+ If a `PVPoolReferenceStore` instance for the given PV inverter ids doesn't
263+ exist, a new one is created and used for creating the `PVPool`.
264+
265+ Args:
266+ pv_inverter_ids: Optional set of IDs of PV inverters to be managed by the
267+ `PVPool`.
268+ name: An optional name used to identify this instance of the pool or a
269+ corresponding actor in the logs.
270+ priority: The priority of the actor making the call.
271+
272+ Returns:
273+ A `PVPool` instance.
274+ """
275+ from ..timeseries .pv_pool import PVPool
276+ from ..timeseries .pv_pool ._pv_pool_reference_store import PVPoolReferenceStore
277+
278+ if not self ._pv_power_wrapper .started :
279+ self ._pv_power_wrapper .start ()
280+
281+ # We use frozenset to make a hashable key from the input set.
282+ ref_store_key : frozenset [int ] = frozenset ()
283+ if pv_inverter_ids is not None :
284+ ref_store_key = frozenset (pv_inverter_ids )
285+
286+ pool_key = f"{ ref_store_key } -{ priority } "
287+ if pool_key in self ._known_pool_keys :
288+ _logger .warning (
289+ "A PVPool instance was already created for pv_inverter_ids=%s and "
290+ "priority=%s using `microgrid.pv_pool(...)`."
291+ "\n Hint: If the multiple instances are created from the same actor, "
292+ "consider reusing the same instance."
293+ "\n Hint: If the instances are created from different actors, "
294+ "consider using different priorities to distinguish them." ,
295+ pv_inverter_ids ,
296+ priority ,
297+ )
298+ else :
299+ self ._known_pool_keys .add (pool_key )
300+
301+ if ref_store_key not in self ._pv_pool_reference_stores :
302+ self ._pv_pool_reference_stores [ref_store_key ] = PVPoolReferenceStore (
303+ channel_registry = self ._channel_registry ,
304+ resampler_subscription_sender = self ._resampling_request_sender (),
305+ status_receiver = (
306+ self ._pv_power_wrapper .status_channel .new_receiver (limit = 1 )
307+ ),
308+ power_manager_requests_sender = (
309+ self ._pv_power_wrapper .proposal_channel .new_sender ()
310+ ),
311+ power_manager_bounds_subs_sender = (
312+ self ._pv_power_wrapper .bounds_subscription_channel .new_sender ()
313+ ),
314+ component_ids = pv_inverter_ids ,
315+ )
316+
317+ return PVPool (self ._pv_pool_reference_stores [ref_store_key ], name , priority )
318+
248319 def grid (self ) -> Grid :
249320 """Return the grid measuring point."""
250321 if self ._grid is None :
@@ -504,6 +575,43 @@ def battery_pool(
504575 return _get ().battery_pool (battery_ids , name , priority )
505576
506577
578+ def pv_pool (
579+ pv_inverter_ids : abc .Set [int ] | None = None ,
580+ name : str | None = None ,
581+ priority : int = - sys .maxsize - 1 ,
582+ ) -> PVPool :
583+ """Return a new `PVPool` instance for the given parameters.
584+
585+ The priority value is used to resolve conflicts when multiple actors are trying to
586+ propose different power values for the same set of PV inverters.
587+
588+ !!! note
589+ When specifying priority, bigger values indicate higher priority. The default
590+ priority is the lowest possible value.
591+
592+ It is recommended to reuse the same instance of the `PVPool` within the same
593+ actor, unless they are managing different sets of PV inverters.
594+
595+ In deployments with multiple actors managing the same set of PV inverters, it is
596+ recommended to use different priorities to distinguish between them. If not,
597+ a random prioritization will be imposed on them to resolve conflicts, which may
598+ lead to unexpected behavior like longer duration to converge on the desired
599+ power.
600+
601+ Args:
602+ pv_inverter_ids: Optional set of IDs of PV inverters to be managed by the
603+ `PVPool`. If not specified, all PV inverters available in the component
604+ graph are used.
605+ name: An optional name used to identify this instance of the pool or a
606+ corresponding actor in the logs.
607+ priority: The priority of the actor making the call.
608+
609+ Returns:
610+ A `PVPool` instance.
611+ """
612+ return _get ().pv_pool (pv_inverter_ids , name , priority )
613+
614+
507615def grid () -> Grid :
508616 """Return the grid measuring point."""
509617 return _get ().grid ()
0 commit comments