diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index d46ff52b..e9c490a8 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,9 +1,5 @@ # Frequenz Microgrid API Client Release Notes -## Summary - - - ## Upgrading - `ApiClient`: @@ -18,7 +14,4 @@ ## New Features - The client now inherits from `frequenz.client.base.BaseApiClient`, so it provides a few new features, like `disconnect()`ing or using it as a context manager. Please refer to the [`BaseApiClient` documentation](https://frequenz-floss.github.io/frequenz-client-base-python/latest/reference/frequenz/client/base/client/#frequenz.client.base.client.BaseApiClient) for more information on these features. - -## Bug Fixes - - +- The client now supports setting reactive power for components through the new `set_reactive_power` method. diff --git a/src/frequenz/client/microgrid/_client.py b/src/frequenz/client/microgrid/_client.py index 8979ed5a..f6dd4973 100644 --- a/src/frequenz/client/microgrid/_client.py +++ b/src/frequenz/client/microgrid/_client.py @@ -432,6 +432,34 @@ async def set_power( # noqa: DOC502 (raises ApiClientError indirectly) method_name="SetPowerActive", ) + async def set_reactive_power( # noqa: DOC502 (raises ApiClientError indirectly) + self, component_id: int, reactive_power_var: float + ) -> None: + """Send request to the Microgrid to set reactive power for component. + + Negative values are for inductive (lagging) power , and positive values are for + capacitive (leading) power. + + Args: + component_id: id of the component to set power. + reactive_power_var: reactive power to set for the component. + + Raises: + ApiClientError: If the are any errors communicating with the Microgrid API, + most likely a subclass of + [GrpcError][frequenz.client.microgrid.GrpcError]. + """ + await client.call_stub_method( + self, + lambda: self.stub.SetPowerReactive( + microgrid_pb2.SetPowerReactiveParam( + component_id=component_id, power=reactive_power_var + ), + timeout=int(DEFAULT_GRPC_CALL_TIMEOUT), + ), + method_name="SetPowerReactive", + ) + async def set_bounds( # noqa: DOC503 (raises ApiClientError indirectly) self, component_id: int, diff --git a/tests/test_client.py b/tests/test_client.py index 6ca18475..86090fe7 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -42,6 +42,7 @@ def __init__(self, *, retry_strategy: retry.Strategy | None = None) -> None: mock_stub.ListComponents = mock.AsyncMock("ListComponents") mock_stub.ListConnections = mock.AsyncMock("ListConnections") mock_stub.SetPowerActive = mock.AsyncMock("SetPowerActive") + mock_stub.SetPowerReactive = mock.AsyncMock("SetPowerReactive") mock_stub.AddInclusionBounds = mock.AsyncMock("AddInclusionBounds") mock_stub.StreamComponentData = mock.Mock("StreamComponentData") super().__init__("grpc://mock_host:1234", retry_strategy=retry_strategy) @@ -607,6 +608,48 @@ async def test_set_power_grpc_error() -> None: await client.set_power(component_id=83, power_w=100.0) +@pytest.mark.parametrize( + "reactive_power_var", + [0, 0.0, 12, -75, 0.1, -0.0001, 134.0], +) +async def test_set_reactive_power_ok( + reactive_power_var: float, meter83: microgrid_pb2.Component +) -> None: + """Test if charge is able to charge component.""" + client = _TestClient() + client.mock_stub.ListComponents.return_value = microgrid_pb2.ComponentList( + components=[meter83] + ) + + await client.set_reactive_power( + component_id=83, reactive_power_var=reactive_power_var + ) + client.mock_stub.SetPowerReactive.assert_called_once() + call_args = client.mock_stub.SetPowerReactive.call_args[0] + assert call_args[0] == microgrid_pb2.SetPowerReactiveParam( + component_id=83, power=reactive_power_var + ) + + +async def test_set_reactive_power_grpc_error() -> None: + """Test set_power() raises ApiClientError when the gRPC call fails.""" + client = _TestClient() + client.mock_stub.SetPowerReactive.side_effect = grpc.aio.AioRpcError( + mock.MagicMock(name="mock_status"), + mock.MagicMock(name="mock_initial_metadata"), + mock.MagicMock(name="mock_trailing_metadata"), + "fake grpc details", + "fake grpc debug_error_string", + ) + with pytest.raises( + ApiClientError, + match=r"Failed calling 'SetPowerReactive' on 'grpc://mock_host:1234': .* " + r">: fake grpc details " + r"\(fake grpc debug_error_string\)", + ): + await client.set_reactive_power(component_id=83, reactive_power_var=100.0) + + @pytest.mark.parametrize( "bounds", [