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
9 changes: 7 additions & 2 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,16 @@

## Upgrading

- The `typing-extensions` dependency minimum version was bumped to 4.6 to support Python 3.12.
- The metrics and components enums `.from_proto()` are deprecated, please use the new `enum_from_proto()` instead.
- Some minimum dependencies have been bumped, you might need to update your minimum dependencies too:

* `frequenz-api-common` to 0.6.1
* `frequenz-core` to 1.0.2

## New Features

<!-- Here goes the main new features and examples or instructions on how to use them -->
- A new module `frequenz.client.common.enum_proto` has been added, which provides a generic `enum_from_proto()` function to convert protobuf enums to Python enums.
- The `frequenz.client.common.microgrid.ComponentCategory` was extended to include the missing categories.

## Bug Fixes

Expand Down
18 changes: 15 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",
"frequenz-api-common >= 0.6.0, < 7",
"typing-extensions >= 4.13.0, < 5",
"frequenz-api-common >= 0.6.1, < 7",
"frequenz-core >= 1.0.2, < 2",
]
dynamic = ["version"]

Expand Down Expand Up @@ -144,7 +145,18 @@ disable = [
]

[tool.pytest.ini_options]
addopts = "-W=all -Werror -Wdefault::DeprecationWarning -Wdefault::PendingDeprecationWarning '-Wdefault:Protobuf gencode version 5.27.2 is exactly one major version older than the runtime version:UserWarning' -vv"
addopts = "-vv"
filterwarnings = [
"error",
Copy link

Copilot AI Jul 1, 2025

Choose a reason for hiding this comment

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

The simple "error" filter may not match any specific warning category; consider using explicit filters (e.g., "error::Warning") or re-enable -Werror to ensure all warnings are treated as errors as intended.

Suggested change
"error",
"error::Warning",

Copilot uses AI. Check for mistakes.
"once::DeprecationWarning",
"once::PendingDeprecationWarning",
# 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"]
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)
# -> 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