Skip to content

Commit dcf7b0d

Browse files
committed
Properly handle PV power requests when no or only some meters exist.
So far we only had configurations like this: Meter -> Inverter -> PV. However the scenario with Inverter -> PV is also possible and now handled correctly. Signed-off-by: Mathias L. Baumann <[email protected]>
1 parent 786569d commit dcf7b0d

File tree

5 files changed

+77
-53
lines changed

5 files changed

+77
-53
lines changed

RELEASE_NOTES.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,7 @@
1515
## Bug Fixes
1616

1717
- Fixes a bug in the ring buffer updating the end timestamp of gaps when they are outdated.
18+
- Properly handles PV configurations with no or only some meters before the PV
19+
component.
20+
So far we only had configurations like this: Meter -> Inverter -> PV. However
21+
the scenario with Inverter -> PV is also possible and now handled correctly.

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

Lines changed: 28 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,10 @@
99
from collections import abc
1010

1111
from ....microgrid import connection_manager
12-
from ....microgrid.component import ComponentCategory, ComponentMetricId, InverterType
12+
from ....microgrid.component import ComponentCategory, ComponentMetricId
1313
from ..._quantities import Power
1414
from .._formula_engine import FormulaEngine
15-
from ._formula_generator import (
16-
NON_EXISTING_COMPONENT_ID,
17-
FormulaGenerationError,
18-
FormulaGenerator,
19-
FormulaType,
20-
)
15+
from ._formula_generator import NON_EXISTING_COMPONENT_ID, FormulaGenerator, FormulaType
2116

2217
_logger = logging.getLogger(__name__)
2318

@@ -38,8 +33,8 @@ def generate(self) -> FormulaEngine[Power]:
3833
"pv-power", ComponentMetricId.ACTIVE_POWER, Power.from_watts
3934
)
4035

41-
pv_meters = self._get_pv_meters()
42-
if not pv_meters:
36+
pv_components = self._get_pv_power_components()
37+
if not pv_components:
4338
_logger.warning(
4439
"Unable to find any PV inverters in the component graph. "
4540
"Subscribing to the resampling actor with a non-existing "
@@ -55,7 +50,7 @@ def generate(self) -> FormulaEngine[Power]:
5550

5651
builder.push_oper("(")
5752
builder.push_oper("(")
58-
for idx, comp_id in enumerate(pv_meters):
53+
for idx, comp_id in enumerate(pv_components):
5954
if idx > 0:
6055
builder.push_oper("+")
6156

@@ -71,39 +66,30 @@ def generate(self) -> FormulaEngine[Power]:
7166

7267
return builder.build()
7368

74-
def _get_pv_meters(self) -> abc.Set[int]:
75-
component_graph = connection_manager.get().component_graph
69+
def _get_pv_power_components(self) -> abc.Set[int]:
70+
"""Get the component ids of the PV inverters or meters in the component graph.
7671
77-
pv_inverters = list(
78-
comp
79-
for comp in component_graph.components()
80-
if comp.category == ComponentCategory.INVERTER
81-
and comp.type == InverterType.SOLAR
82-
)
83-
pv_meters: set[int] = set()
72+
Returns:
73+
A set of component ids of the PV inverters or meters in the component graph.
8474
85-
if not pv_inverters:
86-
return pv_meters
75+
Raises:
76+
RuntimeError: if the grid component has no PV inverters or meters as successors.
77+
"""
78+
component_graph = connection_manager.get().component_graph
79+
grid_successors = self._get_grid_component_successors()
8780

88-
for pv_inverter in pv_inverters:
89-
predecessors = component_graph.predecessors(pv_inverter.component_id)
90-
if len(predecessors) != 1:
91-
raise FormulaGenerationError(
92-
"Expected exactly one predecessor for PV inverter "
93-
f"{pv_inverter.component_id}, but found {len(predecessors)}."
81+
if len(grid_successors) == 1:
82+
successor = next(iter(grid_successors))
83+
if successor.category != ComponentCategory.METER:
84+
raise RuntimeError(
85+
"Only grid successor in the component graph is not a meter."
9486
)
95-
meter = next(iter(predecessors))
96-
if meter.category != ComponentCategory.METER:
97-
raise FormulaGenerationError(
98-
f"Expected predecessor of PV inverter {pv_inverter.component_id} "
99-
f"to be a meter, but found {meter.category}."
100-
)
101-
meter_successors = component_graph.successors(meter.component_id)
102-
if len(meter_successors) != 1:
103-
raise FormulaGenerationError(
104-
f"Expected exactly one successor for meter {meter.component_id}"
105-
f", connected to PV inverter {pv_inverter.component_id}"
106-
f", but found {len(meter_successors)}."
107-
)
108-
pv_meters.add(meter.component_id)
109-
return pv_meters
87+
grid_successors = component_graph.successors(successor.component_id)
88+
89+
pv_meters_or_inverters: set[int] = set()
90+
91+
for successor in grid_successors:
92+
if component_graph.is_pv_chain(successor):
93+
pv_meters_or_inverters.add(successor.component_id)
94+
95+
return pv_meters_or_inverters

tests/timeseries/mock_microgrid.py

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -295,26 +295,20 @@ def add_batteries(self, count: int, no_meter: bool = False) -> None:
295295
self._connections.add(Connection(meter_id, inv_id))
296296
self._connections.add(Connection(inv_id, bat_id))
297297

298-
def add_solar_inverters(self, count: int) -> None:
298+
def add_solar_inverters(self, count: int, no_meter: bool = False) -> None:
299299
"""Add pv inverters and connected pv meters to the microgrid.
300300
301301
Args:
302302
count: number of inverters to add to the microgrid.
303+
no_meter: if True, do not add a meter for each inverter.
303304
"""
304305
for _ in range(count):
305306
meter_id = self._id_increment * 10 + self.meter_id_suffix
306307
inv_id = self._id_increment * 10 + self.inverter_id_suffix
307308
self._id_increment += 1
308309

309-
self.meter_ids.append(meter_id)
310310
self.pv_inverter_ids.append(inv_id)
311311

312-
self._components.add(
313-
Component(
314-
meter_id,
315-
ComponentCategory.METER,
316-
)
317-
)
318312
self._components.add(
319313
Component(
320314
inv_id,
@@ -323,9 +317,20 @@ def add_solar_inverters(self, count: int) -> None:
323317
)
324318
)
325319
self._start_inverter_streaming(inv_id)
326-
self._start_meter_streaming(meter_id)
327-
self._connections.add(Connection(self._connect_to, meter_id))
328-
self._connections.add(Connection(meter_id, inv_id))
320+
321+
if no_meter:
322+
self._connections.add(Connection(self._connect_to, inv_id))
323+
else:
324+
self.meter_ids.append(meter_id)
325+
self._components.add(
326+
Component(
327+
meter_id,
328+
ComponentCategory.METER,
329+
)
330+
)
331+
self._start_meter_streaming(meter_id)
332+
self._connections.add(Connection(self._connect_to, meter_id))
333+
self._connections.add(Connection(meter_id, inv_id))
329334

330335
def add_ev_chargers(self, count: int) -> None:
331336
"""Add EV Chargers to the microgrid.

tests/timeseries/mock_resampler.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,13 @@ async def send_meter_power(self, values: list[float | None]) -> None:
145145
sample = Sample(self._next_ts, None if not value else Quantity(value))
146146
await chan.send(sample)
147147

148+
async def send_pv_inverter_power(self, values: list[float | None]) -> None:
149+
"""Send the given values as resampler output for PV Inverter power."""
150+
assert len(values) == len(self._pv_inverter_power_senders)
151+
for chan, value in zip(self._pv_inverter_power_senders, values):
152+
sample = Sample(self._next_ts, None if not value else Quantity(value))
153+
await chan.send(sample)
154+
148155
async def send_evc_power(self, values: list[float | None]) -> None:
149156
"""Send the given values as resampler output for EV Charger power."""
150157
assert len(values) == len(self._ev_power_senders)

tests/timeseries/test_logical_meter.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,28 @@ async def test_pv_power(self, mocker: MockerFixture) -> None:
191191
await pv_consumption_power_receiver.receive()
192192
).value == Power.from_watts(0.0)
193193

194+
async def test_pv_power_no_meter(self, mocker: MockerFixture) -> None:
195+
"""Test the pv power formula."""
196+
mockgrid = MockMicrogrid(grid_side_meter=False)
197+
mockgrid.add_solar_inverters(2, no_meter=True)
198+
await mockgrid.start(mocker)
199+
200+
logical_meter = microgrid.logical_meter()
201+
pv_power_receiver = logical_meter.pv_power.new_receiver()
202+
pv_production_power_receiver = logical_meter.pv_production_power.new_receiver()
203+
pv_consumption_power_receiver = (
204+
logical_meter.pv_consumption_power.new_receiver()
205+
)
206+
207+
await mockgrid.mock_resampler.send_pv_inverter_power([-1.0, -2.0])
208+
assert (await pv_power_receiver.receive()).value == Power.from_watts(-3.0)
209+
assert (await pv_production_power_receiver.receive()).value == Power.from_watts(
210+
3.0
211+
)
212+
assert (
213+
await pv_consumption_power_receiver.receive()
214+
).value == Power.from_watts(0.0)
215+
194216
async def test_consumer_power_grid_meter(self, mocker: MockerFixture) -> None:
195217
"""Test the consumer power formula with a grid meter."""
196218
mockgrid = MockMicrogrid(grid_side_meter=True)

0 commit comments

Comments
 (0)