Skip to content

Commit ee08d95

Browse files
committed
Add flexible MockMicrogrid for testing logical meter formulas
... and update the logical meter tests to use a generated grid power formula instead. Signed-off-by: Sahas Subramanian <[email protected]>
1 parent fb61bb3 commit ee08d95

File tree

2 files changed

+333
-143
lines changed

2 files changed

+333
-143
lines changed

tests/timeseries/mock_microgrid.py

Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
# License: MIT
2+
# Copyright © 2022 Frequenz Energy-as-a-Service GmbH
3+
4+
"""A configurable mock microgrid for testing logical meter formulas."""
5+
6+
from __future__ import annotations
7+
8+
import time
9+
from typing import Iterator, Tuple
10+
11+
from frequenz.api.microgrid import microgrid_pb2
12+
from frequenz.api.microgrid.common_pb2 import AC, Metric
13+
from frequenz.api.microgrid.inverter_pb2 import Data as InverterData
14+
from frequenz.api.microgrid.inverter_pb2 import Inverter
15+
from frequenz.api.microgrid.inverter_pb2 import Type as InverterType
16+
from frequenz.api.microgrid.meter_pb2 import Data as MeterData
17+
from frequenz.api.microgrid.meter_pb2 import Meter
18+
from frequenz.api.microgrid.microgrid_pb2 import ComponentData, ComponentIdParam
19+
from frequenz.channels import Broadcast, Sender
20+
from google.protobuf.timestamp_pb2 import Timestamp # pylint: disable=no-name-in-module
21+
from grpc.aio import grpc
22+
from pytest_mock import MockerFixture
23+
24+
from frequenz.sdk import microgrid
25+
from frequenz.sdk.actor import (
26+
ChannelRegistry,
27+
ComponentMetricRequest,
28+
ComponentMetricsResamplingActor,
29+
DataSourcingActor,
30+
)
31+
from tests.microgrid import mock_api
32+
33+
34+
class MockMicrogrid:
35+
"""Setup a MockApi instance with multiple component layouts for tests."""
36+
37+
grid_id = 1
38+
main_meter_id = 4
39+
meter_id_suffix = 7
40+
inverter_id_suffix = 8
41+
battery_id_suffix = 9
42+
43+
def __init__(
44+
self,
45+
server: mock_api.MockGrpcServer,
46+
servicer: mock_api.MockMicrogridServicer,
47+
grid_side_meter: bool,
48+
):
49+
"""Create a new instance.
50+
51+
NOTE: Use the `MockMicrogrid.new()` method to create an instance instead.
52+
"""
53+
self._server = server
54+
self._servicer = servicer
55+
self._id_increment = 0
56+
self._grid_side_meter = grid_side_meter
57+
58+
self._connect_to = self.grid_id
59+
if self._grid_side_meter:
60+
self._connect_to = self.main_meter_id
61+
62+
self.battery_inverter_ids: list[int] = []
63+
self.pv_inverter_ids: list[int] = []
64+
self.battery_ids: list[int] = []
65+
self.meter_ids: list[int] = [4]
66+
67+
async def start(self) -> Tuple[Sender[ComponentMetricRequest], ChannelRegistry]:
68+
"""Start the MockServer, and the data source and resampling actors.
69+
70+
Returns:
71+
A sender to send requests to the Resampling actor, and a corresponding
72+
channel registry.
73+
"""
74+
await self._server.start()
75+
return await self._init_client_and_actors()
76+
77+
@classmethod
78+
async def new(
79+
cls, mocker: MockerFixture, *, grid_side_meter: bool = True
80+
) -> MockMicrogrid:
81+
"""Create a new MockMicrogrid instance."""
82+
server, servicer = await MockMicrogrid._setup_service(mocker)
83+
return MockMicrogrid(server, servicer, grid_side_meter)
84+
85+
@classmethod
86+
async def _setup_service(
87+
cls, mocker: MockerFixture
88+
) -> Tuple[mock_api.MockGrpcServer, mock_api.MockMicrogridServicer]:
89+
"""Initialize a mock microgrid api for a test."""
90+
microgrid._microgrid._MICROGRID = None # pylint: disable=protected-access
91+
servicer = mock_api.MockMicrogridServicer()
92+
93+
# pylint: disable=unused-argument
94+
def get_component_data(
95+
request: ComponentIdParam, context: grpc.ServicerContext
96+
) -> Iterator[ComponentData]:
97+
"""Return an iterator for mock ComponentData."""
98+
# pylint: disable=stop-iteration-return
99+
100+
def meter_msg(value: float) -> ComponentData:
101+
timestamp = Timestamp()
102+
timestamp.GetCurrentTime()
103+
return ComponentData(
104+
id=request.id,
105+
ts=timestamp,
106+
meter=Meter(
107+
data=MeterData(ac=AC(power_active=Metric(value=value)))
108+
),
109+
)
110+
111+
def inverter_msg(value: float) -> ComponentData:
112+
timestamp = Timestamp()
113+
timestamp.GetCurrentTime()
114+
return ComponentData(
115+
id=request.id,
116+
ts=timestamp,
117+
inverter=Inverter(
118+
data=InverterData(ac=AC(power_active=Metric(value=value)))
119+
),
120+
)
121+
122+
if request.id % 10 == cls.inverter_id_suffix:
123+
next_msg = inverter_msg
124+
elif (
125+
request.id % 10 == cls.meter_id_suffix
126+
or request.id == cls.main_meter_id
127+
):
128+
next_msg = meter_msg
129+
else:
130+
raise RuntimeError(
131+
f"Component id {request.id} unsupported by MockMicrogrid"
132+
)
133+
for value in range(1, 10):
134+
yield next_msg(value=value + request.id)
135+
time.sleep(0.1)
136+
137+
mocker.patch.object(servicer, "GetComponentData", get_component_data)
138+
139+
server = mock_api.MockGrpcServer(servicer, port=57891)
140+
141+
servicer.add_component(
142+
1, microgrid_pb2.ComponentCategory.COMPONENT_CATEGORY_GRID
143+
)
144+
servicer.add_component(
145+
4, microgrid_pb2.ComponentCategory.COMPONENT_CATEGORY_METER
146+
)
147+
servicer.add_connection(1, 4)
148+
149+
return (server, servicer)
150+
151+
def add_batteries(self, count: int) -> None:
152+
"""Add batteries with connected inverters and meters to the microgrid.
153+
154+
Args:
155+
count: number of battery sets to add.
156+
"""
157+
for _ in range(count):
158+
meter_id = self._id_increment * 10 + 7
159+
inv_id = self._id_increment * 10 + 8
160+
bat_id = self._id_increment * 10 + 9
161+
self._id_increment += 1
162+
163+
self.meter_ids.append(meter_id)
164+
self.battery_inverter_ids.append(inv_id)
165+
self.battery_ids.append(bat_id)
166+
167+
self._servicer.add_component(
168+
meter_id,
169+
microgrid_pb2.ComponentCategory.COMPONENT_CATEGORY_METER,
170+
)
171+
self._servicer.add_component(
172+
inv_id,
173+
microgrid_pb2.ComponentCategory.COMPONENT_CATEGORY_INVERTER,
174+
InverterType.TYPE_BATTERY,
175+
)
176+
self._servicer.add_component(
177+
bat_id,
178+
microgrid_pb2.ComponentCategory.COMPONENT_CATEGORY_BATTERY,
179+
)
180+
self._servicer.add_connection(self._connect_to, meter_id)
181+
self._servicer.add_connection(meter_id, inv_id)
182+
self._servicer.add_connection(inv_id, bat_id)
183+
184+
def add_solar_inverters(self, count: int) -> None:
185+
"""Add pv inverters and connected pv meters to the microgrid.
186+
187+
Args:
188+
count: number of inverters to add to the microgrid.
189+
"""
190+
for _ in range(count):
191+
meter_id = self._id_increment * 10 + 7
192+
inv_id = self._id_increment * 10 + 8
193+
self._id_increment += 1
194+
195+
self.meter_ids.append(meter_id)
196+
self.pv_inverter_ids.append(inv_id)
197+
198+
self._servicer.add_component(
199+
meter_id,
200+
microgrid_pb2.ComponentCategory.COMPONENT_CATEGORY_METER,
201+
)
202+
self._servicer.add_component(
203+
inv_id,
204+
microgrid_pb2.ComponentCategory.COMPONENT_CATEGORY_INVERTER,
205+
InverterType.TYPE_SOLAR,
206+
)
207+
self._servicer.add_connection(self._connect_to, meter_id)
208+
self._servicer.add_connection(meter_id, inv_id)
209+
210+
async def _init_client_and_actors(
211+
self,
212+
) -> Tuple[Sender[ComponentMetricRequest], ChannelRegistry]:
213+
await microgrid.initialize("[::1]", 57891)
214+
215+
channel_registry = ChannelRegistry(name="Microgrid Channel Registry")
216+
217+
data_source_request_channel = Broadcast[ComponentMetricRequest](
218+
"Data Source Request Channel"
219+
)
220+
data_source_request_sender = data_source_request_channel.new_sender()
221+
data_source_request_receiver = data_source_request_channel.new_receiver()
222+
223+
resampling_actor_request_channel = Broadcast[ComponentMetricRequest](
224+
"Resampling Actor Request Channel"
225+
)
226+
resampling_actor_request_sender = resampling_actor_request_channel.new_sender()
227+
resampling_actor_request_receiver = (
228+
resampling_actor_request_channel.new_receiver()
229+
)
230+
231+
DataSourcingActor(
232+
request_receiver=data_source_request_receiver, registry=channel_registry
233+
)
234+
235+
ComponentMetricsResamplingActor(
236+
channel_registry=channel_registry,
237+
data_sourcing_request_sender=data_source_request_sender,
238+
resampling_request_receiver=resampling_actor_request_receiver,
239+
resampling_period_s=0.1,
240+
)
241+
242+
return (resampling_actor_request_sender, channel_registry)
243+
244+
async def cleanup(self) -> None:
245+
"""Clean up after a test."""
246+
await self._server.stop(0.1)
247+
microgrid._microgrid._MICROGRID = None # pylint: disable=protected-access

0 commit comments

Comments
 (0)