Skip to content

Commit df875ee

Browse files
authored
Control methods for EVChargerPool using PowerManager and PowerDistributor (#900)
This PR adds new `propose_power` and `power_status` methods 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. The following changes were made: - An `EVChargerStatusTracker` implementation which tracks the health of an EV charger and whether an EV is connected. - `PowerDistributor` and `PowerManager` are now modular to support multiple component types. - Add an `EVChargerManager` that plugs into the `PowerDistributor` - Connect the `PowerDistributor` and `PowerManager` to the `EVChargerPool`. Fixes #872.
2 parents 1299db2 + 08c514e commit df875ee

File tree

29 files changed

+1850
-101
lines changed

29 files changed

+1850
-101
lines changed

RELEASE_NOTES.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
- Support for per-component interaction in `EVChargerPool` has been removed.
1818

19+
- 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.
20+
1921
## New Features
2022

2123
<!-- Here goes the main new features and examples or instructions on how to use them -->

benchmarks/power_distribution/power_distributor.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ async def run_test( # pylint: disable=too-many-locals
119119
requests_receiver=power_request_channel.new_receiver(),
120120
results_sender=power_result_channel.new_sender(),
121121
component_pool_status_sender=battery_status_channel.new_sender(),
122+
wait_for_data_sec=2.0,
122123
):
123124
tasks: list[Coroutine[Any, Any, list[Result]]] = []
124125
tasks.append(send_requests(batteries, num_requests))

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

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -133,17 +133,22 @@ def _add_bounds_tracker(self, component_ids: frozenset[int]) -> None:
133133
microgrid,
134134
)
135135

136-
if self._component_category is not ComponentCategory.BATTERY:
136+
bounds_receiver: Receiver[SystemBounds]
137+
# pylint: disable=protected-access
138+
if self._component_category is ComponentCategory.BATTERY:
139+
battery_pool = microgrid.battery_pool(component_ids)
140+
bounds_receiver = battery_pool._system_power_bounds.new_receiver()
141+
elif self._component_category is ComponentCategory.EV_CHARGER:
142+
ev_charger_pool = microgrid.ev_charger_pool(component_ids)
143+
bounds_receiver = ev_charger_pool._system_power_bounds.new_receiver()
144+
# pylint: enable=protected-access
145+
else:
137146
err = (
138147
"PowerManagingActor: Unsupported component category: "
139148
f"{self._component_category}"
140149
)
141150
_logger.error(err)
142151
raise NotImplementedError(err)
143-
battery_pool = microgrid.battery_pool(component_ids)
144-
# pylint: disable=protected-access
145-
bounds_receiver = battery_pool._system_power_bounds.new_receiver()
146-
# pylint: enable=protected-access
147152

148153
self._system_bounds[component_ids] = SystemBounds(
149154
timestamp=datetime.now(tz=timezone.utc),

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55

66
from ._battery_manager import BatteryManager
77
from ._component_manager import ComponentManager
8+
from ._ev_charger_manager import EVChargerManager
89

910
__all__ = [
1011
"BatteryManager",
1112
"ComponentManager",
13+
"EVChargerManager",
1214
]

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

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -127,8 +127,18 @@ class BatteryManager(ComponentManager):
127127
def __init__(
128128
self,
129129
component_pool_status_sender: Sender[ComponentPoolStatus],
130+
results_sender: Sender[Result],
130131
):
131-
"""Initialize the battery data manager."""
132+
"""Initialize this instance.
133+
134+
Args:
135+
component_pool_status_sender: Channel sender to send the status of the
136+
battery pool to. This status is used by the battery pool metric
137+
streams, to dynamically adjust the values based on the health of the
138+
individual batteries.
139+
results_sender: Channel sender to send the power distribution results to.
140+
"""
141+
self._results_sender = results_sender
132142
self._batteries = connection_manager.get().component_graph.components(
133143
component_categories={ComponentCategory.BATTERY}
134144
)
@@ -181,20 +191,18 @@ async def stop(self) -> None:
181191
await self._component_pool_status_tracker.stop()
182192

183193
@override
184-
async def distribute_power(self, request: Request) -> Result:
194+
async def distribute_power(self, request: Request) -> None:
185195
"""Distribute the requested power to the components.
186196
187197
Args:
188198
request: Request to get the distribution for.
189-
190-
Returns:
191-
Result of the distribution.
192199
"""
193200
distribution_result = await self._get_distribution(request)
194201
if not isinstance(distribution_result, DistributionResult):
195-
return distribution_result
196-
result = await self._distribute_power(request, distribution_result)
197-
return result
202+
result = distribution_result
203+
else:
204+
result = await self._distribute_power(request, distribution_result)
205+
await self._results_sender.send(result)
198206

199207
async def _get_distribution(self, request: Request) -> DistributionResult | Result:
200208
"""Get the distribution of the batteries.

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,14 @@ class ComponentManager(abc.ABC):
2020
def __init__(
2121
self,
2222
component_pool_status_sender: Sender[ComponentPoolStatus],
23+
results_sender: Sender[Result],
2324
):
2425
"""Initialize the component data manager.
2526
2627
Args:
2728
component_pool_status_sender: Channel for sending information about which
2829
components are expected to be working.
30+
results_sender: Channel for sending the results of power distribution.
2931
"""
3032

3133
@abc.abstractmethod
@@ -37,7 +39,7 @@ async def start(self) -> None:
3739
"""Start the component data manager."""
3840

3941
@abc.abstractmethod
40-
async def distribute_power(self, request: Request) -> Result:
42+
async def distribute_power(self, request: Request) -> None:
4143
"""Distribute the requested power to the components.
4244
4345
Args:
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# License: MIT
2+
# Copyright © 2024 Frequenz Energy-as-a-Service GmbH
3+
4+
"""Manage ev chargers for the power distributor."""
5+
6+
from ._ev_charger_manager import EVChargerManager
7+
8+
__all__ = [
9+
"EVChargerManager",
10+
]
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# License: MIT
2+
# Copyright © 2024 Frequenz Energy-as-a-Service GmbH
3+
4+
"""Configuration for the power distributor's EV charger manager."""
5+
6+
from collections import abc
7+
from dataclasses import dataclass, field
8+
from datetime import timedelta
9+
10+
from .....timeseries import Current
11+
12+
13+
@dataclass(frozen=True)
14+
class EVDistributionConfig:
15+
"""Configuration for the power distributor's EV charger manager."""
16+
17+
component_ids: abc.Set[int]
18+
"""The component ids of the EV chargers."""
19+
20+
min_current: Current = field(default_factory=lambda: Current.from_amperes(6.0))
21+
"""The minimum current that can be allocated to an EV charger."""
22+
23+
initial_current: Current = field(default_factory=lambda: Current.from_amperes(10.0))
24+
"""The initial current that can be allocated to an EV charger."""
25+
26+
increase_power_interval: timedelta = timedelta(seconds=60)
27+
"""The interval at which the power can be increased for an EV charger."""

0 commit comments

Comments
 (0)