Skip to content

Commit be82217

Browse files
authored
Add a PVPool implementation (#914)
2 parents 6637d7f + 6e1400d commit be82217

File tree

32 files changed

+1699
-96
lines changed

32 files changed

+1699
-96
lines changed

RELEASE_NOTES.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,14 @@
1818

1919
- New `propose_power` and `power_status` methods have been added to the `EVChargerPool` similar to the `BatteryPool`. These method interface with the `PowerManager` and `PowerDistributor`, which currently uses a first-come-first-serve algorithm to distribute power to EVs.
2020

21+
- PV Power is now available from `microgrid.pv_pool().power`, and no longer from `microgrid.logical_meter().pv_power`.
22+
2123
## New Features
2224

2325
- Warning messages are logged when multiple instances of `*Pool`s are created for the same set of batteries, with the same priority values.
2426

27+
- A PV Pool, with `propose_power`, `power_status` and `power` methods similar to Battery and EV Pools.
28+
2529
## Bug Fixes
2630

2731
- A bug was fixed where the grid fuse was not created properly and would end up with a `max_current` with type `float` instead of `Current`.

benchmarks/power_distribution/power_distributor.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ async def run_test( # pylint: disable=too-many-locals
116116
power_result_channel = Broadcast[Result](name="power-result")
117117
async with PowerDistributingActor(
118118
component_category=ComponentCategory.BATTERY,
119+
component_type=None,
119120
requests_receiver=power_request_channel.new_receiver(),
120121
results_sender=power_result_channel.new_sender(),
121122
component_pool_status_sender=battery_status_channel.new_sender(),

src/frequenz/sdk/_internal/_channels.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99

1010
from frequenz.channels import Receiver
1111

12+
from ._asyncio import cancel_and_await
13+
1214
T_co = typing.TypeVar("T_co", covariant=True)
1315

1416

@@ -72,3 +74,7 @@ def has_value(self) -> bool:
7274
async def _run(self) -> None:
7375
async for value in self._receiver:
7476
self._latest_value = value
77+
78+
async def stop(self) -> None:
79+
"""Stop the cache."""
80+
await cancel_and_await(self._task)

src/frequenz/sdk/actor/_power_managing/_power_managing_actor.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
from frequenz.channels import Receiver, Sender, select, selected_from
1414
from frequenz.channels.timer import SkipMissedAndDrift, Timer
15-
from frequenz.client.microgrid import ComponentCategory
15+
from frequenz.client.microgrid import ComponentCategory, ComponentType, InverterType
1616
from typing_extensions import override
1717

1818
from ...timeseries._base_types import SystemBounds
@@ -32,28 +32,37 @@ class PowerManagingActor(Actor):
3232

3333
def __init__( # pylint: disable=too-many-arguments
3434
self,
35-
component_category: ComponentCategory,
3635
proposals_receiver: Receiver[Proposal],
3736
bounds_subscription_receiver: Receiver[ReportRequest],
3837
power_distributing_requests_sender: Sender[power_distributing.Request],
3938
power_distributing_results_receiver: Receiver[power_distributing.Result],
4039
channel_registry: ChannelRegistry,
40+
*,
41+
component_category: ComponentCategory,
42+
component_type: ComponentType | None = None,
4143
# arguments to actors need to serializable, so we pass an enum for the algorithm
4244
# instead of an instance of the algorithm.
4345
algorithm: Algorithm = Algorithm.MATRYOSHKA,
4446
):
4547
"""Create a new instance of the power manager.
4648
4749
Args:
48-
component_category: The category of the component this power manager
49-
instance is going to support.
5050
proposals_receiver: The receiver for proposals.
5151
bounds_subscription_receiver: The receiver for bounds subscriptions.
5252
power_distributing_requests_sender: The sender for power distribution
5353
requests.
5454
power_distributing_results_receiver: The receiver for power distribution
5555
results.
5656
channel_registry: The channel registry.
57+
component_category: The category of the component this power manager
58+
instance is going to support.
59+
component_type: The type of the component of the given category that this
60+
actor is responsible for. This is used only when the component category
61+
is not enough to uniquely identify the component. For example, when the
62+
category is `ComponentCategory.INVERTER`, the type is needed to identify
63+
the inverter as a solar inverter or a battery inverter. This can be
64+
`None` when the component category is enough to uniquely identify the
65+
component.
5766
algorithm: The power management algorithm to use.
5867
5968
Raises:
@@ -65,6 +74,7 @@ def __init__( # pylint: disable=too-many-arguments
6574
)
6675

6776
self._component_category = component_category
77+
self._component_type = component_type
6878
self._bounds_subscription_receiver = bounds_subscription_receiver
6979
self._power_distributing_requests_sender = power_distributing_requests_sender
7080
self._power_distributing_results_receiver = power_distributing_results_receiver
@@ -141,6 +151,12 @@ def _add_bounds_tracker(self, component_ids: frozenset[int]) -> None:
141151
elif self._component_category is ComponentCategory.EV_CHARGER:
142152
ev_charger_pool = microgrid.ev_charger_pool(component_ids)
143153
bounds_receiver = ev_charger_pool._system_power_bounds.new_receiver()
154+
elif (
155+
self._component_category is ComponentCategory.INVERTER
156+
and self._component_type is InverterType.SOLAR
157+
):
158+
pv_pool = microgrid.pv_pool(component_ids)
159+
bounds_receiver = pv_pool._system_power_bounds.new_receiver()
144160
# pylint: enable=protected-access
145161
else:
146162
err = (

src/frequenz/sdk/actor/power_distributing/_component_managers/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@
66
from ._battery_manager import BatteryManager
77
from ._component_manager import ComponentManager
88
from ._ev_charger_manager import EVChargerManager
9+
from ._pv_inverter_manager import PVManager
910

1011
__all__ = [
1112
"BatteryManager",
1213
"ComponentManager",
1314
"EVChargerManager",
15+
"PVManager",
1416
]

src/frequenz/sdk/actor/power_distributing/_component_managers/_battery_manager.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,10 @@ async def start(self) -> None:
188188
@override
189189
async def stop(self) -> None:
190190
"""Stop the battery data manager."""
191+
for bat_cache in self._battery_caches.values():
192+
await bat_cache.stop()
193+
for inv_cache in self._inverter_caches.values():
194+
await inv_cache.stop()
191195
await self._component_pool_status_tracker.stop()
192196

193197
@override
@@ -668,6 +672,13 @@ def _parse_result(
668672
battery_ids,
669673
request_timeout.total_seconds(),
670674
)
675+
except Exception: # pylint: disable=broad-except
676+
failed_power += distribution[inverter_id]
677+
failed_batteries = failed_batteries.union(battery_ids)
678+
_logger.exception(
679+
"Unknown error while setting power to batteries: %s",
680+
battery_ids,
681+
)
671682

672683
return failed_power, failed_batteries
673684

src/frequenz/sdk/actor/power_distributing/_component_managers/_ev_charger_manager/_ev_charger_manager.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ async def distribute_power(self, request: Request) -> None:
9292
@override
9393
async def stop(self) -> None:
9494
"""Stop the ev charger manager."""
95+
await self._voltage_cache.stop()
9596
await self._component_pool_status_tracker.stop()
9697

9798
def _get_ev_charger_ids(self) -> collections.abc.Set[int]:
@@ -330,16 +331,21 @@ async def _set_api_power(
330331
succeeded_components.add(component_id)
331332

332333
match task.exception():
333-
case asyncio.CancelledError:
334+
case asyncio.CancelledError():
334335
_logger.warning(
335336
"Timeout while setting power to EV charger %s", component_id
336337
)
337-
case grpc.aio.AioRpcError as err:
338+
case grpc.aio.AioRpcError() as err:
338339
_logger.warning(
339340
"Error while setting power to EV charger %s: %s",
340341
component_id,
341342
err,
342343
)
344+
case Exception():
345+
_logger.exception(
346+
"Unknown error while setting power to EV charger: %s",
347+
component_id,
348+
)
343349
if failed_components:
344350
return PartialFailure(
345351
failed_components=failed_components,
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# License: MIT
2+
# Copyright © 2024 Frequenz Energy-as-a-Service GmbH
3+
4+
"""Manage PV inverters for the power distributor."""
5+
6+
from ._pv_inverter_manager import PVManager
7+
8+
__all__ = ["PVManager"]

0 commit comments

Comments
 (0)