Skip to content

Commit cfa03b0

Browse files
committed
Add DeliveryArea and EnergyMarketCodeType
This will be used to retrieve the microgrid information. Signed-off-by: Leandro Lucarella <[email protected]>
1 parent 823c4a9 commit cfa03b0

File tree

4 files changed

+298
-0
lines changed

4 files changed

+298
-0
lines changed

src/frequenz/client/microgrid/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99

1010
from ._client import MicrogridApiClient
11+
from ._delivery_area import DeliveryArea, EnergyMarketCodeType
1112
from ._exception import (
1213
ApiClientError,
1314
ClientNotConnected,
@@ -38,6 +39,8 @@
3839
"ClientNotConnected",
3940
"ComponentId",
4041
"DataLoss",
42+
"DeliveryArea",
43+
"EnergyMarketCodeType",
4144
"EnterpriseId",
4245
"EntityAlreadyExists",
4346
"EntityNotFound",
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# License: MIT
2+
# Copyright © 2025 Frequenz Energy-as-a-Service GmbH
3+
4+
"""Delivery area information for the energy market."""
5+
6+
import enum
7+
from dataclasses import dataclass
8+
9+
10+
@enum.unique
11+
class EnergyMarketCodeType(enum.Enum):
12+
"""The identification code types used in the energy market.
13+
14+
CodeType specifies the type of identification code used for uniquely
15+
identifying various entities such as delivery areas, market participants,
16+
and grid components within the energy market.
17+
18+
This enumeration aims to
19+
offer compatibility across different jurisdictional standards.
20+
21+
Note: Understanding Code Types
22+
Different regions or countries may have their own standards for uniquely
23+
identifying various entities within the energy market. For example, in
24+
Europe, the Energy Identification Code (EIC) is commonly used for this
25+
purpose.
26+
27+
Note: Extensibility
28+
New code types can be added to this enum to accommodate additional regional
29+
standards, enhancing the API's adaptability.
30+
31+
Danger: Validation Required
32+
The chosen code type should correspond correctly with the `code` field in
33+
the relevant message objects, such as `DeliveryArea` or `Counterparty`.
34+
Failure to match the code type with the correct code could lead to
35+
processing errors.
36+
"""
37+
38+
UNSPECIFIED = 0
39+
"""Unspecified type. This value is a placeholder and should not be used."""
40+
41+
EUROPE_EIC = 1
42+
"""European Energy Identification Code Standard."""
43+
44+
US_NERC = 2
45+
"""North American Electric Reliability Corporation identifiers."""
46+
47+
48+
@dataclass(frozen=True, kw_only=True)
49+
class DeliveryArea:
50+
"""A geographical or administrative region where electricity deliveries occur.
51+
52+
DeliveryArea represents the geographical or administrative region, usually defined
53+
and maintained by a Transmission System Operator (TSO), where electricity deliveries
54+
for a contract occur.
55+
56+
The concept is important to energy trading as it delineates the agreed-upon delivery
57+
location. Delivery areas can have different codes based on the jurisdiction in
58+
which they operate.
59+
60+
Note: Jurisdictional Differences
61+
This is typically represented by specific codes according to local jurisdiction.
62+
63+
In Europe, this is represented by an
64+
[EIC](https://en.wikipedia.org/wiki/Energy_Identification_Code) (Energy
65+
Identification Code). [List of
66+
EICs](https://www.entsoe.eu/data/energy-identification-codes-eic/eic-approved-codes/).
67+
"""
68+
69+
code: str | None
70+
"""The code representing the unique identifier for the delivery area."""
71+
72+
code_type: EnergyMarketCodeType | int
73+
"""Type of code used for identifying the delivery area itself.
74+
75+
This code could be extended in the future, in case an unknown code type is
76+
encountered, a plain integer value is used to represent it.
77+
"""
78+
79+
def __str__(self) -> str:
80+
"""Return a human-readable string representation of this instance."""
81+
code = self.code or "<NO CODE>"
82+
code_type = (
83+
f"type={self.code_type}"
84+
if isinstance(self.code_type, int)
85+
else self.code_type.name
86+
)
87+
return f"{code}[{code_type}]"
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# License: MIT
2+
# Copyright © 2025 Frequenz Energy-as-a-Service GmbH
3+
4+
"""Loading of DeliveryArea objects from protobuf messages."""
5+
6+
import logging
7+
8+
from frequenz.api.common.v1.grid import delivery_area_pb2
9+
10+
from ._delivery_area import DeliveryArea, EnergyMarketCodeType
11+
from ._util import enum_from_proto
12+
13+
_logger = logging.getLogger(__name__)
14+
15+
16+
def delivery_area_from_proto(message: delivery_area_pb2.DeliveryArea) -> DeliveryArea:
17+
"""Convert a protobuf delivery area message to a delivery area object.
18+
19+
Args:
20+
message: The protobuf message to convert.
21+
22+
Returns:
23+
The resulting delivery area object.
24+
"""
25+
issues: list[str] = []
26+
27+
code = message.code or None
28+
if code is None:
29+
issues.append("code is empty")
30+
31+
code_type = enum_from_proto(message.code_type, EnergyMarketCodeType)
32+
if code_type is EnergyMarketCodeType.UNSPECIFIED:
33+
issues.append("code_type is unspecified")
34+
elif isinstance(code_type, int):
35+
issues.append("code_type is unrecognized")
36+
37+
if issues:
38+
_logger.warning(
39+
"Found issues in delivery area: %s | Protobuf message:\n%s",
40+
", ".join(issues),
41+
message,
42+
)
43+
44+
return DeliveryArea(code=code, code_type=code_type)

tests/test_delivery_area.py

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
# License: MIT
2+
# Copyright © 2025 Frequenz Energy-as-a-Service GmbH
3+
4+
"""Tests for the DeliveryArea class and its protobuf conversion."""
5+
6+
from dataclasses import dataclass
7+
8+
import pytest
9+
from frequenz.api.common.v1.grid import delivery_area_pb2
10+
11+
from frequenz.client.microgrid import DeliveryArea, EnergyMarketCodeType
12+
from frequenz.client.microgrid._delivery_area_proto import delivery_area_from_proto
13+
14+
15+
@dataclass(frozen=True, kw_only=True)
16+
class _DeliveryAreaTestCase:
17+
"""Test case for DeliveryArea creation."""
18+
19+
name: str
20+
"""Description of the test case."""
21+
22+
code: str | None
23+
"""The code to use for the delivery area."""
24+
25+
code_type: EnergyMarketCodeType | int
26+
"""The type of code being used."""
27+
28+
expected_str: str
29+
"""Expected string representation."""
30+
31+
32+
@dataclass(frozen=True, kw_only=True)
33+
class _ProtoConversionTestCase:
34+
"""Test case for protobuf conversion."""
35+
36+
name: str
37+
"""Description of the test case."""
38+
39+
code: str | None
40+
"""The code to set in the protobuf message."""
41+
42+
code_type: int
43+
"""The code type to set in the protobuf message."""
44+
45+
expected_code: str | None
46+
"""Expected code in the resulting DeliveryArea."""
47+
48+
expected_code_type: EnergyMarketCodeType | int
49+
"""Expected code type in the resulting DeliveryArea."""
50+
51+
expect_warning: bool
52+
"""Whether to expect a warning during conversion."""
53+
54+
55+
@pytest.mark.parametrize(
56+
"case",
57+
[
58+
_DeliveryAreaTestCase(
59+
name="valid_EIC_code",
60+
code="10Y1001A1001A450",
61+
code_type=EnergyMarketCodeType.EUROPE_EIC,
62+
expected_str="10Y1001A1001A450[EUROPE_EIC]",
63+
),
64+
_DeliveryAreaTestCase(
65+
name="valid_NERC_code",
66+
code="PJM",
67+
code_type=EnergyMarketCodeType.US_NERC,
68+
expected_str="PJM[US_NERC]",
69+
),
70+
_DeliveryAreaTestCase(
71+
name="no_code",
72+
code=None,
73+
code_type=EnergyMarketCodeType.EUROPE_EIC,
74+
expected_str="<NO CODE>[EUROPE_EIC]",
75+
),
76+
_DeliveryAreaTestCase(
77+
name="unspecified_code_type",
78+
code="TEST",
79+
code_type=EnergyMarketCodeType.UNSPECIFIED,
80+
expected_str="TEST[UNSPECIFIED]",
81+
),
82+
_DeliveryAreaTestCase(
83+
name="unknown_code_type",
84+
code="TEST",
85+
code_type=999,
86+
expected_str="TEST[type=999]",
87+
),
88+
],
89+
ids=lambda case: case.name,
90+
)
91+
def test_creation(case: _DeliveryAreaTestCase) -> None:
92+
"""Test creating DeliveryArea instances with various parameters."""
93+
area = DeliveryArea(code=case.code, code_type=case.code_type)
94+
assert area.code == case.code
95+
assert area.code_type == case.code_type
96+
assert str(area) == case.expected_str
97+
98+
99+
@pytest.mark.parametrize(
100+
"case",
101+
[
102+
_ProtoConversionTestCase(
103+
name="valid_EIC_code",
104+
code="10Y1001A1001A450",
105+
code_type=delivery_area_pb2.EnergyMarketCodeType.ENERGY_MARKET_CODE_TYPE_EUROPE_EIC,
106+
expected_code="10Y1001A1001A450",
107+
expected_code_type=EnergyMarketCodeType.EUROPE_EIC,
108+
expect_warning=False,
109+
),
110+
_ProtoConversionTestCase(
111+
name="valid_NERC_code",
112+
code="PJM",
113+
code_type=delivery_area_pb2.EnergyMarketCodeType.ENERGY_MARKET_CODE_TYPE_US_NERC,
114+
expected_code="PJM",
115+
expected_code_type=EnergyMarketCodeType.US_NERC,
116+
expect_warning=False,
117+
),
118+
_ProtoConversionTestCase(
119+
name="no_code",
120+
code=None,
121+
code_type=delivery_area_pb2.EnergyMarketCodeType.ENERGY_MARKET_CODE_TYPE_EUROPE_EIC,
122+
expected_code=None,
123+
expected_code_type=EnergyMarketCodeType.EUROPE_EIC,
124+
expect_warning=True,
125+
),
126+
_ProtoConversionTestCase(
127+
name="unspecified_code_type",
128+
code="TEST",
129+
code_type=delivery_area_pb2.EnergyMarketCodeType.ENERGY_MARKET_CODE_TYPE_UNSPECIFIED,
130+
expected_code="TEST",
131+
expected_code_type=EnergyMarketCodeType.UNSPECIFIED,
132+
expect_warning=True,
133+
),
134+
_ProtoConversionTestCase(
135+
name="unknown_code_type",
136+
code="TEST",
137+
code_type=999,
138+
expected_code="TEST",
139+
expected_code_type=999,
140+
expect_warning=True,
141+
),
142+
],
143+
ids=lambda case: case.name,
144+
)
145+
def test_from_proto(
146+
caplog: pytest.LogCaptureFixture, case: _ProtoConversionTestCase
147+
) -> None:
148+
"""Test conversion from protobuf message to DeliveryArea."""
149+
# We do the type-ignore here because we want to test the case of an
150+
# arbitrary int too.
151+
proto = delivery_area_pb2.DeliveryArea(
152+
code=case.code or "", code_type=case.code_type # type: ignore[arg-type]
153+
)
154+
with caplog.at_level("WARNING"):
155+
area = delivery_area_from_proto(proto)
156+
157+
assert area.code == case.expected_code
158+
assert area.code_type == case.expected_code_type
159+
160+
if case.expect_warning:
161+
assert len(caplog.records) > 0
162+
assert "Found issues in delivery area" in caplog.records[0].message
163+
else:
164+
assert len(caplog.records) == 0

0 commit comments

Comments
 (0)