Skip to content

Commit ab10cc3

Browse files
authored
Remove support for per-component interaction in EVChargerPool (#895)
It will be replaced by a `BatteryPool`-like interface.
2 parents 096b8b4 + 0176e10 commit ab10cc3

File tree

6 files changed

+8
-530
lines changed

6 files changed

+8
-530
lines changed

RELEASE_NOTES.md

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

1515
- The `maxsize` parameter in calls to `BatteryPool.{soc/capacity/temperature}.new_receiver()` methods have now been renamed to `limit`, to be consistent with the channels repository.
1616

17+
- Support for per-component interaction in `EVChargerPool` has been removed.
18+
1719
## New Features
1820

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

src/frequenz/sdk/microgrid/__init__.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -112,13 +112,6 @@
112112
[`power`][frequenz.sdk.timeseries.ev_charger_pool.EVChargerPool.power] method that
113113
streams the total power measured for all the {{glossary("ev-charger", "EV Chargers")}}
114114
at a site.
115-
116-
It also offers a
117-
[`component_data`][frequenz.sdk.timeseries.ev_charger_pool.EVChargerPool.component_data]
118-
method for fetching the status of individual EV Chargers, including state changes like
119-
when an EV is connected or disconnected, and a
120-
[`set_bounds`][frequenz.sdk.timeseries.ev_charger_pool.EVChargerPool.set_bounds] method
121-
to limit the charge power of individual EV Chargers.
122115
""" # noqa: D205, D400
123116

124117
from ..actor import ResamplerConfig

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

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,11 @@
33

44
"""Interactions with EV Chargers."""
55

6-
from ._ev_charger_pool import EVChargerData, EVChargerPool, EVChargerPoolError
6+
from ._ev_charger_pool import EVChargerPool, EVChargerPoolError
77
from ._set_current_bounds import ComponentCurrentLimit
8-
from ._state_tracker import EVChargerState
98

109
__all__ = [
1110
"ComponentCurrentLimit",
1211
"EVChargerPool",
13-
"EVChargerData",
1412
"EVChargerPoolError",
15-
"EVChargerState",
1613
]

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

Lines changed: 3 additions & 209 deletions
Original file line numberDiff line numberDiff line change
@@ -4,53 +4,28 @@
44
"""Interactions with pools of EV Chargers."""
55

66

7-
import asyncio
8-
import logging
97
import uuid
10-
from asyncio import Task
118
from collections import abc
12-
from dataclasses import dataclass
13-
from datetime import timedelta
149

15-
from frequenz.channels import Broadcast, ChannelClosedError, Receiver, Sender
16-
from frequenz.client.microgrid import ComponentCategory, ComponentMetricId
10+
from frequenz.channels import Sender
11+
from frequenz.client.microgrid import ComponentCategory
1712

18-
from ..._internal._asyncio import cancel_and_await
1913
from ...actor import ChannelRegistry, ComponentMetricRequest
2014
from ...microgrid import connection_manager
21-
from .. import Sample, Sample3Phase
22-
from .._quantities import Current, Power, Quantity
15+
from .._quantities import Current, Power
2316
from ..formula_engine import FormulaEngine, FormulaEngine3Phase
2417
from ..formula_engine._formula_engine_pool import FormulaEnginePool
2518
from ..formula_engine._formula_generators import (
2619
EVChargerCurrentFormula,
2720
EVChargerPowerFormula,
2821
FormulaGeneratorConfig,
2922
)
30-
from ._set_current_bounds import BoundsSetter, ComponentCurrentLimit
31-
from ._state_tracker import EVChargerState, StateTracker
32-
33-
_logger = logging.getLogger(__name__)
3423

3524

3625
class EVChargerPoolError(Exception):
3726
"""An error that occurred in any of the EVChargerPool methods."""
3827

3928

40-
@dataclass(frozen=True)
41-
class EVChargerData:
42-
"""Data for an EV Charger, including the 3-phase current and the component state."""
43-
44-
component_id: int
45-
"""The component ID of the EV Charger."""
46-
47-
current: Sample3Phase[Current]
48-
"""The 3-phase current of the EV Charger."""
49-
50-
state: EVChargerState
51-
"""The state of the EV Charger."""
52-
53-
5429
class EVChargerPool:
5530
"""An interface for interaction with pools of EV Chargers.
5631
@@ -64,21 +39,13 @@ class EVChargerPool:
6439
and 3-phase
6540
[`current`][frequenz.sdk.timeseries.ev_charger_pool.EVChargerPool.current]
6641
measurements of the EV Chargers in the pool.
67-
- The
68-
[`component_data`][frequenz.sdk.timeseries.ev_charger_pool.EVChargerPool.component_data]
69-
method for fetching the 3-phase current and state of individual EV Chargers in
70-
the pool.
71-
- The
72-
[`set_bounds`][frequenz.sdk.timeseries.ev_charger_pool.EVChargerPool.set_bounds]
73-
method for limiting the max current of individual EV Chargers in the pool.
7442
"""
7543

7644
def __init__(
7745
self,
7846
channel_registry: ChannelRegistry,
7947
resampler_subscription_sender: Sender[ComponentMetricRequest],
8048
component_ids: abc.Set[int] | None = None,
81-
repeat_interval: timedelta = timedelta(seconds=3.0),
8249
) -> None:
8350
"""Create an `EVChargerPool` instance.
8451
@@ -95,11 +62,8 @@ def __init__(
9562
component_ids: An optional list of component_ids belonging to this pool. If
9663
not specified, IDs of all EV Chargers in the microgrid will be fetched
9764
from the component graph.
98-
repeat_interval: Interval after which to repeat the last set bounds to the
99-
microgrid API, if no new calls to `set_bounds` have been made.
10065
"""
10166
self._channel_registry: ChannelRegistry = channel_registry
102-
self._repeat_interval: timedelta = repeat_interval
10367
self._resampler_subscription_sender: Sender[ComponentMetricRequest] = (
10468
resampler_subscription_sender
10569
)
@@ -114,17 +78,12 @@ def __init__(
11478
component_categories={ComponentCategory.EV_CHARGER}
11579
)
11680
}
117-
self._state_tracker: StateTracker | None = None
118-
self._status_streams: dict[int, tuple[Task[None], Broadcast[EVChargerData]]] = (
119-
{}
120-
)
12181
self._namespace: str = f"ev-charger-pool-{uuid.uuid4()}"
12282
self._formula_pool: FormulaEnginePool = FormulaEnginePool(
12383
self._namespace,
12484
self._channel_registry,
12585
self._resampler_subscription_sender,
12686
)
127-
self._bounds_setter: BoundsSetter | None = None
12887

12988
@property
13089
def component_ids(self) -> abc.Set[int]:
@@ -185,171 +144,6 @@ def power(self) -> FormulaEngine[Power]:
185144
assert isinstance(engine, FormulaEngine)
186145
return engine
187146

188-
def component_data(self, component_id: int) -> Receiver[EVChargerData]:
189-
"""Stream 3-phase current values and state of an EV Charger.
190-
191-
Args:
192-
component_id: id of the EV Charger for which data is requested.
193-
194-
Returns:
195-
A receiver that streams objects containing 3-phase current and state of
196-
an EV Charger.
197-
"""
198-
if recv := self._status_streams.get(component_id, None):
199-
task, output_chan = recv
200-
if not task.done():
201-
return output_chan.new_receiver()
202-
_logger.warning("Restarting component_status for id: %s", component_id)
203-
else:
204-
output_chan = Broadcast[EVChargerData](
205-
name=f"evpool-component_status-{component_id}"
206-
)
207-
208-
task = asyncio.create_task(
209-
self._stream_component_data(component_id, output_chan.new_sender())
210-
)
211-
212-
self._status_streams[component_id] = (task, output_chan)
213-
214-
return output_chan.new_receiver()
215-
216-
async def set_bounds(self, component_id: int, max_current: Current) -> None:
217-
"""Send given max current bound for the given EV Charger to the microgrid API.
218-
219-
Bounds are used to limit the max current drawn by an EV, although the exact
220-
value will be determined by the EV.
221-
222-
Args:
223-
component_id: ID of EV Charger to set the current bounds to.
224-
max_current: maximum current that an EV can draw from this EV Charger.
225-
"""
226-
if not self._bounds_setter:
227-
self._bounds_setter = BoundsSetter(self._repeat_interval)
228-
await self._bounds_setter.set(component_id, max_current.as_amperes())
229-
230-
def new_bounds_sender(self) -> Sender[ComponentCurrentLimit]:
231-
"""Return a `Sender` for setting EV Charger current bounds with.
232-
233-
Bounds are used to limit the max current drawn by an EV, although the exact
234-
value will be determined by the EV.
235-
236-
Returns:
237-
A new `Sender`.
238-
"""
239-
if not self._bounds_setter:
240-
self._bounds_setter = BoundsSetter(self._repeat_interval)
241-
return self._bounds_setter.new_bounds_sender()
242-
243147
async def stop(self) -> None:
244148
"""Stop all tasks and channels owned by the EVChargerPool."""
245-
if self._bounds_setter:
246-
await self._bounds_setter.stop()
247-
if self._state_tracker:
248-
await self._state_tracker.stop()
249149
await self._formula_pool.stop()
250-
for stream in self._status_streams.values():
251-
task, chan = stream
252-
await chan.close()
253-
await cancel_and_await(task)
254-
255-
async def _get_current_streams(self, component_id: int) -> tuple[
256-
Receiver[Sample[Quantity]],
257-
Receiver[Sample[Quantity]],
258-
Receiver[Sample[Quantity]],
259-
]:
260-
"""Fetch current streams from the resampler for each phase.
261-
262-
Args:
263-
component_id: id of EV Charger for which current streams are being fetched.
264-
265-
Returns:
266-
A tuple of 3 receivers stream resampled current values for the given
267-
component id, one for each phase.
268-
"""
269-
270-
async def resampler_subscribe(
271-
metric_id: ComponentMetricId,
272-
) -> Receiver[Sample[Quantity]]:
273-
request = ComponentMetricRequest(
274-
namespace="ev-pool",
275-
component_id=component_id,
276-
metric_id=metric_id,
277-
start_time=None,
278-
)
279-
await self._resampler_subscription_sender.send(request)
280-
return self._channel_registry.get_or_create(
281-
Sample[Quantity], request.get_channel_name()
282-
).new_receiver()
283-
284-
return (
285-
await resampler_subscribe(ComponentMetricId.CURRENT_PHASE_1),
286-
await resampler_subscribe(ComponentMetricId.CURRENT_PHASE_2),
287-
await resampler_subscribe(ComponentMetricId.CURRENT_PHASE_3),
288-
)
289-
290-
async def _stream_component_data(
291-
self,
292-
component_id: int,
293-
sender: Sender[EVChargerData],
294-
) -> None:
295-
"""Stream 3-phase current values and state of an EV Charger.
296-
297-
Args:
298-
component_id: id of the EV Charger for which data is requested.
299-
sender: A sender to stream EV Charger data to.
300-
301-
Raises:
302-
ChannelClosedError: If the channels from the resampler are closed.
303-
"""
304-
if not self._state_tracker:
305-
self._state_tracker = StateTracker(self._component_ids)
306-
307-
(phase_1_rx, phase_2_rx, phase_3_rx) = await self._get_current_streams(
308-
component_id
309-
)
310-
while True:
311-
try:
312-
(phase_1, phase_2, phase_3) = (
313-
await phase_1_rx.receive(),
314-
await phase_2_rx.receive(),
315-
await phase_3_rx.receive(),
316-
)
317-
except ChannelClosedError:
318-
_logger.exception("Streams closed for component_id=%s.", component_id)
319-
raise
320-
321-
sample = Sample3Phase(
322-
timestamp=phase_1.timestamp,
323-
value_p1=(
324-
None
325-
if phase_1.value is None
326-
else Current.from_amperes(phase_1.value.base_value)
327-
),
328-
value_p2=(
329-
None
330-
if phase_2.value is None
331-
else Current.from_amperes(phase_2.value.base_value)
332-
),
333-
value_p3=(
334-
None
335-
if phase_3.value is None
336-
else Current.from_amperes(phase_3.value.base_value)
337-
),
338-
)
339-
340-
if (
341-
phase_1.value is None
342-
and phase_2.value is None
343-
and phase_3.value is None
344-
):
345-
state = EVChargerState.MISSING
346-
else:
347-
state = self._state_tracker.get(component_id)
348-
349-
await sender.send(
350-
EVChargerData(
351-
component_id=component_id,
352-
current=sample,
353-
state=state,
354-
)
355-
)

0 commit comments

Comments
 (0)