Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +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().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.
Expand Down
9 changes: 8 additions & 1 deletion src/frequenz/sdk/microgrid/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"""

from ..actor import ResamplerConfig
from . import _data_pipeline, client, component, connection_manager
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

Expand All @@ -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)


Expand All @@ -32,5 +37,7 @@ async def initialize(host: str, port: int, resampler_config: ResamplerConfig) ->
"component",
"battery_pool",
"ev_charger_pool",
"fuse",
"grid",
"logical_meter",
]
2 changes: 2 additions & 0 deletions src/frequenz/sdk/microgrid/client/_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
)
from ..component._component import (
_component_category_from_protobuf,
_component_metadata_from_protobuf,
_component_type_from_protobuf,
)
from ._connection import Connection
Expand Down Expand Up @@ -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,
)
Expand Down
11 changes: 10 additions & 1 deletion src/frequenz/sdk/microgrid/component/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -21,10 +28,12 @@
"Component",
"ComponentData",
"ComponentCategory",
"ComponentMetadata",
"ComponentMetricId",
"EVChargerCableState",
"EVChargerComponentState",
"EVChargerData",
"GridMetadata",
"InverterData",
"InverterType",
"MeterData",
Expand Down
43 changes: 40 additions & 3 deletions src/frequenz/sdk/microgrid/component/_component.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@

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.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."""
Expand All @@ -32,7 +35,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.
Expand Down Expand Up @@ -106,13 +109,39 @@ def _component_category_from_protobuf(
return ComponentCategory(component_category)


@dataclass(frozen=True)
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."""


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:
max_current = Current.from_amperes(component_metadata.rated_fuse_current)
fuse = Fuse(max_current)
return GridMetadata(fuse)

return None


@dataclass(frozen=True)
class Component:
"""Metadata for a single microgrid component."""

component_id: int
category: ComponentCategory
type: Optional[ComponentType] = None
type: ComponentType | None = None
metadata: ComponentMetadata | None = None

def is_valid(self) -> bool:
"""Check if this instance contains valid data.
Expand All @@ -125,6 +154,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."""
Expand Down
16 changes: 16 additions & 0 deletions src/frequenz/sdk/microgrid/fuse.py
Original file line number Diff line number Diff line change
@@ -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."""
80 changes: 80 additions & 0 deletions src/frequenz/sdk/microgrid/grid.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# 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
from .fuse import Fuse


@dataclass(frozen=True)
class Grid:
"""A grid connection point."""

fuse: Fuse
"""The fuse protecting the grid connection point."""


_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, 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

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:
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)


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
15 changes: 15 additions & 0 deletions tests/microgrid/mock_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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."""
Expand All @@ -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)
Expand Down
22 changes: 20 additions & 2 deletions tests/microgrid/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,13 @@
Component,
ComponentCategory,
EVChargerData,
GridMetadata,
InverterData,
InverterType,
MeterData,
)
from frequenz.sdk.microgrid.fuse import Fuse
from frequenz.sdk.timeseries import Current

from . import mock_api

Expand Down Expand Up @@ -106,7 +109,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),
Expand All @@ -117,9 +119,24 @@ async def test_components(self) -> None:
(999, components_pb.ComponentCategory.COMPONENT_CATEGORY_SENSOR),
]
)

servicer.add_component(
101,
components_pb.ComponentCategory.COMPONENT_CATEGORY_GRID,
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),
Component(
101,
ComponentCategory.GRID,
None,
GridMetadata(fuse=grid_fuse),
),
Component(104, ComponentCategory.METER),
Component(105, ComponentCategory.INVERTER, InverterType.NONE),
Component(106, ComponentCategory.BATTERY),
Expand All @@ -136,6 +153,7 @@ async def test_components(self) -> None:
servicer.add_component(
99,
components_pb.ComponentCategory.COMPONENT_CATEGORY_INVERTER,
None,
components_pb.InverterType.INVERTER_TYPE_BATTERY,
)

Expand Down
Loading