Skip to content

Commit a995e5b

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

File tree

5 files changed

+82
-2
lines changed

5 files changed

+82
-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
limit=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,13 +4,15 @@
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
from frequenz.client.microgrid import ComponentCategory
1213

1314
from ...actor import ChannelRegistry, ComponentMetricRequest
15+
from ...actor._power_managing import ReportRequest
1416
from ...actor.power_distributing import ComponentPoolStatus
1517
from ...microgrid import connection_manager
1618
from .._base_types import SystemBounds
@@ -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: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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 EV chargers."""
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 EV chargers.
28+
29+
These bounds are adjusted to any restrictions placed by actors with higher
30+
priorities.
31+
"""

0 commit comments

Comments
 (0)