From 7b4a9fb5398e3ff320848156c9c77fcdd4d72992 Mon Sep 17 00:00:00 2001 From: Tiyash Basu Date: Mon, 28 Aug 2023 13:33:22 +0200 Subject: [PATCH 1/7] Implement a hash method for `Component` Objects of the `Component` type are used in `ComponentGraph` in sets. The implicit hash method works there for now, but adding new members to the `Component` class may make it unhashable in the future, if the added members themselves are unhashable. Implementing an explicit hash method here removes this burden from from future member types. Component IDs are supposed to be unique in a microgrid, therefore hashing a `Component` object just based on its component ID is enough. Signed-off-by: Tiyash Basu --- src/frequenz/sdk/microgrid/component/_component.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/frequenz/sdk/microgrid/component/_component.py b/src/frequenz/sdk/microgrid/component/_component.py index 3a3c24673..6542d63e5 100644 --- a/src/frequenz/sdk/microgrid/component/_component.py +++ b/src/frequenz/sdk/microgrid/component/_component.py @@ -125,6 +125,14 @@ def is_valid(self) -> bool: self.component_id > 0 and any(t == self.category for t in ComponentCategory) ) or (self.component_id == 0 and self.category == ComponentCategory.GRID) + def __hash__(self) -> int: + """Compute a hash of this instance, obtained by hashing the `component_id` field. + + Returns: + Hash of this instance. + """ + return hash(self.component_id) + class ComponentMetricId(Enum): """An enum representing the various metrics available in the microgrid.""" From a17939d4f1e19c487cc0d19939c93c1f1e29ed45 Mon Sep 17 00:00:00 2001 From: Tiyash Basu Date: Mon, 28 Aug 2023 18:28:19 +0200 Subject: [PATCH 2/7] Refactor optional type declarations in _component.py This commit refactors the declarations of types in format `Optional[X]` to `X | None`. Signed-off-by: Tiyash Basu --- src/frequenz/sdk/microgrid/component/_component.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/frequenz/sdk/microgrid/component/_component.py b/src/frequenz/sdk/microgrid/component/_component.py index 6542d63e5..3553f4c95 100644 --- a/src/frequenz/sdk/microgrid/component/_component.py +++ b/src/frequenz/sdk/microgrid/component/_component.py @@ -7,7 +7,6 @@ from dataclasses import dataclass from enum import Enum -from typing import Optional import frequenz.api.common.components_pb2 as components_pb import frequenz.api.microgrid.inverter_pb2 as inverter_pb @@ -32,7 +31,7 @@ class InverterType(ComponentType): def _component_type_from_protobuf( component_category: components_pb.ComponentCategory.ValueType, component_metadata: inverter_pb.Metadata, -) -> Optional[ComponentType]: +) -> ComponentType | None: """Convert a protobuf InverterType message to Component enum. For internal-only use by the `microgrid` package. @@ -112,7 +111,7 @@ class Component: component_id: int category: ComponentCategory - type: Optional[ComponentType] = None + type: ComponentType | None = None def is_valid(self) -> bool: """Check if this instance contains valid data. From 2e71987ec4b1ef253738b4c008dcb6dabe9c1412 Mon Sep 17 00:00:00 2001 From: Tiyash Basu Date: Thu, 24 Aug 2023 15:21:21 +0200 Subject: [PATCH 3/7] Add metadata to Components This commit introduces metadata to Components by incorporating GridMetadata, which now includes the maximum grid current, specified in Amperes. Signed-off-by: Tiyash Basu --- src/frequenz/sdk/microgrid/client/_client.py | 2 ++ .../sdk/microgrid/component/__init__.py | 11 +++++++- .../sdk/microgrid/component/_component.py | 25 ++++++++++++++++++ tests/microgrid/test_client.py | 9 ++++++- tests/microgrid/test_graph.py | 26 ++++++++++++++++--- 5 files changed, 67 insertions(+), 6 deletions(-) diff --git a/src/frequenz/sdk/microgrid/client/_client.py b/src/frequenz/sdk/microgrid/client/_client.py index e581781b4..534efcf43 100644 --- a/src/frequenz/sdk/microgrid/client/_client.py +++ b/src/frequenz/sdk/microgrid/client/_client.py @@ -36,6 +36,7 @@ ) from ..component._component import ( _component_category_from_protobuf, + _component_metadata_from_protobuf, _component_type_from_protobuf, ) from ._connection import Connection @@ -254,6 +255,7 @@ async def components(self) -> Iterable[Component]: c.id, _component_category_from_protobuf(c.category), _component_type_from_protobuf(c.category, c.inverter), + _component_metadata_from_protobuf(c.category, c.grid), ), components_only, ) diff --git a/src/frequenz/sdk/microgrid/component/__init__.py b/src/frequenz/sdk/microgrid/component/__init__.py index 9e6d74606..f1c58851d 100644 --- a/src/frequenz/sdk/microgrid/component/__init__.py +++ b/src/frequenz/sdk/microgrid/component/__init__.py @@ -6,7 +6,14 @@ This package provides classes to operate con microgrid components. """ -from ._component import Component, ComponentCategory, ComponentMetricId, InverterType +from ._component import ( + Component, + ComponentCategory, + ComponentMetadata, + ComponentMetricId, + GridMetadata, + InverterType, +) from ._component_data import ( BatteryData, ComponentData, @@ -21,10 +28,12 @@ "Component", "ComponentData", "ComponentCategory", + "ComponentMetadata", "ComponentMetricId", "EVChargerCableState", "EVChargerComponentState", "EVChargerData", + "GridMetadata", "InverterData", "InverterType", "MeterData", diff --git a/src/frequenz/sdk/microgrid/component/_component.py b/src/frequenz/sdk/microgrid/component/_component.py index 3553f4c95..de2b01b54 100644 --- a/src/frequenz/sdk/microgrid/component/_component.py +++ b/src/frequenz/sdk/microgrid/component/_component.py @@ -9,6 +9,7 @@ from enum import Enum import frequenz.api.common.components_pb2 as components_pb +import frequenz.api.microgrid.grid_pb2 as grid_pb import frequenz.api.microgrid.inverter_pb2 as inverter_pb @@ -105,6 +106,29 @@ def _component_category_from_protobuf( return ComponentCategory(component_category) +@dataclass(frozen=True) +class ComponentMetadata: + """Base class for component metadata classes.""" + + +@dataclass(frozen=True) +class GridMetadata(ComponentMetadata): + """Metadata for a grid connection point.""" + + max_current: float + """maximum current rating of the grid connection point in amps.""" + + +def _component_metadata_from_protobuf( + component_category: components_pb.ComponentCategory.ValueType, + component_metadata: grid_pb.Metadata, +) -> GridMetadata | None: + if component_category == components_pb.ComponentCategory.COMPONENT_CATEGORY_GRID: + return GridMetadata(float(component_metadata.rated_fuse_current)) + + return None + + @dataclass(frozen=True) class Component: """Metadata for a single microgrid component.""" @@ -112,6 +136,7 @@ class Component: component_id: int category: ComponentCategory type: ComponentType | None = None + metadata: ComponentMetadata | None = None def is_valid(self) -> bool: """Check if this instance contains valid data. diff --git a/tests/microgrid/test_client.py b/tests/microgrid/test_client.py index 853d183ad..f5b4b3032 100644 --- a/tests/microgrid/test_client.py +++ b/tests/microgrid/test_client.py @@ -21,6 +21,7 @@ Component, ComponentCategory, EVChargerData, + GridMetadata, InverterData, InverterType, MeterData, @@ -117,9 +118,15 @@ async def test_components(self) -> None: (999, components_pb.ComponentCategory.COMPONENT_CATEGORY_SENSOR), ] ) + assert set(await microgrid.components()) == { Component(100, ComponentCategory.NONE), - Component(101, ComponentCategory.GRID), + Component( + 101, + ComponentCategory.GRID, + None, + GridMetadata(max_current=0.0), + ), Component(104, ComponentCategory.METER), Component(105, ComponentCategory.INVERTER, InverterType.NONE), Component(106, ComponentCategory.BATTERY), diff --git a/tests/microgrid/test_graph.py b/tests/microgrid/test_graph.py index a58bdb769..740d4abd0 100644 --- a/tests/microgrid/test_graph.py +++ b/tests/microgrid/test_graph.py @@ -18,7 +18,12 @@ import frequenz.sdk.microgrid._graph as gr from frequenz.sdk.microgrid.client import Connection, MicrogridGrpcClient -from frequenz.sdk.microgrid.component import Component, ComponentCategory, InverterType +from frequenz.sdk.microgrid.component import ( + Component, + ComponentCategory, + GridMetadata, + InverterType, +) from .mock_api import MockGrpcServer, MockMicrogridServicer @@ -816,8 +821,16 @@ async def test_refresh_from_api(self) -> None: servicer.set_connections([(101, 111), (111, 131)]) await graph.refresh_from_api(client) + # Note: we need to add GriMetadata as a dict here, because that's what + # the ComponentGraph does too, and we need to be able to compare the + # two graphs. expected = { - Component(101, ComponentCategory.GRID), + Component( + 101, + ComponentCategory.GRID, + None, + asdict(GridMetadata(max_current=0.0)), # type: ignore + ), Component(111, ComponentCategory.METER), Component(131, ComponentCategory.EV_CHARGER), } @@ -843,7 +856,12 @@ async def test_refresh_from_api(self) -> None: servicer.set_connections([(707, 717), (717, 727), (727, 737), (717, 747)]) await graph.refresh_from_api(client) expected = { - Component(707, ComponentCategory.GRID), + Component( + 707, + ComponentCategory.GRID, + None, + asdict(GridMetadata(max_current=0.0)), # type: ignore + ), Component(717, ComponentCategory.METER), Component(727, ComponentCategory.INVERTER, InverterType.NONE), Component(737, ComponentCategory.BATTERY), @@ -1146,7 +1164,7 @@ def test__validate_grid_endpoint(self) -> None: gr.InvalidGraphError, match=r"Grid endpoint 1 has graph predecessors: \[Component" r"\(component_id=99, category=, " - r"type=None\)\]", + r"type=None, metadata=None\)\]", ) as _err_predecessors: graph._validate_grid_endpoint() From 94ed0830fe685f4cc73de5d2c0f69f8104403b27 Mon Sep 17 00:00:00 2001 From: Tiyash Basu Date: Thu, 24 Aug 2023 17:59:56 +0200 Subject: [PATCH 4/7] Allow setting grid metadata in mocks This commit allows tests to specify the `rated_fuse_current` member in the metadata objects of grid connection points. The test data supplied using this mechanism can be used in downstream validations. Signed-off-by: Tiyash Basu --- tests/microgrid/mock_api.py | 15 +++++++++++++++ tests/microgrid/test_client.py | 11 +++++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/tests/microgrid/mock_api.py b/tests/microgrid/mock_api.py index 37d5bb86f..3e07b3824 100644 --- a/tests/microgrid/mock_api.py +++ b/tests/microgrid/mock_api.py @@ -30,6 +30,7 @@ from frequenz.api.microgrid.battery_pb2 import Battery from frequenz.api.microgrid.battery_pb2 import Data as BatteryData from frequenz.api.microgrid.ev_charger_pb2 import EvCharger +from frequenz.api.microgrid.grid_pb2 import Metadata as GridMetadata from frequenz.api.microgrid.inverter_pb2 import Inverter from frequenz.api.microgrid.inverter_pb2 import Metadata as InverterMetadata from frequenz.api.microgrid.meter_pb2 import Data as MeterData @@ -57,6 +58,8 @@ from google.protobuf.timestamp_pb2 import Timestamp from google.protobuf.wrappers_pb2 import BoolValue +from frequenz.sdk.timeseries import Current + class MockMicrogridServicer( # pylint: disable=too-many-public-methods MicrogridServicer @@ -89,6 +92,7 @@ def add_component( self, component_id: int, component_category: ComponentCategory.V, + max_current: Optional[Current] = None, inverter_type: InverterType.V = InverterType.INVERTER_TYPE_UNSPECIFIED, ) -> None: """Add a component to the mock service.""" @@ -100,6 +104,17 @@ def add_component( inverter=InverterMetadata(type=inverter_type), ) ) + elif ( + component_category == ComponentCategory.COMPONENT_CATEGORY_GRID + and max_current is not None + ): + self._components.append( + Component( + id=component_id, + category=component_category, + grid=GridMetadata(rated_fuse_current=int(max_current.as_amperes())), + ) + ) else: self._components.append( Component(id=component_id, category=component_category) diff --git a/tests/microgrid/test_client.py b/tests/microgrid/test_client.py index f5b4b3032..7dd98d45d 100644 --- a/tests/microgrid/test_client.py +++ b/tests/microgrid/test_client.py @@ -26,6 +26,7 @@ InverterType, MeterData, ) +from frequenz.sdk.timeseries import Current from . import mock_api @@ -107,7 +108,6 @@ async def test_components(self) -> None: 100, components_pb.ComponentCategory.COMPONENT_CATEGORY_UNSPECIFIED, ), - (101, components_pb.ComponentCategory.COMPONENT_CATEGORY_GRID), (104, components_pb.ComponentCategory.COMPONENT_CATEGORY_METER), (105, components_pb.ComponentCategory.COMPONENT_CATEGORY_INVERTER), (106, components_pb.ComponentCategory.COMPONENT_CATEGORY_BATTERY), @@ -119,13 +119,19 @@ async def test_components(self) -> None: ] ) + servicer.add_component( + 101, + components_pb.ComponentCategory.COMPONENT_CATEGORY_GRID, + Current.from_amperes(123.0), + ) + assert set(await microgrid.components()) == { Component(100, ComponentCategory.NONE), Component( 101, ComponentCategory.GRID, None, - GridMetadata(max_current=0.0), + GridMetadata(max_current=123.0), ), Component(104, ComponentCategory.METER), Component(105, ComponentCategory.INVERTER, InverterType.NONE), @@ -143,6 +149,7 @@ async def test_components(self) -> None: servicer.add_component( 99, components_pb.ComponentCategory.COMPONENT_CATEGORY_INVERTER, + None, components_pb.InverterType.INVERTER_TYPE_BATTERY, ) From b57cee85384f1f3ee2173d82a6729b9e32e2cb4f Mon Sep 17 00:00:00 2001 From: Tiyash Basu Date: Thu, 24 Aug 2023 14:06:21 +0200 Subject: [PATCH 5/7] Add a module for grid connection points Grid connection points are nodes in the component graph that abstract grids in a microgrid. Such nodes have their own metadata, which include their rated fuse limits. Microgrids with zero or one grid connection points are accepted. Note that a microgrid may have zero grid connection points, if it is configured as an island. This commit adds a module exposing a representation of such grid connection points. The module is named `frequenz.sdk.microgrid.grid`. The `grid` module exposes a singleton object that represents the grid connection point. It gets initialized when `microgrid.initialize()` is called. It can be obtained by calling `microgrid.grid_connection.get()`. Signed-off-by: Tiyash Basu --- RELEASE_NOTES.md | 4 ++ src/frequenz/sdk/microgrid/__init__.py | 8 ++- src/frequenz/sdk/microgrid/grid.py | 71 ++++++++++++++++++++++++++ tests/microgrid/test_grid.py | 59 +++++++++++++++++++++ 4 files changed, 141 insertions(+), 1 deletion(-) create mode 100644 src/frequenz/sdk/microgrid/grid.py create mode 100644 tests/microgrid/test_grid.py diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 91d3f15d3..2a79c97fa 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -102,6 +102,10 @@ This release replaces the `@actor` decorator with a new `Actor` class. - `Actor`: This new class inherits from `BackgroundService` and it replaces the `@actor` decorator. +- Calling `microgrid.initialize()` now also initializes the microgrid's grid connection point as a singleton object of a newly added type `Grid`. This object can be obtained by calling `microgrid.grid.get()`. This object exposes the max current that can course through the grid connection point, which is useful for the power distribution algorithm. The max current is provided by the Microgrid API, and can be obtained by calling `microgrid.grid.get().max_current`. + + Note that a microgrid is allowed to have zero or one grid connection point. Microgrids configured as islands will have zero grid connection points, and microgrids configured as grid-connected will have one grid connection point. + ## Bug Fixes - Fixes a bug in the ring buffer updating the end timestamp of gaps when they are outdated. diff --git a/src/frequenz/sdk/microgrid/__init__.py b/src/frequenz/sdk/microgrid/__init__.py index ad303f0d3..fe6718406 100644 --- a/src/frequenz/sdk/microgrid/__init__.py +++ b/src/frequenz/sdk/microgrid/__init__.py @@ -8,7 +8,7 @@ """ from ..actor import ResamplerConfig -from . import _data_pipeline, client, component, connection_manager +from . import _data_pipeline, client, component, connection_manager, grid from ._data_pipeline import battery_pool, ev_charger_pool, logical_meter from ._graph import ComponentGraph @@ -22,6 +22,11 @@ async def initialize(host: str, port: int, resampler_config: ResamplerConfig) -> resampler_config: Configuration for the resampling actor. """ await connection_manager.initialize(host, port) + + api_client = connection_manager.get().api_client + components = await api_client.components() + grid.initialize(components) + await _data_pipeline.initialize(resampler_config) @@ -32,5 +37,6 @@ async def initialize(host: str, port: int, resampler_config: ResamplerConfig) -> "component", "battery_pool", "ev_charger_pool", + "grid", "logical_meter", ] diff --git a/src/frequenz/sdk/microgrid/grid.py b/src/frequenz/sdk/microgrid/grid.py new file mode 100644 index 000000000..6c7487daf --- /dev/null +++ b/src/frequenz/sdk/microgrid/grid.py @@ -0,0 +1,71 @@ +# License: MIT +# Copyright © 2023 Frequenz Energy-as-a-Service GmbH + +"""Grid connection point. + +This module provides the `Grid` type, which represents a grid connection point +in a microgrid. +""" + +import logging +from collections.abc import Iterable +from dataclasses import dataclass + +from .component import Component +from .component._component import ComponentCategory + + +@dataclass(frozen=True) +class Grid: + """A grid connection point.""" + + max_current: float + """The maximum current that can course through the grid connection point, in Amperes.""" + + +_GRID: Grid | None = None + + +def initialize(components: Iterable[Component]) -> None: + """Initialize the grid connection. + + Args: + components: The components in the microgrid. + + Raises: + RuntimeError: If there is more than 1 grid connection point in the + microgrid. + """ + global _GRID # pylint: disable=global-statement + + grid_connections = list( + component + for component in components + if component.category == ComponentCategory.GRID + ) + + grid_connections_count = len(grid_connections) + + if grid_connections_count == 0: + logging.info( + "No grid connection found for this microgrid. This is normal for an islanded microgrid." + ) + elif grid_connections_count > 1: + raise RuntimeError( + f"Expected at most one grid connection, got {grid_connections_count}" + ) + else: + max_current = grid_connections[0].metadata.max_current # type: ignore + _GRID = Grid(max_current) + + +def get() -> Grid | None: + """Get the grid connection. + + Note that a microgrid configured as an island will not have a grid + connection point. For such microgrids, this function will return `None`. + + Returns: + The grid connection. + """ + return _GRID diff --git a/tests/microgrid/test_grid.py b/tests/microgrid/test_grid.py new file mode 100644 index 000000000..309c92324 --- /dev/null +++ b/tests/microgrid/test_grid.py @@ -0,0 +1,59 @@ +# License: MIT +# Copyright © 2023 Frequenz Energy-as-a-Service GmbH + +""" +Tests for the `Grid` module. +""" + +from frequenz.sdk import microgrid +from frequenz.sdk.microgrid.component import Component, ComponentCategory, GridMetadata +from frequenz.sdk.microgrid.grid import Grid + + +async def test_grid() -> None: + """Test the grid connection module.""" + + # The tests here need to be in this exact sequence, because the grid connection + # is a singleton. Once it gets created, it stays in memory for the duration of + # the tests, unless we explicitly delete it. + + # validate that islands with no grid connection are accepted. + components = [ + Component(2, ComponentCategory.METER), + ] + + microgrid.grid.initialize(components) + + grid = microgrid.grid.get() + assert grid is None + + # validate that the microgrid initialization fails when there are multiple + # grid connection points. + components = [ + Component(1, ComponentCategory.GRID, None, GridMetadata(123.0)), + Component(2, ComponentCategory.GRID, None, GridMetadata(345.0)), + Component(3, ComponentCategory.METER), + ] + + try: + microgrid.grid.initialize(components) + assert False, "Expected microgrid.grid.initialize to raise a RuntimeError." + except RuntimeError: + pass + + grid = microgrid.grid.get() + assert grid is None + + # validate that microgrids with one grid connection are accepted. + components = [ + Component(1, ComponentCategory.GRID, None, GridMetadata(123.0)), + Component(2, ComponentCategory.METER), + ] + + microgrid.grid.initialize(components) + + grid = microgrid.grid.get() + assert grid == Grid(max_current=123.0) + + max_current = grid.max_current + assert max_current == 123.0 From 619858fac2cfbcb05a16a41d77f02ca21f0dc32e Mon Sep 17 00:00:00 2001 From: Tiyash Basu Date: Fri, 25 Aug 2023 12:09:06 +0200 Subject: [PATCH 6/7] Add a Fuse class and use it in Grid This commit introduces a class to represent fuses. Fuses have just one property representing their rated max current. If the current flowing through a fuse is greater than this limit, then the fuse will break the circuit. This commit also introduces using a Fuse object to represent the allowed max current at a grid connection point. Signed-off-by: Tiyash Basu --- RELEASE_NOTES.md | 5 ++++- src/frequenz/sdk/microgrid/__init__.py | 3 ++- src/frequenz/sdk/microgrid/fuse.py | 16 ++++++++++++++++ src/frequenz/sdk/microgrid/grid.py | 9 ++++++--- tests/microgrid/test_grid.py | 12 +++++++++--- 5 files changed, 37 insertions(+), 8 deletions(-) create mode 100644 src/frequenz/sdk/microgrid/fuse.py diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 2a79c97fa..60629eefa 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -102,10 +102,13 @@ This release replaces the `@actor` decorator with a new `Actor` class. - `Actor`: This new class inherits from `BackgroundService` and it replaces the `@actor` decorator. -- Calling `microgrid.initialize()` now also initializes the microgrid's grid connection point as a singleton object of a newly added type `Grid`. This object can be obtained by calling `microgrid.grid.get()`. This object exposes the max current that can course through the grid connection point, which is useful for the power distribution algorithm. The max current is provided by the Microgrid API, and can be obtained by calling `microgrid.grid.get().max_current`. +- Calling `microgrid.initialize()` now also initializes the microgrid's grid connection point as a singleton object of a newly added type `Grid`. This object can be obtained by calling `microgrid.grid.get()`. This object exposes the max current that can course through the grid connection point, which is useful for the power distribution algorithm. The max current is provided by the Microgrid API, and can be obtained by calling `microgrid.grid.get().fuse.max_current`. Note that a microgrid is allowed to have zero or one grid connection point. Microgrids configured as islands will have zero grid connection points, and microgrids configured as grid-connected will have one grid connection point. +- A new class `Fuse` has been added to represent fuses. This class has a member variable `max_current` which represents the maximum current that can course through the fuse. If the current flowing through a fuse is greater than this limit, then the fuse will break the circuit. + + ## Bug Fixes - Fixes a bug in the ring buffer updating the end timestamp of gaps when they are outdated. diff --git a/src/frequenz/sdk/microgrid/__init__.py b/src/frequenz/sdk/microgrid/__init__.py index fe6718406..cb70e3e2e 100644 --- a/src/frequenz/sdk/microgrid/__init__.py +++ b/src/frequenz/sdk/microgrid/__init__.py @@ -8,7 +8,7 @@ """ from ..actor import ResamplerConfig -from . import _data_pipeline, client, component, connection_manager, grid +from . import _data_pipeline, client, component, connection_manager, fuse, grid from ._data_pipeline import battery_pool, ev_charger_pool, logical_meter from ._graph import ComponentGraph @@ -37,6 +37,7 @@ async def initialize(host: str, port: int, resampler_config: ResamplerConfig) -> "component", "battery_pool", "ev_charger_pool", + "fuse", "grid", "logical_meter", ] diff --git a/src/frequenz/sdk/microgrid/fuse.py b/src/frequenz/sdk/microgrid/fuse.py new file mode 100644 index 000000000..f5c80261f --- /dev/null +++ b/src/frequenz/sdk/microgrid/fuse.py @@ -0,0 +1,16 @@ +# License: MIT +# Copyright © 2023 Frequenz Energy-as-a-Service GmbH + +"""Fuse data class.""" + +from dataclasses import dataclass + +from ..timeseries import Current + + +@dataclass(frozen=True) +class Fuse: + """Fuse data class.""" + + max_current: Current + """Rated current of the fuse.""" diff --git a/src/frequenz/sdk/microgrid/grid.py b/src/frequenz/sdk/microgrid/grid.py index 6c7487daf..8129e280f 100644 --- a/src/frequenz/sdk/microgrid/grid.py +++ b/src/frequenz/sdk/microgrid/grid.py @@ -11,16 +11,18 @@ from collections.abc import Iterable from dataclasses import dataclass +from ..timeseries import Current from .component import Component from .component._component import ComponentCategory +from .fuse import Fuse @dataclass(frozen=True) class Grid: """A grid connection point.""" - max_current: float - """The maximum current that can course through the grid connection point, in Amperes.""" + fuse: Fuse + """The fuse protecting the grid connection point.""" _GRID: Grid | None = None @@ -56,7 +58,8 @@ def initialize(components: Iterable[Component]) -> None: ) else: max_current = grid_connections[0].metadata.max_current # type: ignore - _GRID = Grid(max_current) + fuse = Fuse(Current.from_amperes(max_current)) + _GRID = Grid(fuse) def get() -> Grid | None: diff --git a/tests/microgrid/test_grid.py b/tests/microgrid/test_grid.py index 309c92324..96d398d12 100644 --- a/tests/microgrid/test_grid.py +++ b/tests/microgrid/test_grid.py @@ -7,7 +7,9 @@ from frequenz.sdk import microgrid from frequenz.sdk.microgrid.component import Component, ComponentCategory, GridMetadata +from frequenz.sdk.microgrid.fuse import Fuse from frequenz.sdk.microgrid.grid import Grid +from frequenz.sdk.timeseries import Current async def test_grid() -> None: @@ -53,7 +55,11 @@ async def test_grid() -> None: microgrid.grid.initialize(components) grid = microgrid.grid.get() - assert grid == Grid(max_current=123.0) - max_current = grid.max_current - assert max_current == 123.0 + expected_fuse_current = Current.from_amperes(123.0) + expected_fuse = Fuse(expected_fuse_current) + + assert grid == Grid(fuse=expected_fuse) + + fuse_current = grid.fuse.max_current + assert fuse_current == expected_fuse_current From d2de57180b42148fbebd2ccd44317e5be0380d66 Mon Sep 17 00:00:00 2001 From: Tiyash Basu Date: Fri, 25 Aug 2023 12:45:37 +0200 Subject: [PATCH 7/7] Use fuse in GridMetadata This commit uses a Fuse object to represent the max current limit in component metadata in the internal layer that parses component objects obtained directly from the Microgrid API. This makes the abstraction of fuses one level deeper, increasing the chances of the `grid_connection.py` file being more stable in the future if/when further changes are introduced in the Microgrid API. Signed-off-by: Tiyash Basu --- src/frequenz/sdk/microgrid/component/_component.py | 13 +++++++++---- src/frequenz/sdk/microgrid/grid.py | 14 ++++++++++---- tests/microgrid/test_client.py | 6 +++++- tests/microgrid/test_graph.py | 10 ++++++++-- tests/microgrid/test_grid.py | 9 ++++++--- 5 files changed, 38 insertions(+), 14 deletions(-) diff --git a/src/frequenz/sdk/microgrid/component/_component.py b/src/frequenz/sdk/microgrid/component/_component.py index de2b01b54..16f4fa936 100644 --- a/src/frequenz/sdk/microgrid/component/_component.py +++ b/src/frequenz/sdk/microgrid/component/_component.py @@ -12,6 +12,9 @@ import frequenz.api.microgrid.grid_pb2 as grid_pb import frequenz.api.microgrid.inverter_pb2 as inverter_pb +from ...timeseries import Current +from ..fuse import Fuse + class ComponentType(Enum): """A base class from which individual component types are derived.""" @@ -110,21 +113,23 @@ def _component_category_from_protobuf( class ComponentMetadata: """Base class for component metadata classes.""" + fuse: Fuse | None = None + """The fuse at the grid connection point.""" + @dataclass(frozen=True) class GridMetadata(ComponentMetadata): """Metadata for a grid connection point.""" - max_current: float - """maximum current rating of the grid connection point in amps.""" - def _component_metadata_from_protobuf( component_category: components_pb.ComponentCategory.ValueType, component_metadata: grid_pb.Metadata, ) -> GridMetadata | None: if component_category == components_pb.ComponentCategory.COMPONENT_CATEGORY_GRID: - return GridMetadata(float(component_metadata.rated_fuse_current)) + max_current = Current.from_amperes(component_metadata.rated_fuse_current) + fuse = Fuse(max_current) + return GridMetadata(fuse) return None diff --git a/src/frequenz/sdk/microgrid/grid.py b/src/frequenz/sdk/microgrid/grid.py index 8129e280f..b0ae3cd3e 100644 --- a/src/frequenz/sdk/microgrid/grid.py +++ b/src/frequenz/sdk/microgrid/grid.py @@ -11,7 +11,6 @@ from collections.abc import Iterable from dataclasses import dataclass -from ..timeseries import Current from .component import Component from .component._component import ComponentCategory from .fuse import Fuse @@ -36,7 +35,8 @@ def initialize(components: Iterable[Component]) -> None: Raises: RuntimeError: If there is more than 1 grid connection point in the - microgrid. + microgrid, or if the grid connection point is not initialized, + or if the grid connection point does not have a fuse. """ global _GRID # pylint: disable=global-statement @@ -57,8 +57,14 @@ def initialize(components: Iterable[Component]) -> None: f"Expected at most one grid connection, got {grid_connections_count}" ) else: - max_current = grid_connections[0].metadata.max_current # type: ignore - fuse = Fuse(Current.from_amperes(max_current)) + if grid_connections[0].metadata is None: + raise RuntimeError("Grid metadata is None") + + fuse = grid_connections[0].metadata.fuse + + if fuse is None: + raise RuntimeError("Grid fuse is None") + _GRID = Grid(fuse) diff --git a/tests/microgrid/test_client.py b/tests/microgrid/test_client.py index 7dd98d45d..8b38e72bb 100644 --- a/tests/microgrid/test_client.py +++ b/tests/microgrid/test_client.py @@ -26,6 +26,7 @@ InverterType, MeterData, ) +from frequenz.sdk.microgrid.fuse import Fuse from frequenz.sdk.timeseries import Current from . import mock_api @@ -125,13 +126,16 @@ async def test_components(self) -> None: Current.from_amperes(123.0), ) + grid_max_current = Current.from_amperes(123.0) + grid_fuse = Fuse(grid_max_current) + assert set(await microgrid.components()) == { Component(100, ComponentCategory.NONE), Component( 101, ComponentCategory.GRID, None, - GridMetadata(max_current=123.0), + GridMetadata(fuse=grid_fuse), ), Component(104, ComponentCategory.METER), Component(105, ComponentCategory.INVERTER, InverterType.NONE), diff --git a/tests/microgrid/test_graph.py b/tests/microgrid/test_graph.py index 740d4abd0..7d6f35640 100644 --- a/tests/microgrid/test_graph.py +++ b/tests/microgrid/test_graph.py @@ -24,6 +24,8 @@ GridMetadata, InverterType, ) +from frequenz.sdk.microgrid.fuse import Fuse +from frequenz.sdk.timeseries import Current from .mock_api import MockGrpcServer, MockMicrogridServicer @@ -821,6 +823,9 @@ async def test_refresh_from_api(self) -> None: servicer.set_connections([(101, 111), (111, 131)]) await graph.refresh_from_api(client) + grid_max_current = Current.zero() + grid_fuse = Fuse(grid_max_current) + # Note: we need to add GriMetadata as a dict here, because that's what # the ComponentGraph does too, and we need to be able to compare the # two graphs. @@ -829,7 +834,7 @@ async def test_refresh_from_api(self) -> None: 101, ComponentCategory.GRID, None, - asdict(GridMetadata(max_current=0.0)), # type: ignore + asdict(GridMetadata(fuse=grid_fuse)), # type: ignore ), Component(111, ComponentCategory.METER), Component(131, ComponentCategory.EV_CHARGER), @@ -855,12 +860,13 @@ async def test_refresh_from_api(self) -> None: ) servicer.set_connections([(707, 717), (717, 727), (727, 737), (717, 747)]) await graph.refresh_from_api(client) + expected = { Component( 707, ComponentCategory.GRID, None, - asdict(GridMetadata(max_current=0.0)), # type: ignore + asdict(GridMetadata(fuse=grid_fuse)), # type: ignore ), Component(717, ComponentCategory.METER), Component(727, ComponentCategory.INVERTER, InverterType.NONE), diff --git a/tests/microgrid/test_grid.py b/tests/microgrid/test_grid.py index 96d398d12..b401266c4 100644 --- a/tests/microgrid/test_grid.py +++ b/tests/microgrid/test_grid.py @@ -31,9 +31,12 @@ async def test_grid() -> None: # validate that the microgrid initialization fails when there are multiple # grid connection points. + fuse_current = Current.from_amperes(123.0) + fuse = Fuse(fuse_current) + components = [ - Component(1, ComponentCategory.GRID, None, GridMetadata(123.0)), - Component(2, ComponentCategory.GRID, None, GridMetadata(345.0)), + Component(1, ComponentCategory.GRID, None, GridMetadata(fuse)), + Component(2, ComponentCategory.GRID, None, GridMetadata(fuse)), Component(3, ComponentCategory.METER), ] @@ -48,7 +51,7 @@ async def test_grid() -> None: # validate that microgrids with one grid connection are accepted. components = [ - Component(1, ComponentCategory.GRID, None, GridMetadata(123.0)), + Component(1, ComponentCategory.GRID, None, GridMetadata(fuse)), Component(2, ComponentCategory.METER), ]