Skip to content

Commit d75b37a

Browse files
committed
Add power_status method to EVChargerPool
Signed-off-by: Sahas Subramanian <[email protected]>
1 parent 31ca2e1 commit d75b37a

File tree

5 files changed

+87
-2
lines changed

5 files changed

+87
-2
lines changed

src/frequenz/sdk/microgrid/_data_pipeline.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,9 @@ def ev_charger_pool(
202202
status_receiver=self._ev_power_wrapper.status_channel.new_receiver(
203203
maxsize=1
204204
),
205+
power_manager_bounds_subs_sender=(
206+
self._ev_power_wrapper.bounds_subscription_channel.new_sender()
207+
),
205208
component_ids=ev_charger_ids,
206209
)
207210
return EVChargerPool(self._ev_charger_pools[key], name, priority)

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44
"""Interactions with EV Chargers."""
55

66
from ._ev_charger_pool import EVChargerPool, EVChargerPoolError
7+
from ._result_types import EVChargerPoolReport
78

89
__all__ = [
910
"EVChargerPool",
1011
"EVChargerPoolError",
12+
"EVChargerPoolReport",
1113
]

src/frequenz/sdk/timeseries/ev_charger_pool/_ev_charger_pool.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@
44
"""Interactions with pools of EV Chargers."""
55

66

7+
import asyncio
8+
import typing
79
import uuid
810
from collections import abc
911

1012
from ..._internal._channels import ReceiverFetcher
13+
from ...actor import _power_managing
1114
from .._base_types import SystemBounds
1215
from .._quantities import Current, Power
1316
from ..formula_engine import FormulaEngine, FormulaEngine3Phase
@@ -17,6 +20,7 @@
1720
FormulaGeneratorConfig,
1821
)
1922
from ._ev_charger_pool_reference_store import EVChargerPoolReferenceStore
23+
from ._result_types import EVChargerPoolReport
2024

2125

2226
class EVChargerPoolError(Exception):
@@ -58,7 +62,7 @@ def __init__( # pylint: disable=too-many-arguments
5862
priority: The priority of the actor using this wrapper.
5963
"""
6064
self._ev_charger_pool = ev_charger_pool_ref
61-
unique_id = uuid.uuid4()
65+
unique_id = str(uuid.uuid4())
6266
self._source_id = unique_id if name is None else f"{name}-{unique_id}"
6367
self._priority = priority
6468

@@ -125,6 +129,38 @@ def power(self) -> FormulaEngine[Power]:
125129
assert isinstance(engine, FormulaEngine)
126130
return engine
127131

132+
@property
133+
def power_status(self) -> ReceiverFetcher[EVChargerPoolReport]:
134+
"""Get a receiver to receive new power status reports when they change.
135+
136+
These include
137+
- the current inclusion/exclusion bounds available for the pool's priority,
138+
- the current target power for the pool's set of batteries,
139+
- the result of the last distribution request for the pool's set of batteries.
140+
141+
Returns:
142+
A receiver that will stream power status reports for the pool's priority.
143+
"""
144+
sub = _power_managing.ReportRequest(
145+
source_id=self._source_id,
146+
priority=self._priority,
147+
component_ids=self._ev_charger_pool.component_ids,
148+
)
149+
self._ev_charger_pool.power_bounds_subs[sub.get_channel_name()] = (
150+
asyncio.create_task(
151+
self._ev_charger_pool.power_manager_bounds_subs_sender.send(sub)
152+
)
153+
)
154+
channel = self._ev_charger_pool.channel_registry.get_or_create(
155+
_power_managing._Report, # pylint: disable=protected-access
156+
sub.get_channel_name(),
157+
)
158+
channel.resend_latest = True
159+
160+
# More details on why the cast is needed here:
161+
# https://github.com/frequenz-floss/frequenz-sdk-python/issues/823
162+
return typing.cast(ReceiverFetcher[EVChargerPoolReport], channel)
163+
128164
async def stop(self) -> None:
129165
"""Stop all tasks and channels owned by the EVChargerPool."""
130166
await self._ev_charger_pool.stop()

src/frequenz/sdk/timeseries/ev_charger_pool/_ev_charger_pool_reference_store.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@
44
"""Manages shared state/tasks for a set of EV chargers."""
55

66

7+
import asyncio
78
import uuid
89
from collections import abc
910

1011
from frequenz.channels import Broadcast, Receiver, Sender
1112

1213
from ...actor import ChannelRegistry, ComponentMetricRequest
14+
from ...actor._power_managing import ReportRequest
1315
from ...actor.power_distributing import ComponentPoolStatus
1416
from ...microgrid import connection_manager
1517
from ...microgrid.component import ComponentCategory
@@ -31,11 +33,12 @@ class EVChargerPoolReferenceStore:
3133
They are exposed through the EVChargerPool class.
3234
"""
3335

34-
def __init__(
36+
def __init__( # pylint: disable=too-many-arguments
3537
self,
3638
channel_registry: ChannelRegistry,
3739
resampler_subscription_sender: Sender[ComponentMetricRequest],
3840
status_receiver: Receiver[ComponentPoolStatus],
41+
power_manager_bounds_subs_sender: Sender[ReportRequest],
3942
component_ids: abc.Set[int] | None = None,
4043
):
4144
"""Create an instance of the class.
@@ -47,13 +50,16 @@ def __init__(
4750
resampling actor.
4851
status_receiver: A receiver that streams the status of the EV Chargers in
4952
the pool.
53+
power_manager_bounds_subs_sender: A Channel sender for sending power bounds
54+
subscription requests to the power managing actor.
5055
component_ids: An optional list of component_ids belonging to this pool. If
5156
not specified, IDs of all EV Chargers in the microgrid will be fetched
5257
from the component graph.
5358
"""
5459
self.channel_registry = channel_registry
5560
self.resampler_subscription_sender = resampler_subscription_sender
5661
self.status_receiver = status_receiver
62+
self.power_manager_bounds_subs_sender = power_manager_bounds_subs_sender
5763

5864
if component_ids is not None:
5965
self.component_ids: frozenset[int] = frozenset(component_ids)
@@ -68,6 +74,8 @@ def __init__(
6874
}
6975
)
7076

77+
self.power_bounds_subs: dict[str, asyncio.Task[None]] = {}
78+
7179
self.namespace: str = f"ev-charger-pool-{uuid.uuid4()}"
7280
self.formula_pool = FormulaEnginePool(
7381
self.namespace,
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# License: MIT
2+
# Copyright © 2024 Frequenz Energy-as-a-Service GmbH
3+
4+
"""Types for exposing EV charger pool reports."""
5+
6+
import typing
7+
8+
from ...actor import power_distributing
9+
from .._base_types import Bounds
10+
from .._quantities import Power
11+
12+
13+
class EVChargerPoolReport(typing.Protocol):
14+
"""A status report for an EV chargers pool."""
15+
16+
target_power: Power | None
17+
"""The currently set power for the batteries."""
18+
19+
distribution_result: power_distributing.Result | None
20+
"""The result of the last power distribution.
21+
22+
This is `None` if no power distribution has been performed yet.
23+
"""
24+
25+
@property
26+
def bounds(self) -> Bounds[Power] | None:
27+
"""The usable bounds for the batteries.
28+
29+
These bounds are adjusted to any restrictions placed by actors with higher
30+
priorities.
31+
32+
There might be exclusion zones within these bounds. If necessary, the
33+
[`adjust_to_bounds`][frequenz.sdk.timeseries.battery_pool.BatteryPoolReport.adjust_to_bounds]
34+
method may be used to check if a desired power value fits the bounds, or to get
35+
the closest possible power values that do fit the bounds.
36+
"""

0 commit comments

Comments
 (0)