Skip to content
Closed
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
10 changes: 7 additions & 3 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 @ git+https://github.com/frequenz-floss/frequenz-api-common.git@2e89add6a16d42b23612f0f791a499919f3738ed",
"frequenz-core >= 1.0.2, < 2",
]
dynamic = ["version"]

Expand Down Expand Up @@ -149,8 +150,11 @@ 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',
]
testpaths = ["tests", "src"]
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)
# -> 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."""
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 electrical 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.electrical_components.electrical_components_pb2 import (
Expand All @@ -16,11 +18,19 @@
from frequenz.api.common.v1.microgrid.electrical_components.electrical_components_pb2 import (
ElectricalComponentStateCode as PBElectricalComponentStateCode,
)
from frequenz.core.id import BaseId
from typing_extensions import deprecated

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


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


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

UNSPECIFIED = (
Expand Down Expand Up @@ -67,20 +77,38 @@ class ElectricalComponentCategory(Enum):
"""A relay, used for switching electrical circuits on and off."""

PRECHARGER = PBElectricalComponentCategory.ELECTRICAL_COMPONENT_CATEGORY_PRECHARGER
"""A precharger, used for preparing electrical circuits for switching on."""
"""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 = PBElectricalComponentCategory.ELECTRICAL_COMPONENT_CATEGORY_FUSE
"""A fuse, used for protecting electrical circuits from overcurrent."""

TRANSFORMER = (
VOLTAGE_TRANSFORMER = (
PBElectricalComponentCategory.ELECTRICAL_COMPONENT_CATEGORY_VOLTAGE_TRANSFORMER
)
"""A transformer, used for changing the voltage of electrical circuits."""
"""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 = PBElectricalComponentCategory.ELECTRICAL_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: PBElectricalComponentCategory.ValueType
) -> ElectricalComponentCategory:
Expand All @@ -105,7 +133,8 @@ def to_proto(self) -> PBElectricalComponentCategory.ValueType:
return self.value


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

UNSPECIFIED = (
Expand Down Expand Up @@ -207,6 +236,7 @@ class ElectricalComponentStateCode(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: PBElectricalComponentStateCode.ValueType
) -> ElectricalComponentStateCode:
Expand All @@ -231,7 +261,8 @@ def to_proto(self) -> PBElectricalComponentStateCode.ValueType:
return self.value


class ElectricalComponentDiagnosticCode(Enum):
@enum.unique
class ElectricalComponentDiagnosticCode(enum.Enum):
"""All diagnostics that can occur across electrical component categories."""

UNSPECIFIED = (
Expand Down Expand Up @@ -426,6 +457,7 @@ class ElectricalComponentDiagnosticCode(Enum):
times."""

@classmethod
@deprecated("Use `frequenz.client.common.enum_proto.enum_from_proto` instead.")
def from_proto(
cls, component_error_code: PBElectricalComponentDiagnosticCode.ValueType
) -> ElectricalComponentDiagnosticCode:
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.electrical_components import ElectricalComponentId
from frequenz.client.common.microgrid.sensors import SensorId


@pytest.mark.parametrize(
"id_class, prefix",
[
(EnterpriseId, "EID"),
(MicrogridId, "MID"),
(ElectricalComponentId, "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)"
50 changes: 50 additions & 0 deletions tests/test_enum_proto.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# License: MIT
# Copyright © 2025 Frequenz Energy-as-a-Service GmbH

"""Tests for enum_from_proto utility."""

import enum

import pytest

from frequenz.client.common.enum_proto import enum_from_proto


class _TestEnum(enum.Enum):
"""A test enum for enum_from_proto tests."""

ZERO = 0
ONE = 1
TWO = 2


@pytest.mark.parametrize("enum_member", _TestEnum)
def test_valid_allow_invalid(enum_member: _TestEnum) -> None:
"""Test conversion of valid enum values."""
assert enum_from_proto(enum_member.value, _TestEnum) == enum_member
assert (
enum_from_proto(enum_member.value, _TestEnum, allow_invalid=True) == enum_member
)


@pytest.mark.parametrize("value", [42, -1])
def test_invalid_allow_invalid(value: int) -> None:
"""Test unknown values with allow_invalid=True (default)."""
assert enum_from_proto(value, _TestEnum) == value
assert enum_from_proto(value, _TestEnum, allow_invalid=True) == value


@pytest.mark.parametrize("enum_member", _TestEnum)
def test_valid_disallow_invalid(enum_member: _TestEnum) -> None:
"""Test unknown values with allow_invalid=False (should raise ValueError)."""
assert (
enum_from_proto(enum_member.value, _TestEnum, allow_invalid=False)
== enum_member
)


@pytest.mark.parametrize("value", [42, -1])
def test_invalid_disallow(value: int) -> None:
"""Test unknown values with allow_invalid=False (should raise ValueError)."""
with pytest.raises(ValueError, match=rf"^{value} is not a valid _TestEnum$"):
enum_from_proto(value, _TestEnum, allow_invalid=False)
Loading