Skip to content

Commit 837d482

Browse files
Add power formula to BatteryPool (#338)
And remove `battery_power` from the `LogicalMeter`.
2 parents 4cdecb1 + c1114b3 commit 837d482

File tree

11 files changed

+116
-57
lines changed

11 files changed

+116
-57
lines changed

RELEASE_NOTES.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@
66

77
## Upgrading
88

9+
* Battery power is no longer available through the `LogicalMeter`, but through the `BatteryPool` (#338)
10+
11+
``` python
12+
battery_power_receiver = microgrid.battery_pool().power.new_receiver()
13+
```
14+
915
+ Formulas composition has changed (#327) -
1016
- receivers from formulas are no longer composable.
1117
- formula composition is now done by composing FormulaEngine instances.
@@ -18,7 +24,7 @@
1824

1925
self._inverter_power = (
2026
microgrid.logical_meter().pv_power
21-
+ microgrid.logical_meter().battery_power
27+
+ microgrid.battery_pool().power
2228
).build("inverter_power")
2329

2430
inverter_power_receiver = self._inverter_power.new_receiver()

src/frequenz/sdk/microgrid/_data_pipeline.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,18 @@
1010

1111
from __future__ import annotations
1212

13+
import logging
1314
import typing
1415
from collections import abc
1516
from dataclasses import dataclass
1617

1718
from frequenz.channels import Bidirectional, Broadcast, Sender
1819

20+
from . import connection_manager
21+
from .component import ComponentCategory
22+
23+
logger = logging.getLogger(__name__)
24+
1925
# A number of imports had to be done inside functions where they are used, to break
2026
# import cycles.
2127
#
@@ -101,7 +107,6 @@ def logical_meter(self) -> LogicalMeter:
101107
Returns:
102108
A logical meter instance.
103109
"""
104-
from ..microgrid import connection_manager
105110
from ..timeseries.logical_meter import LogicalMeter
106111

107112
if self._logical_meter is None:
@@ -171,6 +176,8 @@ def battery_pool(
171176

172177
if key not in self._battery_pools:
173178
self._battery_pools[key] = BatteryPool(
179+
channel_registry=self._channel_registry,
180+
resampler_subscription_sender=self._resampling_request_sender(),
174181
batteries_status_receiver=self._battery_status_channel.new_receiver(
175182
maxsize=1
176183
),
@@ -196,6 +203,16 @@ def _start_power_distributing_actor(self) -> None:
196203
if self._power_distributing_actor:
197204
return
198205

206+
component_graph = connection_manager.get().component_graph
207+
if not component_graph.components(
208+
component_category={ComponentCategory.BATTERY}
209+
):
210+
logger.warning(
211+
"No batteries found in the component graph. "
212+
"The power distributing actor will not be started."
213+
)
214+
return
215+
199216
from ..actor.power_distributing import PowerDistributingActor
200217

201218
# The PowerDistributingActor is started with only a single default user channel.

src/frequenz/sdk/timeseries/_formula_engine/_formula_generators/_battery_power_formula.py

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,13 @@
66
import logging
77

88
from ....microgrid import connection_manager
9-
from ....microgrid.component import ComponentCategory, ComponentMetricId, InverterType
9+
from ....microgrid.component import ComponentMetricId
1010
from ..._formula_engine import FormulaEngine
11-
from ._formula_generator import NON_EXISTING_COMPONENT_ID, FormulaGenerator
11+
from ._formula_generator import (
12+
NON_EXISTING_COMPONENT_ID,
13+
ComponentNotFound,
14+
FormulaGenerator,
15+
)
1216

1317
logger = logging.getLogger(__name__)
1418

@@ -37,28 +41,32 @@ def generate(
3741
in the component graph.
3842
"""
3943
builder = self._get_builder("battery-power", ComponentMetricId.ACTIVE_POWER)
40-
component_graph = connection_manager.get().component_graph
41-
battery_inverters = list(
42-
comp
43-
for comp in component_graph.components()
44-
if comp.category == ComponentCategory.INVERTER
45-
and comp.type == InverterType.BATTERY
46-
)
47-
48-
if not battery_inverters:
44+
component_ids = self._config.component_ids
45+
if not component_ids:
4946
logger.warning(
50-
"Unable to find any battery inverters in the component graph. "
47+
"No Battery component IDs specified. "
5148
"Subscribing to the resampling actor with a non-existing "
5249
"component id, so that `0` values are sent from the formula."
5350
)
54-
# If there are no battery inverters, we have to send 0 values as the same
55-
# frequency as the other streams. So we subscribe with a non-existing
51+
# If there are no Batteries, we have to send 0 values as the same
52+
# frequency as the other streams. So we subscribe with a non-existing
5653
# component id, just to get a `None` message at the resampling interval.
5754
builder.push_component_metric(
5855
NON_EXISTING_COMPONENT_ID, nones_are_zeros=True
5956
)
6057
return builder.build()
6158

59+
component_graph = connection_manager.get().component_graph
60+
61+
battery_inverters = list(
62+
next(iter(component_graph.predecessors(bat_id))) for bat_id in component_ids
63+
)
64+
65+
if len(component_ids) != len(battery_inverters):
66+
raise ComponentNotFound(
67+
"Can't find inverters for all batteries from the component graph."
68+
)
69+
6270
for idx, comp in enumerate(battery_inverters):
6371
if idx > 0:
6472
builder.push_oper("+")

src/frequenz/sdk/timeseries/_formula_engine/_formula_generators/_ev_charger_current_formula.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from __future__ import annotations
77

88
import logging
9+
from collections import abc
910

1011
from ....microgrid.component import ComponentMetricId
1112
from .._formula_engine import FormulaEngine, FormulaEngine3Phase
@@ -67,7 +68,7 @@ def generate(self) -> FormulaEngine3Phase:
6768

6869
def _gen_phase_formula(
6970
self,
70-
component_ids: set[int],
71+
component_ids: abc.Set[int],
7172
metric_id: ComponentMetricId,
7273
) -> FormulaEngine:
7374
builder = self._get_builder("ev-current", metric_id)

src/frequenz/sdk/timeseries/_formula_engine/_formula_generators/_formula_generator.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import sys
99
from abc import ABC, abstractmethod
10+
from collections import abc
1011
from dataclasses import dataclass
1112

1213
from frequenz.channels import Sender
@@ -32,7 +33,7 @@ class ComponentNotFound(FormulaGenerationError):
3233
class FormulaGeneratorConfig:
3334
"""Config for formula generators."""
3435

35-
component_ids: set[int] | None = None
36+
component_ids: abc.Set[int] | None = None
3637

3738

3839
class FormulaGenerator(ABC):

src/frequenz/sdk/timeseries/battery_pool/battery_pool.py

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,24 @@
66
from __future__ import annotations
77

88
import asyncio
9+
import uuid
910
from collections.abc import Set
1011
from datetime import timedelta
1112
from typing import Any
1213

13-
from frequenz.channels import Receiver
14+
from frequenz.channels import Receiver, Sender
1415

1516
from ..._internal._constants import RECEIVER_MAX_SIZE
1617
from ..._internal.asyncio import cancel_and_await
18+
from ...actor import ChannelRegistry, ComponentMetricRequest
1719
from ...actor.power_distributing._battery_pool_status import BatteryStatus
1820
from ...microgrid import connection_manager
1921
from ...microgrid.component import ComponentCategory
22+
from .._formula_engine import FormulaEngine, FormulaEnginePool
23+
from .._formula_engine._formula_generators import (
24+
BatteryPowerFormula,
25+
FormulaGeneratorConfig,
26+
)
2027
from ._methods import AggregateMethod, SendOnUpdate
2128
from ._metric_calculator import CapacityCalculator, PowerBoundsCalculator, SoCCalculator
2229
from ._result_types import CapacityMetrics, PowerMetrics, SoCMetrics
@@ -29,15 +36,21 @@ class BatteryPool:
2936
fetching high level metrics for this subset.
3037
"""
3138

32-
def __init__(
39+
def __init__( # pylint: disable=too-many-arguments
3340
self,
41+
channel_registry: ChannelRegistry,
42+
resampler_subscription_sender: Sender[ComponentMetricRequest],
3443
batteries_status_receiver: Receiver[BatteryStatus],
3544
min_update_interval: timedelta,
3645
batteries_id: Set[int] | None = None,
3746
) -> None:
3847
"""Create the class instance.
3948
4049
Args:
50+
channel_registry: A channel registry instance shared with the resampling
51+
actor.
52+
resampler_subscription_sender: A sender for sending metric requests to the
53+
resampling actor.
4154
batteries_status_receiver: Receiver to receive status of the batteries.
4255
Receivers should has maxsize = 1 to fetch only the latest status.
4356
Battery status channel should has resend_latest = True.
@@ -64,13 +77,21 @@ def __init__(
6477

6578
self._working_batteries: set[int] = set()
6679

67-
self._update_battery_status_task = asyncio.create_task(
68-
self._update_battery_status(batteries_status_receiver)
69-
)
80+
if self._batteries:
81+
self._update_battery_status_task = asyncio.create_task(
82+
self._update_battery_status(batteries_status_receiver)
83+
)
7084

7185
self._min_update_interval = min_update_interval
7286
self._active_methods: dict[str, AggregateMethod[Any]] = {}
7387

88+
self._namespace: str = f"battery-pool-{self._batteries}-{uuid.uuid4()}"
89+
self._formula_pool: FormulaEnginePool = FormulaEnginePool(
90+
self._namespace,
91+
channel_registry,
92+
resampler_subscription_sender,
93+
)
94+
7495
@property
7596
def battery_ids(self) -> Set[int]:
7697
"""Return ids of the batteries in the pool.
@@ -80,6 +101,26 @@ def battery_ids(self) -> Set[int]:
80101
"""
81102
return self._batteries
82103

104+
@property
105+
def power(self) -> FormulaEngine:
106+
"""Fetch the total power of the batteries in the pool.
107+
108+
If a formula engine to calculate this metric is not already running, it will be
109+
started.
110+
111+
A receiver from the formula engine can be obtained by calling the `new_receiver`
112+
method.
113+
114+
Returns:
115+
A FormulaEngine that will calculate and stream the total power of all
116+
batteries in the pool.
117+
"""
118+
return self._formula_pool.from_generator(
119+
"battery_pool_power",
120+
BatteryPowerFormula,
121+
FormulaGeneratorConfig(component_ids=self._batteries),
122+
) # type: ignore[return-value]
123+
83124
async def soc(
84125
self, maxsize: int | None = RECEIVER_MAX_SIZE
85126
) -> Receiver[SoCMetrics | None]:

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

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
from ...microgrid.component import ComponentMetricId
1616
from .._formula_engine import FormulaEngine, FormulaEngine3Phase, FormulaEnginePool
1717
from .._formula_engine._formula_generators import (
18-
BatteryPowerFormula,
1918
GridCurrentFormula,
2019
GridPowerFormula,
2120
PVPowerFormula,
@@ -176,24 +175,6 @@ def grid_current(self) -> FormulaEngine3Phase:
176175
GridCurrentFormula,
177176
) # type: ignore[return-value]
178177

179-
@property
180-
def battery_power(self) -> FormulaEngine:
181-
"""Fetch the cumulative battery power in the microgrid.
182-
183-
If a formula engine to calculate cumulative battery power is not already
184-
running, it will be started.
185-
186-
A receiver from the formula engine can be created using the `new_receiver`
187-
method.
188-
189-
Returns:
190-
A FormulaEngine that will calculate and stream battery power.
191-
"""
192-
return self._formula_pool.from_generator(
193-
"battery_power",
194-
BatteryPowerFormula,
195-
) # type: ignore[return-value]
196-
197178
@property
198179
def pv_power(self) -> FormulaEngine:
199180
"""Fetch the PV power production in the microgrid.

tests/timeseries/_formula_engine/test_formula_composition.py

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -28,18 +28,17 @@ async def test_formula_composition( # pylint: disable=too-many-locals
2828
mockgrid.add_solar_inverters(2)
2929
await mockgrid.start(mocker)
3030
logical_meter = microgrid.logical_meter()
31-
31+
battery_pool = microgrid.battery_pool()
3232
main_meter_recv = get_resampled_stream(
33+
logical_meter._namespace, # pylint: disable=protected-access
3334
4,
3435
ComponentMetricId.ACTIVE_POWER,
3536
)
3637
grid_power_recv = logical_meter.grid_power.new_receiver()
37-
battery_power_recv = logical_meter.battery_power.new_receiver()
38+
battery_power_recv = battery_pool.power.new_receiver()
3839
pv_power_recv = logical_meter.pv_power.new_receiver()
3940

40-
engine = (logical_meter.pv_power + logical_meter.battery_power).build(
41-
"inv_power"
42-
)
41+
engine = (logical_meter.pv_power + battery_pool.power).build("inv_power")
4342
inv_calc_recv = engine.new_receiver()
4443

4544
count = 0
@@ -69,13 +68,12 @@ async def test_formula_composition_missing_pv(self, mocker: MockerFixture) -> No
6968
mockgrid = MockMicrogrid(grid_side_meter=False)
7069
mockgrid.add_batteries(3)
7170
await mockgrid.start(mocker)
71+
battery_pool = microgrid.battery_pool()
7272
logical_meter = microgrid.logical_meter()
7373

74-
battery_power_recv = logical_meter.battery_power.new_receiver()
74+
battery_power_recv = battery_pool.power.new_receiver()
7575
pv_power_recv = logical_meter.pv_power.new_receiver()
76-
engine = (logical_meter.pv_power + logical_meter.battery_power).build(
77-
"inv_power"
78-
)
76+
engine = (logical_meter.pv_power + battery_pool.power).build("inv_power")
7977
inv_calc_recv = engine.new_receiver()
8078

8179
count = 0
@@ -98,13 +96,12 @@ async def test_formula_composition_missing_bat(self, mocker: MockerFixture) -> N
9896
mockgrid = MockMicrogrid(grid_side_meter=False)
9997
mockgrid.add_solar_inverters(2)
10098
await mockgrid.start(mocker)
99+
battery_pool = microgrid.battery_pool()
101100
logical_meter = microgrid.logical_meter()
102101

103-
battery_power_recv = logical_meter.battery_power.new_receiver()
102+
battery_power_recv = battery_pool.power.new_receiver()
104103
pv_power_recv = logical_meter.pv_power.new_receiver()
105-
engine = (logical_meter.pv_power + logical_meter.battery_power).build(
106-
"inv_power"
107-
)
104+
engine = (logical_meter.pv_power + battery_pool.power).build("inv_power")
108105
inv_calc_recv = engine.new_receiver()
109106

110107
count = 0

tests/timeseries/_formula_engine/utils.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919

2020
def get_resampled_stream( # pylint: disable=too-many-arguments
21+
namespace: str,
2122
comp_id: int,
2223
metric_id: ComponentMetricId,
2324
) -> Receiver[Sample]:
@@ -27,7 +28,7 @@ def get_resampled_stream( # pylint: disable=too-many-arguments
2728

2829
# pylint: disable=protected-access
2930
builder = ResampledFormulaBuilder(
30-
_data_pipeline._get().logical_meter()._namespace,
31+
namespace,
3132
"",
3233
_data_pipeline._get()._channel_registry,
3334
_data_pipeline._get()._resampling_request_sender(),

tests/timeseries/test_ev_charger_pool.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ async def test_ev_power( # pylint: disable=too-many-locals
9393
ev_pool = microgrid.ev_charger_pool()
9494

9595
main_meter_recv = get_resampled_stream(
96+
logical_meter._namespace, # pylint: disable=protected-access
9697
mockgrid.main_meter_id,
9798
ComponentMetricId.ACTIVE_POWER,
9899
)

0 commit comments

Comments
 (0)