Skip to content

Commit aa4fe9a

Browse files
committed
Expose the PVPool through microgrid.pv_pool()
There is a plan to rename these constructors to `microgrid.new_*_pool()`. Not doing that now, so that it can be done together for all the pools in a separate PR. Signed-off-by: Sahas Subramanian <[email protected]>
1 parent be2f093 commit aa4fe9a

File tree

3 files changed

+118
-1
lines changed

3 files changed

+118
-1
lines changed

src/frequenz/sdk/microgrid/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@
124124
grid,
125125
logical_meter,
126126
producer,
127+
pv_pool,
127128
voltage,
128129
)
129130

@@ -149,5 +150,6 @@ async def initialize(host: str, port: int, resampler_config: ResamplerConfig) ->
149150
"frequency",
150151
"logical_meter",
151152
"producer",
153+
"pv_pool",
152154
"voltage",
153155
]

src/frequenz/sdk/microgrid/_data_pipeline.py

Lines changed: 109 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from dataclasses import dataclass
1818

1919
from frequenz.channels import Broadcast, Sender
20-
from frequenz.client.microgrid import ComponentCategory
20+
from frequenz.client.microgrid import ComponentCategory, InverterType
2121

2222
from ..actor._actor import Actor
2323
from ..timeseries._grid_frequency import GridFrequency
@@ -44,6 +44,8 @@
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+
507615
def grid() -> Grid:
508616
"""Return the grid measuring point."""
509617
return _get().grid()

src/frequenz/sdk/timeseries/pv_pool/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,10 @@
22
# Copyright © 2024 Frequenz Energy-as-a-Service GmbH
33

44
"""Interactions with PV inverters."""
5+
6+
from ._pv_pool import PVPool, PVPoolError
7+
8+
__all__ = [
9+
"PVPool",
10+
"PVPoolError",
11+
]

0 commit comments

Comments
 (0)