Skip to content

Commit 829aa1a

Browse files
authored
Merge v0.3.x into v0.x.x (#97)
This merges all v0.3.x changes into the v0.x.x branch. I assume we had an extra v0.3.x branch due to the rename changes of Components -> ElectricalComponents. Since then we decided to handle those API updates using extra paths like `.v1alpha8`, so we can add back the changes of v0.3.x and stay API compatible with our dependees. - 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. - Documentation for some `frequenz.client.common.microgrid.electrical_components.ElectricalComponentCategory` values was improved. - The metrics and components enums `.from_proto()` are deprecated, please use the new `enum_from_proto()` instead. - Classes to represent microgrid-related ID were added (`MicrogridId`, `EnterpriseId`, `ComponentId`, and `SensorId`).
2 parents 1a8b099 + 507a9c2 commit 829aa1a

File tree

9 files changed

+261
-12
lines changed

9 files changed

+261
-12
lines changed

RELEASE_NOTES.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ This release introduces the `v1alpha8` module to support a new API version.
66

77
## Upgrading
88

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

1111
## New Features
1212

pyproject.toml

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,9 @@ classifiers = [
2626
]
2727
requires-python = ">= 3.11, < 4"
2828
dependencies = [
29-
"typing-extensions >= 4.6.0, < 5",
29+
"typing-extensions >= 4.13.0, < 5",
3030
"frequenz-api-common >= 0.8.0, < 9",
31+
"frequenz-core >= 1.0.2, < 2",
3132
]
3233
dynamic = ["version"]
3334

@@ -39,7 +40,7 @@ email = "[email protected]"
3940
dev-flake8 = [
4041
"flake8 == 7.3.0",
4142
"flake8-docstrings == 1.7.0",
42-
"flake8-pyproject == 1.2.3", # For reading the flake8 config from pyproject.toml
43+
"flake8-pyproject == 1.2.3", # For reading the flake8 config from pyproject.toml
4344
"pydoclint == 0.6.6",
4445
"pydocstyle == 6.3.0",
4546
]
@@ -144,15 +145,18 @@ disable = [
144145
]
145146

146147
[tool.pytest.ini_options]
147-
addopts = "-vv"
148148
filterwarnings = [
149149
"error",
150150
"once::DeprecationWarning",
151151
"once::PendingDeprecationWarning",
152-
# We use a raw string (single quote) to avoid the need to escape special
153-
# chars as this is a regex
152+
# We ignore warnings about protobuf gencode version being one version older
153+
# than the current version, as this is supported by protobuf, and we expect to
154+
# have such cases. If we go too far, we will get a proper error anyways.
155+
# We use a raw string (single quotes) to avoid the need to escape special
156+
# characters as this is a regex.
154157
'ignore:Protobuf gencode version .*exactly one major version older.*:UserWarning',
155158
]
159+
addopts = "-vv"
156160
testpaths = ["tests", "src"]
157161
asyncio_mode = "auto"
158162
asyncio_default_fixture_loop_scope = "function"
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# License: MIT
2+
# Copyright © 2025 Frequenz Energy-as-a-Service GmbH
3+
4+
"""Conversion of protobuf int enums to Python enums."""
5+
6+
import enum
7+
from typing import Literal, TypeVar, overload
8+
9+
EnumT = TypeVar("EnumT", bound=enum.Enum)
10+
"""A type variable that is bound to an enum."""
11+
12+
13+
@overload
14+
def enum_from_proto(
15+
value: int, enum_type: type[EnumT], *, allow_invalid: Literal[False]
16+
) -> EnumT: ...
17+
18+
19+
@overload
20+
def enum_from_proto(
21+
value: int, enum_type: type[EnumT], *, allow_invalid: Literal[True] = True
22+
) -> EnumT | int: ...
23+
24+
25+
def enum_from_proto(
26+
value: int, enum_type: type[EnumT], *, allow_invalid: bool = True
27+
) -> EnumT | int:
28+
"""Convert a protobuf int enum value to a python enum.
29+
30+
Example:
31+
```python
32+
import enum
33+
34+
from proto import proto_pb2 # Just an example. pylint: disable=import-error
35+
36+
@enum.unique
37+
class SomeEnum(enum.Enum):
38+
# These values should match the protobuf enum values.
39+
UNSPECIFIED = 0
40+
SOME_VALUE = 1
41+
42+
enum_value = enum_from_proto(proto_pb2.SomeEnum.SOME_ENUM_SOME_VALUE, SomeEnum)
43+
# -> SomeEnum.SOME_VALUE
44+
45+
enum_value = enum_from_proto(42, SomeEnum)
46+
# -> 42
47+
48+
enum_value = enum_from_proto(
49+
proto_pb2.SomeEnum.SOME_ENUM_UNKNOWN_VALUE, SomeEnum, allow_invalid=False
50+
)
51+
# -> ValueError
52+
```
53+
54+
Args:
55+
value: The protobuf int enum value.
56+
enum_type: The python enum type to convert to.
57+
allow_invalid: If `True`, return the value as an `int` if the value is not
58+
a valid member of the enum (this allows for forward-compatibility with new
59+
enum values defined in the protocol but not added to the Python enum yet).
60+
If `False`, raise a `ValueError` if the value is not a valid member of the
61+
enum.
62+
63+
Returns:
64+
The resulting python enum value if the protobuf value is known, otherwise
65+
the input value converted to a plain `int`.
66+
67+
Raises:
68+
ValueError: If `allow_invalid` is `False` and the value is not a valid member
69+
of the enum.
70+
"""
71+
try:
72+
return enum_type(value)
73+
except ValueError:
74+
if allow_invalid:
75+
return value
76+
raise

src/frequenz/client/common/metric/__init__.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,18 @@
33

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

6-
from enum import Enum
6+
import enum
77
from typing import Self
88

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

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

1415

15-
class Metric(Enum):
16+
@enum.unique
17+
class Metric(enum.Enum):
1618
"""List of supported metrics.
1719
1820
AC energy metrics information:
@@ -140,6 +142,7 @@ class Metric(Enum):
140142
SENSOR_IRRADIANCE = PBMetric.METRIC_SENSOR_IRRADIANCE
141143

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

src/frequenz/client/common/microgrid/__init__.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,17 @@
22
# Copyright © 2023 Frequenz Energy-as-a-Service GmbH
33

44
"""Frequenz microgrid definition."""
5+
6+
from typing import final
7+
8+
from frequenz.core.id import BaseId
9+
10+
11+
@final
12+
class EnterpriseId(BaseId, str_prefix="EID"):
13+
"""A unique identifier for an enterprise account."""
14+
15+
16+
@final
17+
class MicrogridId(BaseId, str_prefix="MID"):
18+
"""A unique identifier for a microgrid."""

src/frequenz/client/common/microgrid/components/__init__.py

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22
# Copyright © 2022 Frequenz Energy-as-a-Service GmbH
33

44
"""Defines the components that can be used in a microgrid."""
5+
56
from __future__ import annotations
67

7-
from enum import Enum
8+
import enum
9+
from typing import final
810

911
# pylint: disable=no-name-in-module
1012
from frequenz.api.common.v1.microgrid.components.components_pb2 import (
@@ -16,11 +18,19 @@
1618
from frequenz.api.common.v1.microgrid.components.components_pb2 import (
1719
ComponentStateCode as PBComponentStateCode,
1820
)
21+
from frequenz.core.id import BaseId
22+
from typing_extensions import deprecated
1923

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

2226

23-
class ComponentCategory(Enum):
27+
@final
28+
class ComponentId(BaseId, str_prefix="CID"):
29+
"""A unique identifier for a microgrid component."""
30+
31+
32+
@enum.unique
33+
class ComponentCategory(enum.Enum):
2434
"""Possible types of microgrid component."""
2535

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

52+
CONVERTER = PBComponentCategory.COMPONENT_CATEGORY_CONVERTER
53+
"""A DC-DC converter."""
54+
4255
BATTERY = PBComponentCategory.COMPONENT_CATEGORY_BATTERY
4356
"""A storage system for electrical energy, used by inverters."""
4457

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

61+
CRYPTO_MINER = PBComponentCategory.COMPONENT_CATEGORY_CRYPTO_MINER
62+
"""A crypto miner."""
63+
64+
ELECTROLYZER = PBComponentCategory.COMPONENT_CATEGORY_ELECTROLYZER
65+
"""An electrolyzer for converting water into hydrogen and oxygen."""
66+
4867
CHP = PBComponentCategory.COMPONENT_CATEGORY_CHP
4968
"""A heat and power combustion plant (CHP stands for combined heat and power)."""
5069

70+
RELAY = PBComponentCategory.COMPONENT_CATEGORY_RELAY
71+
"""A relay.
72+
73+
Relays generally have two states: open (connected) and closed (disconnected).
74+
They are generally placed in front of a component, e.g., an inverter, to
75+
control whether the component is connected to the grid or not.
76+
"""
77+
78+
PRECHARGER = PBComponentCategory.COMPONENT_CATEGORY_PRECHARGER
79+
"""A precharge module.
80+
81+
Precharging involves gradually ramping up the DC voltage to prevent any
82+
potential damage to sensitive electrical components like capacitors.
83+
84+
While many inverters and batteries come equipped with in-built precharging
85+
mechanisms, some may lack this feature. In such cases, we need to use
86+
external precharging modules.
87+
"""
88+
89+
FUSE = PBComponentCategory.COMPONENT_CATEGORY_FUSE
90+
"""A fuse."""
91+
92+
VOLTAGE_TRANSFORMER = PBComponentCategory.COMPONENT_CATEGORY_VOLTAGE_TRANSFORMER
93+
"""A voltage transformer.
94+
95+
Voltage transformers are used to step up or step down the voltage, keeping
96+
the power somewhat constant by increasing or decreasing the current. If voltage is
97+
stepped up, current is stepped down, and vice versa.
98+
99+
Note:
100+
Voltage transformers have efficiency losses, so the output power is
101+
always less than the input power.
102+
"""
103+
104+
HVAC = PBComponentCategory.COMPONENT_CATEGORY_HVAC
105+
"""A Heating, Ventilation, and Air Conditioning (HVAC) system."""
106+
51107
@classmethod
108+
@deprecated("Use `frequenz.client.common.enum_proto.enum_from_proto` instead.")
52109
def from_proto(
53110
cls, component_category: PBComponentCategory.ValueType
54111
) -> ComponentCategory:
@@ -73,7 +130,8 @@ def to_proto(self) -> PBComponentCategory.ValueType:
73130
return self.value
74131

75132

76-
class ComponentStateCode(Enum):
133+
@enum.unique
134+
class ComponentStateCode(enum.Enum):
77135
"""All possible states of a microgrid component."""
78136

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

155213
@classmethod
214+
@deprecated("Use `frequenz.client.common.enum_proto.enum_from_proto` instead.")
156215
def from_proto(
157216
cls, component_state: PBComponentStateCode.ValueType
158217
) -> ComponentStateCode:
@@ -177,7 +236,8 @@ def to_proto(self) -> PBComponentStateCode.ValueType:
177236
return self.value
178237

179238

180-
class ComponentErrorCode(Enum):
239+
@enum.unique
240+
class ComponentErrorCode(enum.Enum):
181241
"""All possible errors that can occur across all microgrid component categories."""
182242

183243
UNSPECIFIED = PBComponentErrorCode.COMPONENT_ERROR_CODE_UNSPECIFIED
@@ -330,6 +390,7 @@ class ComponentErrorCode(Enum):
330390
times."""
331391

332392
@classmethod
393+
@deprecated("Use `frequenz.client.common.enum_proto.enum_from_proto` instead.")
333394
def from_proto(
334395
cls, component_error_code: PBComponentErrorCode.ValueType
335396
) -> ComponentErrorCode:
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# License: MIT
2+
# Copyright © 2025 Frequenz Energy-as-a-Service GmbH
3+
4+
"""Microgrid sensors."""
5+
6+
from typing import final
7+
8+
from frequenz.core.id import BaseId
9+
10+
11+
@final
12+
class SensorId(BaseId, str_prefix="SID"):
13+
"""A unique identifier for a microgrid sensor."""

tests/microgrid/test_ids.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# License: MIT
2+
# Copyright © 2025 Frequenz Energy-as-a-Service GmbH
3+
4+
"""Tests for microgrid-related IDs."""
5+
6+
import pytest
7+
from frequenz.core.id import BaseId
8+
9+
from frequenz.client.common.microgrid import EnterpriseId, MicrogridId
10+
from frequenz.client.common.microgrid.components import ComponentId
11+
from frequenz.client.common.microgrid.sensors import SensorId
12+
13+
14+
@pytest.mark.parametrize(
15+
"id_class, prefix",
16+
[
17+
(EnterpriseId, "EID"),
18+
(MicrogridId, "MID"),
19+
(ComponentId, "CID"),
20+
(SensorId, "SID"),
21+
],
22+
)
23+
def test_string_representation(id_class: type[BaseId], prefix: str) -> None:
24+
"""Test string representation of IDs."""
25+
_id = id_class(123)
26+
27+
assert str(_id) == f"{prefix}123"
28+
assert repr(_id) == f"{id_class.__name__}(123)"

0 commit comments

Comments
 (0)