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
2 changes: 1 addition & 1 deletion RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ This release introduces the `v1alpha8` module to support a new API version.

## Upgrading

- The `typing-extensions` dependency minimum version was bumped to 4.6 to support Python 3.12.
<!-- Here goes notes on how to upgrade from previous versions, including deprecations and what they should be replaced with -->

## New Features

Expand Down
14 changes: 9 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,9 @@ classifiers = [
]
requires-python = ">= 3.11, < 4"
dependencies = [
"typing-extensions >= 4.6.0, < 5",
"typing-extensions >= 4.13.0, < 5",
"frequenz-api-common >= 0.8.0, < 9",
"frequenz-core >= 1.0.2, < 2",
]
dynamic = ["version"]

Expand All @@ -39,7 +40,7 @@ email = "[email protected]"
dev-flake8 = [
"flake8 == 7.3.0",
"flake8-docstrings == 1.7.0",
"flake8-pyproject == 1.2.3", # For reading the flake8 config from pyproject.toml
"flake8-pyproject == 1.2.3", # For reading the flake8 config from pyproject.toml
"pydoclint == 0.6.6",
"pydocstyle == 6.3.0",
]
Expand Down Expand Up @@ -144,15 +145,18 @@ disable = [
]

[tool.pytest.ini_options]
addopts = "-vv"
filterwarnings = [
"error",
"once::DeprecationWarning",
"once::PendingDeprecationWarning",
# We use a raw string (single quote) to avoid the need to escape special
# chars as this is a regex
# We ignore warnings about protobuf gencode version being one version older
# than the current version, as this is supported by protobuf, and we expect to
# have such cases. If we go too far, we will get a proper error anyways.
# We use a raw string (single quotes) to avoid the need to escape special
# characters as this is a regex.
'ignore:Protobuf gencode version .*exactly one major version older.*:UserWarning',
]
addopts = "-vv"
testpaths = ["tests", "src"]
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "function"
Expand Down
76 changes: 76 additions & 0 deletions src/frequenz/client/common/enum_proto.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# License: MIT
# Copyright © 2025 Frequenz Energy-as-a-Service GmbH

"""Conversion of protobuf int enums to Python enums."""

import enum
from typing import Literal, TypeVar, overload

EnumT = TypeVar("EnumT", bound=enum.Enum)
"""A type variable that is bound to an enum."""


@overload
def enum_from_proto(
value: int, enum_type: type[EnumT], *, allow_invalid: Literal[False]
) -> EnumT: ...


@overload
def enum_from_proto(
value: int, enum_type: type[EnumT], *, allow_invalid: Literal[True] = True
) -> EnumT | int: ...


def enum_from_proto(
value: int, enum_type: type[EnumT], *, allow_invalid: bool = True
) -> EnumT | int:
"""Convert a protobuf int enum value to a python enum.
Example:
```python
import enum
from proto import proto_pb2 # Just an example. pylint: disable=import-error
@enum.unique
class SomeEnum(enum.Enum):
# These values should match the protobuf enum values.
UNSPECIFIED = 0
SOME_VALUE = 1
enum_value = enum_from_proto(proto_pb2.SomeEnum.SOME_ENUM_SOME_VALUE, SomeEnum)
Copy link

Copilot AI Aug 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The example imports proto_pb2 with a pylint disable comment, but this import will fail in practice. Consider using a more realistic example or clarifying that this is pseudocode.

Copilot uses AI. Check for mistakes.
# -> SomeEnum.SOME_VALUE
enum_value = enum_from_proto(42, SomeEnum)
# -> 42
enum_value = enum_from_proto(
proto_pb2.SomeEnum.SOME_ENUM_UNKNOWN_VALUE, SomeEnum, allow_invalid=False
)
# -> ValueError
```
Args:
value: The protobuf int enum value.
enum_type: The python enum type to convert to.
allow_invalid: If `True`, return the value as an `int` if the value is not
a valid member of the enum (this allows for forward-compatibility with new
enum values defined in the protocol but not added to the Python enum yet).
If `False`, raise a `ValueError` if the value is not a valid member of the
enum.
Returns:
The resulting python enum value if the protobuf value is known, otherwise
the input value converted to a plain `int`.
Raises:
ValueError: If `allow_invalid` is `False` and the value is not a valid member
of the enum.
"""
try:
return enum_type(value)
except ValueError:
if allow_invalid:
return value
raise
7 changes: 5 additions & 2 deletions src/frequenz/client/common/metric/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,18 @@

"""Module to define the metrics used with the common client."""

from enum import Enum
import enum
from typing import Self

# pylint: disable=no-name-in-module
from frequenz.api.common.v1.metrics.metric_sample_pb2 import Metric as PBMetric
from typing_extensions import deprecated

# pylint: enable=no-name-in-module


class Metric(Enum):
@enum.unique
class Metric(enum.Enum):
"""List of supported metrics.

AC energy metrics information:
Expand Down Expand Up @@ -140,6 +142,7 @@ class Metric(Enum):
SENSOR_IRRADIANCE = PBMetric.METRIC_SENSOR_IRRADIANCE

@classmethod
@deprecated("Use `frequenz.client.common.enum_proto.enum_from_proto` instead.")
def from_proto(cls, metric: PBMetric.ValueType) -> Self:
"""Convert a protobuf Metric value to Metric enum.

Expand Down
14 changes: 14 additions & 0 deletions src/frequenz/client/common/microgrid/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,17 @@
# Copyright © 2023 Frequenz Energy-as-a-Service GmbH

"""Frequenz microgrid definition."""

from typing import final

from frequenz.core.id import BaseId


@final
class EnterpriseId(BaseId, str_prefix="EID"):
"""A unique identifier for an enterprise account."""


@final
class MicrogridId(BaseId, str_prefix="MID"):
"""A unique identifier for a microgrid."""
69 changes: 65 additions & 4 deletions src/frequenz/client/common/microgrid/components/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@
# Copyright © 2022 Frequenz Energy-as-a-Service GmbH

"""Defines the components that can be used in a microgrid."""

from __future__ import annotations

from enum import Enum
import enum
from typing import final

# pylint: disable=no-name-in-module
from frequenz.api.common.v1.microgrid.components.components_pb2 import (
Expand All @@ -16,11 +18,19 @@
from frequenz.api.common.v1.microgrid.components.components_pb2 import (
ComponentStateCode as PBComponentStateCode,
)
from frequenz.core.id import BaseId
from typing_extensions import deprecated

# pylint: enable=no-name-in-module


class ComponentCategory(Enum):
@final
class ComponentId(BaseId, str_prefix="CID"):
"""A unique identifier for a microgrid component."""


@enum.unique
class ComponentCategory(enum.Enum):
"""Possible types of microgrid component."""

UNSPECIFIED = PBComponentCategory.COMPONENT_CATEGORY_UNSPECIFIED
Expand All @@ -39,16 +49,63 @@ class ComponentCategory(Enum):
INVERTER = PBComponentCategory.COMPONENT_CATEGORY_INVERTER
"""An electricity generator, with batteries or solar energy."""

CONVERTER = PBComponentCategory.COMPONENT_CATEGORY_CONVERTER
"""A DC-DC converter."""

BATTERY = PBComponentCategory.COMPONENT_CATEGORY_BATTERY
"""A storage system for electrical energy, used by inverters."""

EV_CHARGER = PBComponentCategory.COMPONENT_CATEGORY_EV_CHARGER
"""A station for charging electrical vehicles."""

CRYPTO_MINER = PBComponentCategory.COMPONENT_CATEGORY_CRYPTO_MINER
"""A crypto miner."""

ELECTROLYZER = PBComponentCategory.COMPONENT_CATEGORY_ELECTROLYZER
"""An electrolyzer for converting water into hydrogen and oxygen."""

CHP = PBComponentCategory.COMPONENT_CATEGORY_CHP
"""A heat and power combustion plant (CHP stands for combined heat and power)."""

RELAY = PBComponentCategory.COMPONENT_CATEGORY_RELAY
"""A relay.

Relays generally have two states: open (connected) and closed (disconnected).
They are generally placed in front of a component, e.g., an inverter, to
control whether the component is connected to the grid or not.
"""

PRECHARGER = PBComponentCategory.COMPONENT_CATEGORY_PRECHARGER
"""A precharge module.

Precharging involves gradually ramping up the DC voltage to prevent any
potential damage to sensitive electrical components like capacitors.

While many inverters and batteries come equipped with in-built precharging
mechanisms, some may lack this feature. In such cases, we need to use
external precharging modules.
"""

FUSE = PBComponentCategory.COMPONENT_CATEGORY_FUSE
"""A fuse."""

VOLTAGE_TRANSFORMER = PBComponentCategory.COMPONENT_CATEGORY_VOLTAGE_TRANSFORMER
"""A voltage transformer.

Voltage transformers are used to step up or step down the voltage, keeping
the power somewhat constant by increasing or decreasing the current. If voltage is
stepped up, current is stepped down, and vice versa.

Note:
Voltage transformers have efficiency losses, so the output power is
always less than the input power.
"""

HVAC = PBComponentCategory.COMPONENT_CATEGORY_HVAC
"""A Heating, Ventilation, and Air Conditioning (HVAC) system."""

@classmethod
@deprecated("Use `frequenz.client.common.enum_proto.enum_from_proto` instead.")
def from_proto(
cls, component_category: PBComponentCategory.ValueType
) -> ComponentCategory:
Expand All @@ -73,7 +130,8 @@ def to_proto(self) -> PBComponentCategory.ValueType:
return self.value


class ComponentStateCode(Enum):
@enum.unique
class ComponentStateCode(enum.Enum):
"""All possible states of a microgrid component."""

UNSPECIFIED = PBComponentStateCode.COMPONENT_STATE_CODE_UNSPECIFIED
Expand Down Expand Up @@ -153,6 +211,7 @@ class ComponentStateCode(Enum):
"""The precharger circuit is closed, allowing full current to flow to the main circuit."""

@classmethod
@deprecated("Use `frequenz.client.common.enum_proto.enum_from_proto` instead.")
def from_proto(
cls, component_state: PBComponentStateCode.ValueType
) -> ComponentStateCode:
Expand All @@ -177,7 +236,8 @@ def to_proto(self) -> PBComponentStateCode.ValueType:
return self.value


class ComponentErrorCode(Enum):
@enum.unique
class ComponentErrorCode(enum.Enum):
"""All possible errors that can occur across all microgrid component categories."""

UNSPECIFIED = PBComponentErrorCode.COMPONENT_ERROR_CODE_UNSPECIFIED
Expand Down Expand Up @@ -330,6 +390,7 @@ class ComponentErrorCode(Enum):
times."""

@classmethod
@deprecated("Use `frequenz.client.common.enum_proto.enum_from_proto` instead.")
def from_proto(
cls, component_error_code: PBComponentErrorCode.ValueType
) -> ComponentErrorCode:
Expand Down
13 changes: 13 additions & 0 deletions src/frequenz/client/common/microgrid/sensors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# License: MIT
# Copyright © 2025 Frequenz Energy-as-a-Service GmbH

"""Microgrid sensors."""

from typing import final

from frequenz.core.id import BaseId


@final
class SensorId(BaseId, str_prefix="SID"):
"""A unique identifier for a microgrid sensor."""
28 changes: 28 additions & 0 deletions tests/microgrid/test_ids.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# License: MIT
# Copyright © 2025 Frequenz Energy-as-a-Service GmbH

"""Tests for microgrid-related IDs."""

import pytest
from frequenz.core.id import BaseId

from frequenz.client.common.microgrid import EnterpriseId, MicrogridId
from frequenz.client.common.microgrid.components import ComponentId
from frequenz.client.common.microgrid.sensors import SensorId


@pytest.mark.parametrize(
"id_class, prefix",
[
(EnterpriseId, "EID"),
(MicrogridId, "MID"),
(ComponentId, "CID"),
(SensorId, "SID"),
],
)
def test_string_representation(id_class: type[BaseId], prefix: str) -> None:
"""Test string representation of IDs."""
_id = id_class(123)

assert str(_id) == f"{prefix}123"
assert repr(_id) == f"{id_class.__name__}(123)"
Loading
Loading