Skip to content

Commit 3a7a39f

Browse files
authored
Merge pull request #38 from eduardiazf/feat/electrial-components
feat: ElectricalComponent
2 parents 1cf30d2 + dba649c commit 3a7a39f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

61 files changed

+3720
-589
lines changed

RELEASE_NOTES.md

Lines changed: 13 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,43 +2,24 @@
22

33
## Summary
44

5-
This release introduces a complete Assets API client with CLI support for interacting with Frequenz microgrid assets, including comprehensive error handling and type safety.
6-
7-
## Upgrading
8-
9-
**Breaking Changes:**
10-
11-
- Added new required dependencies: `frequenz-api-assets`, `frequenz-api-common`, `frequenz-client-base`, `grpcio`
12-
13-
**CLI Support:**
14-
Install with `pip install "frequenz-client-assets[cli]"` for command-line functionality.
5+
This release introduces a Assets API client with CLI support for interacting with Frequenz microgrid assets. It provides comprehensive electrical components functionality including batteries, EV chargers, inverters, and grid connection points, with enhanced type safety and error handling.
156

167
## New Features
178

18-
**Assets API Client:**
19-
20-
- Complete gRPC client for Frequenz Assets API
21-
- Extends `BaseApiClient` for authentication and connection management
22-
- `get_microgrid_details()` method for retrieving microgrid information
9+
* **Assets API Client**:
10+
* `list_electrical_components()` method for retrieving electrical components in a microgrid
2311

24-
**Command-Line Interface:**
12+
* **Electrical Components Support**: Comprehensive data classes for electrical components
13+
* `ElectricalComponent` with category-specific information for batteries, EV chargers, inverters, grid connection points, and power transformers
14+
* Battery types: Li-ion, Na-ion with proper enum mapping
15+
* EV charger types: AC, DC, Hybrid charging support
16+
* Operational lifetime tracking and metric configuration bounds
2517

26-
- `python -m frequenz.client.assets microgrid <id>` command
27-
- Environment variable support for API credentials
28-
- JSON output formatting
18+
* **Command-Line Interface**:
19+
* `assets-cli electrical-components <microgrid-id>` command
2920

30-
**Type System:**
31-
32-
- `Microgrid`, `DeliveryArea`, and `Location` data classes
33-
- Protobuf integration with proper type safety
34-
35-
**Exception Handling:**
36-
37-
- Custom exception hierarchy (`AssetsApiError`, `NotFoundError`, `AuthenticationError`, `ServiceUnavailableError`)
38-
- JSON serialization support for error responses
21+
* **Type System**: Enhanced data classes with protobuf integration
22+
* `Microgrid`, `DeliveryArea`, `Location`, and comprehensive electrical component types
23+
* Proper enum mapping: `BatteryType`, `EvChargerType`, `InverterType`, `Metric`
3924

4025
## Bug Fixes
41-
42-
- Improved dependency management with optional dependency groups
43-
- Enhanced gRPC error handling and type safety
44-
- Cleaned up deprecated code

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ dependencies = [
4040
"frequenz-api-assets @ git+https://github.com/frequenz-floss/[email protected]",
4141
"frequenz-api-common >= 0.8.0, < 1",
4242
"frequenz-client-base >= 0.11.0, < 0.12.0",
43-
"frequenz-client-common >= 0.3.2, < 0.4.0",
43+
"frequenz-client-common >= 0.3.6, < 0.4.0",
4444
"grpcio >= 1.73.1, < 2",
4545
]
4646
dynamic = ["version"]
@@ -78,6 +78,7 @@ dev-mypy = [
7878
"mypy == 1.17.1",
7979
"grpc-stubs == 1.53.0.6",
8080
"types-Markdown == 3.8.0.20250809",
81+
"types-protobuf == 6.30.2.20250516",
8182
# For checking the noxfile, docs/ script, and tests
8283
"frequenz-client-assets[dev-mkdocs,dev-noxfile,dev-pytest,cli]",
8384
]

src/frequenz/client/assets/__init__.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,15 @@
44
"""Assets API client."""
55

66
from ._client import AssetsApiClient
7+
from ._delivery_area import DeliveryArea, EnergyMarketCodeType
8+
from ._location import Location
9+
from ._microgrid import Microgrid, MicrogridStatus
710

8-
__all__ = ["AssetsApiClient"]
11+
__all__ = [
12+
"AssetsApiClient",
13+
"DeliveryArea",
14+
"EnergyMarketCodeType",
15+
"Microgrid",
16+
"MicrogridStatus",
17+
"Location",
18+
]

src/frequenz/client/assets/_client.py

Lines changed: 65 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,36 +10,60 @@
1010
from __future__ import annotations
1111

1212
from frequenz.api.assets.v1 import assets_pb2, assets_pb2_grpc
13+
from frequenz.client.base import channel
1314
from frequenz.client.base.client import BaseApiClient, call_stub_method
1415

15-
from frequenz.client.assets.types import Microgrid
16+
from frequenz.client.assets.electrical_component._electrical_component import (
17+
ElectricalComponent,
18+
)
1619

20+
from ._microgrid import Microgrid
21+
from ._microgrid_proto import microgrid_from_proto
22+
from .electrical_component._electrical_component_proto import electrical_component_proto
1723
from .exceptions import ClientNotConnected
1824

25+
DEFAULT_GRPC_CALL_TIMEOUT = 60.0
26+
"""The default timeout for gRPC calls made by this client (in seconds)."""
1927

20-
class AssetsApiClient(BaseApiClient[assets_pb2_grpc.PlatformAssetsStub]):
28+
29+
class AssetsApiClient(
30+
BaseApiClient[assets_pb2_grpc.PlatformAssetsStub]
31+
): # pylint: disable=too-many-arguments
2132
"""A client for the Assets API."""
2233

2334
def __init__(
2435
self,
2536
server_url: str,
26-
auth_key: str | None,
27-
sign_secret: str | None,
37+
*,
38+
auth_key: str | None = None,
39+
sign_secret: str | None = None,
40+
channel_defaults: channel.ChannelOptions = channel.ChannelOptions(),
2841
connect: bool = True,
2942
) -> None:
3043
"""
3144
Initialize the AssetsApiClient.
3245
3346
Args:
34-
server_url: The URL of the server to connect to.
35-
auth_key: The API key to use when connecting to the service.
36-
sign_secret: The secret to use when creating message HMAC.
37-
connect: Whether to connect to the server as soon as a client instance is created.
47+
server_url: The location of the microgrid API server in the form of a URL.
48+
The following format is expected:
49+
"grpc://hostname{:`port`}{?ssl=`ssl`}",
50+
where the `port` should be an int between 0 and 65535 (defaulting to
51+
9090) and `ssl` should be a boolean (defaulting to `true`).
52+
For example: `grpc://localhost:1090?ssl=true`.
53+
auth_key: The authentication key to use for the connection.
54+
sign_secret: The secret to use for signing requests.
55+
channel_defaults: The default options use to create the channel when not
56+
specified in the URL.
57+
connect: Whether to connect to the server as soon as a client instance is
58+
created. If `False`, the client will not connect to the server until
59+
[connect()][frequenz.client.base.client.BaseApiClient.connect] is
60+
called.
3861
"""
3962
super().__init__(
4063
server_url,
4164
assets_pb2_grpc.PlatformAssetsStub,
4265
connect=connect,
66+
channel_defaults=channel_defaults,
4367
auth_key=auth_key,
4468
sign_secret=sign_secret,
4569
)
@@ -61,7 +85,7 @@ def stub(self) -> assets_pb2_grpc.PlatformAssetsAsyncStub:
6185
# use the async stub, so we cast the sync stub to the async stub.
6286
return self._stub # type: ignore
6387

64-
async def get_microgrid_details( # noqa: DOC502 (raises ApiClientError indirectly)
88+
async def get_microgrid( # noqa: DOC502 (raises ApiClientError indirectly)
6589
self, microgrid_id: int
6690
) -> Microgrid:
6791
"""
@@ -77,11 +101,40 @@ async def get_microgrid_details( # noqa: DOC502 (raises ApiClientError indirect
77101
ApiClientError: If there are any errors communicating with the Assets API,
78102
most likely a subclass of [GrpcError][frequenz.client.base.exception.GrpcError].
79103
"""
80-
request = assets_pb2.GetMicrogridRequest(microgrid_id=microgrid_id)
81104
response = await call_stub_method(
82105
self,
83-
lambda: self.stub.GetMicrogrid(request),
106+
lambda: self.stub.GetMicrogrid(
107+
assets_pb2.GetMicrogridRequest(microgrid_id=microgrid_id),
108+
timeout=DEFAULT_GRPC_CALL_TIMEOUT,
109+
),
84110
method_name="GetMicrogrid",
85111
)
86112

87-
return Microgrid.from_protobuf(response.microgrid)
113+
return microgrid_from_proto(response.microgrid)
114+
115+
async def list_microgrid_electrical_components(
116+
self, microgrid_id: int
117+
) -> list[ElectricalComponent]:
118+
"""
119+
Get the electrical components of a microgrid.
120+
121+
Args:
122+
microgrid_id: The ID of the microgrid to get the electrical components of.
123+
124+
Returns:
125+
The electrical components of the microgrid.
126+
"""
127+
response = await call_stub_method(
128+
self,
129+
lambda: self.stub.ListMicrogridElectricalComponents(
130+
assets_pb2.ListMicrogridElectricalComponentsRequest(
131+
microgrid_id=microgrid_id,
132+
),
133+
timeout=DEFAULT_GRPC_CALL_TIMEOUT,
134+
),
135+
method_name="ListMicrogridElectricalComponents",
136+
)
137+
138+
return [
139+
electrical_component_proto(component) for component in response.components
140+
]
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.v1alpha8.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.v1alpha8.grid import delivery_area_pb2
9+
from frequenz.client.common import enum_proto
10+
11+
from ._delivery_area import DeliveryArea, EnergyMarketCodeType
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_proto.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)

0 commit comments

Comments
 (0)