Skip to content

Commit f0f2765

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 1d89b82 commit f0f2765

File tree

4 files changed

+300
-0
lines changed

4 files changed

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