Skip to content

Commit 7a19dcb

Browse files
Add logical meter formula for EV power
Create a formula engine from the component graph for calculating total EV Chargers power. Signed-off-by: Daniel Zullo <[email protected]>
1 parent 34d8f50 commit 7a19dcb

File tree

5 files changed

+113
-1
lines changed

5 files changed

+113
-1
lines changed

RELEASE_NOTES.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
<!-- Here goes the main new features and examples or instructions on how to use them -->
1414

1515
* A new class `OrderedRingBuffer` is now available, providing a sorted ring buffer of datetime-value pairs with tracking of any values that have not yet been written.
16-
16+
* Add logical meter formula for EV power.
1717

1818
## Bug Fixes
1919

src/frequenz/sdk/timeseries/logical_meter/_formula_generators/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from ._battery_power_formula import BatteryPowerFormula
77
from ._battery_soc_formula import BatterySoCFormula
88
from ._ev_charger_current_formula import EVChargerCurrentFormula
9+
from ._ev_charger_power_formula import EVChargerPowerFormula
910
from ._formula_generator import (
1011
ComponentNotFound,
1112
FormulaGenerationError,
@@ -26,6 +27,7 @@
2627
"GridPowerFormula",
2728
"BatteryPowerFormula",
2829
"BatterySoCFormula",
30+
"EVChargerPowerFormula",
2931
"PVPowerFormula",
3032
#
3133
# Current formula generators
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# License: MIT
2+
# Copyright © 2023 Frequenz Energy-as-a-Service GmbH
3+
4+
"""Formula generator from component graph for Grid Power."""
5+
6+
import logging
7+
8+
from .....sdk import microgrid
9+
from ....microgrid.component import ComponentCategory, ComponentMetricId
10+
from .._formula_engine import FormulaEngine
11+
from ._formula_generator import NON_EXISTING_COMPONENT_ID, FormulaGenerator
12+
13+
logger = logging.getLogger(__name__)
14+
15+
16+
class EVChargerPowerFormula(FormulaGenerator):
17+
"""Create a formula engine from the component graph for calculating grid power."""
18+
19+
async def generate(self) -> FormulaEngine:
20+
"""Generate a formula for calculating total EV power from the component graph.
21+
22+
Returns:
23+
A formula engine that calculates total EV charger power values.
24+
"""
25+
builder = self._get_builder("ev-power", ComponentMetricId.ACTIVE_POWER)
26+
component_graph = microgrid.get().component_graph
27+
ev_chargers = [
28+
comp
29+
for comp in component_graph.components()
30+
if comp.category == ComponentCategory.EV_CHARGER
31+
]
32+
33+
if not ev_chargers:
34+
logger.warning(
35+
"Unable to find any EV Chargers in the component graph. "
36+
"Subscribing to the resampling actor with a non-existing "
37+
"component id, so that `0` values are sent from the formula."
38+
)
39+
# If there are no EV Chargers, we have to send 0 values as the same
40+
# frequency as the other streams. So we subscribe with a non-existing
41+
# component id, just to get a `None` message at the resampling interval.
42+
await builder.push_component_metric(
43+
NON_EXISTING_COMPONENT_ID, nones_are_zeros=True
44+
)
45+
return builder.build()
46+
47+
for idx, comp in enumerate(ev_chargers):
48+
if idx > 0:
49+
builder.push_oper("+")
50+
51+
await builder.push_component_metric(comp.component_id, nones_are_zeros=True)
52+
53+
return builder.build()

src/frequenz/sdk/timeseries/logical_meter/_logical_meter.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
BatteryPowerFormula,
2828
BatterySoCFormula,
2929
EVChargerCurrentFormula,
30+
EVChargerPowerFormula,
3031
FormulaGenerator,
3132
GridCurrentFormula,
3233
GridPowerFormula,
@@ -224,6 +225,18 @@ async def ev_charger_current(self) -> FormulaReceiver3Phase:
224225
"ev_charger_current", EVChargerCurrentFormula
225226
)
226227

228+
async def ev_charger_power(self) -> FormulaReceiver:
229+
"""Fetch the cumulative EV charger power for the microgrid.
230+
231+
If a formula engine to calculate EV charger power is not already
232+
running, it will be started. Else, we'll just get a new receiver
233+
to the already existing data stream.
234+
235+
Returns:
236+
A *new* receiver that will stream ev_charger_power values.
237+
"""
238+
return await self._get_formula_stream("ev_charger_power", EVChargerPowerFormula)
239+
227240
async def battery_power(self) -> FormulaReceiver:
228241
"""Fetch the cumulative battery power in the microgrid.
229242

tests/timeseries/test_logical_meter.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -460,3 +460,47 @@ async def test_3_phase_formulas(self, mocker: MockerFixture) -> None:
460460

461461
await mockgrid.cleanup()
462462
await engine._stop() # pylint: disable=protected-access
463+
464+
async def test_ev_power( # pylint: disable=too-many-locals
465+
self,
466+
mocker: MockerFixture,
467+
) -> None:
468+
"""Test the battery power and pv power formulas."""
469+
mockgrid = MockMicrogrid(grid_side_meter=False)
470+
mockgrid.add_ev_chargers(5)
471+
request_sender, channel_registry = await mockgrid.start(mocker)
472+
logical_meter = LogicalMeter(
473+
channel_registry,
474+
request_sender,
475+
microgrid.get().component_graph,
476+
)
477+
478+
main_meter_recv = await self._get_resampled_stream(
479+
logical_meter,
480+
channel_registry,
481+
request_sender,
482+
mockgrid.main_meter_id,
483+
ComponentMetricId.ACTIVE_POWER,
484+
)
485+
grid_power_recv = await logical_meter.grid_power()
486+
ev_power_recv = await logical_meter.ev_charger_power()
487+
488+
await self._synchronize_receivers(
489+
[grid_power_recv, main_meter_recv, ev_power_recv]
490+
)
491+
492+
ev_results = []
493+
for _ in range(10):
494+
grid_pow = await grid_power_recv.receive()
495+
ev_pow = await ev_power_recv.receive()
496+
main_pow = await main_meter_recv.receive()
497+
498+
assert grid_pow is not None and grid_pow.value is not None
499+
assert ev_pow is not None and ev_pow.value is not None
500+
assert main_pow is not None and main_pow.value is not None
501+
assert isclose(grid_pow.value, ev_pow.value + main_pow.value)
502+
503+
ev_results.append(ev_pow.value)
504+
505+
await mockgrid.cleanup()
506+
assert len(ev_results) == 10

0 commit comments

Comments
 (0)