Skip to content

Commit 3b944e4

Browse files
Initialize a Grid object with a given Fuse limit (#610)
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.
2 parents 26b6a33 + d2de571 commit 3b944e4

File tree

11 files changed

+294
-11
lines changed

11 files changed

+294
-11
lines changed

RELEASE_NOTES.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,13 @@
1212

1313
<!-- Here goes the main new features and examples or instructions on how to use them -->
1414

15+
- 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`.
16+
17+
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.
18+
19+
- 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.
20+
21+
1522
## Bug Fixes
1623

1724
<!-- Here goes notable bug fixes that are worth a special mention or explanation -->

src/frequenz/sdk/microgrid/__init__.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"""
99

1010
from ..actor import ResamplerConfig
11-
from . import _data_pipeline, client, component, connection_manager
11+
from . import _data_pipeline, client, component, connection_manager, fuse, grid
1212
from ._data_pipeline import battery_pool, ev_charger_pool, logical_meter
1313
from ._graph import ComponentGraph
1414

@@ -22,6 +22,11 @@ async def initialize(host: str, port: int, resampler_config: ResamplerConfig) ->
2222
resampler_config: Configuration for the resampling actor.
2323
"""
2424
await connection_manager.initialize(host, port)
25+
26+
api_client = connection_manager.get().api_client
27+
components = await api_client.components()
28+
grid.initialize(components)
29+
2530
await _data_pipeline.initialize(resampler_config)
2631

2732

@@ -32,5 +37,7 @@ async def initialize(host: str, port: int, resampler_config: ResamplerConfig) ->
3237
"component",
3338
"battery_pool",
3439
"ev_charger_pool",
40+
"fuse",
41+
"grid",
3542
"logical_meter",
3643
]

src/frequenz/sdk/microgrid/client/_client.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
)
3737
from ..component._component import (
3838
_component_category_from_protobuf,
39+
_component_metadata_from_protobuf,
3940
_component_type_from_protobuf,
4041
)
4142
from ._connection import Connection
@@ -255,6 +256,7 @@ async def components(self) -> Iterable[Component]:
255256
c.id,
256257
_component_category_from_protobuf(c.category),
257258
_component_type_from_protobuf(c.category, c.inverter),
259+
_component_metadata_from_protobuf(c.category, c.grid),
258260
),
259261
components_only,
260262
)

src/frequenz/sdk/microgrid/component/__init__.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,14 @@
66
This package provides classes to operate con microgrid components.
77
"""
88

9-
from ._component import Component, ComponentCategory, ComponentMetricId, InverterType
9+
from ._component import (
10+
Component,
11+
ComponentCategory,
12+
ComponentMetadata,
13+
ComponentMetricId,
14+
GridMetadata,
15+
InverterType,
16+
)
1017
from ._component_data import (
1118
BatteryData,
1219
ComponentData,
@@ -21,10 +28,12 @@
2128
"Component",
2229
"ComponentData",
2330
"ComponentCategory",
31+
"ComponentMetadata",
2432
"ComponentMetricId",
2533
"EVChargerCableState",
2634
"EVChargerComponentState",
2735
"EVChargerData",
36+
"GridMetadata",
2837
"InverterData",
2938
"InverterType",
3039
"MeterData",

src/frequenz/sdk/microgrid/component/_component.py

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,14 @@
77

88
from dataclasses import dataclass
99
from enum import Enum
10-
from typing import Optional
1110

1211
import frequenz.api.common.components_pb2 as components_pb
12+
import frequenz.api.microgrid.grid_pb2 as grid_pb
1313
import frequenz.api.microgrid.inverter_pb2 as inverter_pb
1414

15+
from ...timeseries import Current
16+
from ..fuse import Fuse
17+
1518

1619
class ComponentType(Enum):
1720
"""A base class from which individual component types are derived."""
@@ -32,7 +35,7 @@ class InverterType(ComponentType):
3235
def _component_type_from_protobuf(
3336
component_category: components_pb.ComponentCategory.ValueType,
3437
component_metadata: inverter_pb.Metadata,
35-
) -> Optional[ComponentType]:
38+
) -> ComponentType | None:
3639
"""Convert a protobuf InverterType message to Component enum.
3740
3841
For internal-only use by the `microgrid` package.
@@ -106,13 +109,39 @@ def _component_category_from_protobuf(
106109
return ComponentCategory(component_category)
107110

108111

112+
@dataclass(frozen=True)
113+
class ComponentMetadata:
114+
"""Base class for component metadata classes."""
115+
116+
fuse: Fuse | None = None
117+
"""The fuse at the grid connection point."""
118+
119+
120+
@dataclass(frozen=True)
121+
class GridMetadata(ComponentMetadata):
122+
"""Metadata for a grid connection point."""
123+
124+
125+
def _component_metadata_from_protobuf(
126+
component_category: components_pb.ComponentCategory.ValueType,
127+
component_metadata: grid_pb.Metadata,
128+
) -> GridMetadata | None:
129+
if component_category == components_pb.ComponentCategory.COMPONENT_CATEGORY_GRID:
130+
max_current = Current.from_amperes(component_metadata.rated_fuse_current)
131+
fuse = Fuse(max_current)
132+
return GridMetadata(fuse)
133+
134+
return None
135+
136+
109137
@dataclass(frozen=True)
110138
class Component:
111139
"""Metadata for a single microgrid component."""
112140

113141
component_id: int
114142
category: ComponentCategory
115-
type: Optional[ComponentType] = None
143+
type: ComponentType | None = None
144+
metadata: ComponentMetadata | None = None
116145

117146
def is_valid(self) -> bool:
118147
"""Check if this instance contains valid data.
@@ -125,6 +154,14 @@ def is_valid(self) -> bool:
125154
self.component_id > 0 and any(t == self.category for t in ComponentCategory)
126155
) or (self.component_id == 0 and self.category == ComponentCategory.GRID)
127156

157+
def __hash__(self) -> int:
158+
"""Compute a hash of this instance, obtained by hashing the `component_id` field.
159+
160+
Returns:
161+
Hash of this instance.
162+
"""
163+
return hash(self.component_id)
164+
128165

129166
class ComponentMetricId(Enum):
130167
"""An enum representing the various metrics available in the microgrid."""

src/frequenz/sdk/microgrid/fuse.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# License: MIT
2+
# Copyright © 2023 Frequenz Energy-as-a-Service GmbH
3+
4+
"""Fuse data class."""
5+
6+
from dataclasses import dataclass
7+
8+
from ..timeseries import Current
9+
10+
11+
@dataclass(frozen=True)
12+
class Fuse:
13+
"""Fuse data class."""
14+
15+
max_current: Current
16+
"""Rated current of the fuse."""

src/frequenz/sdk/microgrid/grid.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# License: MIT
2+
# Copyright © 2023 Frequenz Energy-as-a-Service GmbH
3+
4+
"""Grid connection point.
5+
6+
This module provides the `Grid` type, which represents a grid connection point
7+
in a microgrid.
8+
"""
9+
10+
import logging
11+
from collections.abc import Iterable
12+
from dataclasses import dataclass
13+
14+
from .component import Component
15+
from .component._component import ComponentCategory
16+
from .fuse import Fuse
17+
18+
19+
@dataclass(frozen=True)
20+
class Grid:
21+
"""A grid connection point."""
22+
23+
fuse: Fuse
24+
"""The fuse protecting the grid connection point."""
25+
26+
27+
_GRID: Grid | None = None
28+
29+
30+
def initialize(components: Iterable[Component]) -> None:
31+
"""Initialize the grid connection.
32+
33+
Args:
34+
components: The components in the microgrid.
35+
36+
Raises:
37+
RuntimeError: If there is more than 1 grid connection point in the
38+
microgrid, or if the grid connection point is not initialized,
39+
or if the grid connection point does not have a fuse.
40+
"""
41+
global _GRID # pylint: disable=global-statement
42+
43+
grid_connections = list(
44+
component
45+
for component in components
46+
if component.category == ComponentCategory.GRID
47+
)
48+
49+
grid_connections_count = len(grid_connections)
50+
51+
if grid_connections_count == 0:
52+
logging.info(
53+
"No grid connection found for this microgrid. This is normal for an islanded microgrid."
54+
)
55+
elif grid_connections_count > 1:
56+
raise RuntimeError(
57+
f"Expected at most one grid connection, got {grid_connections_count}"
58+
)
59+
else:
60+
if grid_connections[0].metadata is None:
61+
raise RuntimeError("Grid metadata is None")
62+
63+
fuse = grid_connections[0].metadata.fuse
64+
65+
if fuse is None:
66+
raise RuntimeError("Grid fuse is None")
67+
68+
_GRID = Grid(fuse)
69+
70+
71+
def get() -> Grid | None:
72+
"""Get the grid connection.
73+
74+
Note that a microgrid configured as an island will not have a grid
75+
connection point. For such microgrids, this function will return `None`.
76+
77+
Returns:
78+
The grid connection.
79+
"""
80+
return _GRID

tests/microgrid/mock_api.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
from frequenz.api.microgrid.battery_pb2 import Battery
3131
from frequenz.api.microgrid.battery_pb2 import Data as BatteryData
3232
from frequenz.api.microgrid.ev_charger_pb2 import EvCharger
33+
from frequenz.api.microgrid.grid_pb2 import Metadata as GridMetadata
3334
from frequenz.api.microgrid.inverter_pb2 import Inverter
3435
from frequenz.api.microgrid.inverter_pb2 import Metadata as InverterMetadata
3536
from frequenz.api.microgrid.meter_pb2 import Data as MeterData
@@ -57,6 +58,8 @@
5758
from google.protobuf.timestamp_pb2 import Timestamp
5859
from google.protobuf.wrappers_pb2 import BoolValue
5960

61+
from frequenz.sdk.timeseries import Current
62+
6063

6164
class MockMicrogridServicer( # pylint: disable=too-many-public-methods
6265
MicrogridServicer
@@ -89,6 +92,7 @@ def add_component(
8992
self,
9093
component_id: int,
9194
component_category: ComponentCategory.V,
95+
max_current: Optional[Current] = None,
9296
inverter_type: InverterType.V = InverterType.INVERTER_TYPE_UNSPECIFIED,
9397
) -> None:
9498
"""Add a component to the mock service."""
@@ -100,6 +104,17 @@ def add_component(
100104
inverter=InverterMetadata(type=inverter_type),
101105
)
102106
)
107+
elif (
108+
component_category == ComponentCategory.COMPONENT_CATEGORY_GRID
109+
and max_current is not None
110+
):
111+
self._components.append(
112+
Component(
113+
id=component_id,
114+
category=component_category,
115+
grid=GridMetadata(rated_fuse_current=int(max_current.as_amperes())),
116+
)
117+
)
103118
else:
104119
self._components.append(
105120
Component(id=component_id, category=component_category)

tests/microgrid/test_client.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,13 @@
2121
Component,
2222
ComponentCategory,
2323
EVChargerData,
24+
GridMetadata,
2425
InverterData,
2526
InverterType,
2627
MeterData,
2728
)
29+
from frequenz.sdk.microgrid.fuse import Fuse
30+
from frequenz.sdk.timeseries import Current
2831

2932
from . import mock_api
3033

@@ -106,7 +109,6 @@ async def test_components(self) -> None:
106109
100,
107110
components_pb.ComponentCategory.COMPONENT_CATEGORY_UNSPECIFIED,
108111
),
109-
(101, components_pb.ComponentCategory.COMPONENT_CATEGORY_GRID),
110112
(104, components_pb.ComponentCategory.COMPONENT_CATEGORY_METER),
111113
(105, components_pb.ComponentCategory.COMPONENT_CATEGORY_INVERTER),
112114
(106, components_pb.ComponentCategory.COMPONENT_CATEGORY_BATTERY),
@@ -117,9 +119,24 @@ async def test_components(self) -> None:
117119
(999, components_pb.ComponentCategory.COMPONENT_CATEGORY_SENSOR),
118120
]
119121
)
122+
123+
servicer.add_component(
124+
101,
125+
components_pb.ComponentCategory.COMPONENT_CATEGORY_GRID,
126+
Current.from_amperes(123.0),
127+
)
128+
129+
grid_max_current = Current.from_amperes(123.0)
130+
grid_fuse = Fuse(grid_max_current)
131+
120132
assert set(await microgrid.components()) == {
121133
Component(100, ComponentCategory.NONE),
122-
Component(101, ComponentCategory.GRID),
134+
Component(
135+
101,
136+
ComponentCategory.GRID,
137+
None,
138+
GridMetadata(fuse=grid_fuse),
139+
),
123140
Component(104, ComponentCategory.METER),
124141
Component(105, ComponentCategory.INVERTER, InverterType.NONE),
125142
Component(106, ComponentCategory.BATTERY),
@@ -136,6 +153,7 @@ async def test_components(self) -> None:
136153
servicer.add_component(
137154
99,
138155
components_pb.ComponentCategory.COMPONENT_CATEGORY_INVERTER,
156+
None,
139157
components_pb.InverterType.INVERTER_TYPE_BATTERY,
140158
)
141159

0 commit comments

Comments
 (0)