Skip to content

Commit bb446f5

Browse files
authored
Add the set_reactive_power method (#99)
2 parents fa467e7 + c94c310 commit bb446f5

File tree

4 files changed

+82
-3
lines changed

4 files changed

+82
-3
lines changed

RELEASE_NOTES.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Frequenz Microgrid API Client Release Notes
22

3-
## Bug Fixes
3+
## New Features
44

5-
- Fix a bug where SSL was enabled by default. It is now disabled by default as in previous versions.
5+
- The client now supports setting reactive power for components through the new `set_reactive_power` method.

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,9 @@ dev-mkdocs = [
7373
]
7474
dev-mypy = [
7575
"mypy == 1.11.2",
76+
"grpc-stubs == 1.53.0.5",
7677
"types-Markdown == 3.7.0.20240822",
78+
"types-protobuf == 5.28.3.20241030",
7779
# For checking the noxfile, docs/ script, and tests
7880
"frequenz-client-microgrid[dev-mkdocs,dev-noxfile,dev-pytest]",
7981
]

src/frequenz/client/microgrid/_client.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -441,7 +441,41 @@ async def set_power(self, component_id: int, power_w: float) -> None:
441441
grpc_error=grpc_error,
442442
) from grpc_error
443443

444-
async def set_bounds(
444+
async def set_reactive_power( # noqa: DOC502 (raises ApiClientError indirectly)
445+
self, component_id: int, reactive_power_var: float
446+
) -> None:
447+
"""Send request to the Microgrid to set reactive power for component.
448+
449+
Negative values are for inductive (lagging) power , and positive values are for
450+
capacitive (leading) power.
451+
452+
Args:
453+
component_id: id of the component to set power.
454+
reactive_power_var: reactive power to set for the component.
455+
456+
Raises:
457+
ApiClientError: If the are any errors communicating with the Microgrid API,
458+
most likely a subclass of
459+
[GrpcError][frequenz.client.microgrid.GrpcError].
460+
"""
461+
try:
462+
await cast(
463+
Awaitable[Empty],
464+
self.api.SetPowerReactive(
465+
microgrid_pb2.SetPowerReactiveParam(
466+
component_id=component_id, power=reactive_power_var
467+
),
468+
timeout=int(DEFAULT_GRPC_CALL_TIMEOUT),
469+
),
470+
)
471+
except grpc.aio.AioRpcError as grpc_error:
472+
raise ApiClientError.from_grpc_error(
473+
server_url=self._server_url,
474+
operation="SetPowerReactive",
475+
grpc_error=grpc_error,
476+
) from grpc_error
477+
478+
async def set_bounds( # noqa: DOC503 (raises ApiClientError indirectly)
445479
self,
446480
component_id: int,
447481
lower: float,

tests/test_client.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ def __init__(self, *, retry_strategy: retry.Strategy | None = None) -> None:
4242
mock_stub.ListComponents = mock.AsyncMock("ListComponents")
4343
mock_stub.ListConnections = mock.AsyncMock("ListConnections")
4444
mock_stub.SetPowerActive = mock.AsyncMock("SetPowerActive")
45+
mock_stub.SetPowerReactive = mock.AsyncMock("SetPowerReactive")
4546
mock_stub.AddInclusionBounds = mock.AsyncMock("AddInclusionBounds")
4647
mock_stub.StreamComponentData = mock.Mock("StreamComponentData")
4748
super().__init__("grpc://mock_host:1234", retry_strategy=retry_strategy)
@@ -607,6 +608,48 @@ async def test_set_power_grpc_error() -> None:
607608
await client.set_power(component_id=83, power_w=100.0)
608609

609610

611+
@pytest.mark.parametrize(
612+
"reactive_power_var",
613+
[0, 0.0, 12, -75, 0.1, -0.0001, 134.0],
614+
)
615+
async def test_set_reactive_power_ok(
616+
reactive_power_var: float, meter83: microgrid_pb2.Component
617+
) -> None:
618+
"""Test if charge is able to charge component."""
619+
client = _TestClient()
620+
client.mock_stub.ListComponents.return_value = microgrid_pb2.ComponentList(
621+
components=[meter83]
622+
)
623+
624+
await client.set_reactive_power(
625+
component_id=83, reactive_power_var=reactive_power_var
626+
)
627+
client.mock_stub.SetPowerReactive.assert_called_once()
628+
call_args = client.mock_stub.SetPowerReactive.call_args[0]
629+
assert call_args[0] == microgrid_pb2.SetPowerReactiveParam(
630+
component_id=83, power=reactive_power_var
631+
)
632+
633+
634+
async def test_set_reactive_power_grpc_error() -> None:
635+
"""Test set_power() raises ApiClientError when the gRPC call fails."""
636+
client = _TestClient()
637+
client.mock_stub.SetPowerReactive.side_effect = grpc.aio.AioRpcError(
638+
mock.MagicMock(name="mock_status"),
639+
mock.MagicMock(name="mock_initial_metadata"),
640+
mock.MagicMock(name="mock_trailing_metadata"),
641+
"fake grpc details",
642+
"fake grpc debug_error_string",
643+
)
644+
with pytest.raises(
645+
ApiClientError,
646+
match=r"Failed calling 'SetPowerReactive' on 'grpc://mock_host:1234': .* "
647+
r"<status=<MagicMock name='mock_status\.name' id='.*'>>: fake grpc details "
648+
r"\(fake grpc debug_error_string\)",
649+
):
650+
await client.set_reactive_power(component_id=83, reactive_power_var=100.0)
651+
652+
610653
@pytest.mark.parametrize(
611654
"bounds",
612655
[

0 commit comments

Comments
 (0)