Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
2 changes: 1 addition & 1 deletion .github/workflows/ci-pr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
uses: frequenz-floss/[email protected]

- name: Fetch sources
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
submodules: true

Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ jobs:
uses: frequenz-floss/[email protected]

- name: Fetch sources
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
submodules: true

Expand Down Expand Up @@ -177,7 +177,7 @@ jobs:
uses: frequenz-floss/[email protected]

- name: Fetch sources
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
submodules: true

Expand Down Expand Up @@ -213,7 +213,7 @@ jobs:
uses: frequenz-floss/[email protected]

- name: Fetch sources
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
submodules: true

Expand Down
45 changes: 13 additions & 32 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,43 +2,24 @@

## Summary

This release introduces a complete Assets API client with CLI support for interacting with Frequenz microgrid assets, including comprehensive error handling and type safety.

## Upgrading

**Breaking Changes:**

- Added new required dependencies: `frequenz-api-assets`, `frequenz-api-common`, `frequenz-client-base`, `grpcio`

**CLI Support:**
Install with `pip install "frequenz-client-assets[cli]"` for command-line functionality.
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.

## New Features

**Assets API Client:**

- Complete gRPC client for Frequenz Assets API
- Extends `BaseApiClient` for authentication and connection management
- `get_microgrid_details()` method for retrieving microgrid information
* **Assets API Client**:
* `list_electrical_components()` method for retrieving electrical components in a microgrid

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

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

**Type System:**

- `Microgrid`, `DeliveryArea`, and `Location` data classes
- Protobuf integration with proper type safety

**Exception Handling:**

- Custom exception hierarchy (`AssetsApiError`, `NotFoundError`, `AuthenticationError`, `ServiceUnavailableError`)
- JSON serialization support for error responses
* **Type System**: Enhanced data classes with protobuf integration
* `Microgrid`, `DeliveryArea`, `Location`, and comprehensive electrical component types
* Proper enum mapping: `BatteryType`, `EvChargerType`, `InverterType`, `Metric`

## Bug Fixes

- Improved dependency management with optional dependency groups
- Enhanced gRPC error handling and type safety
- Cleaned up deprecated code
25 changes: 13 additions & 12 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
[build-system]
requires = [
"setuptools == 80.9.0",
"setuptools_scm[toml] == 8.3.1",
"setuptools_scm[toml] == 9.2.0",
"frequenz-repo-config[lib] == 0.13.5",
]
build-backend = "setuptools.build_meta"
Expand Down Expand Up @@ -40,7 +40,7 @@ dependencies = [
"frequenz-api-assets @ git+https://github.com/frequenz-floss/[email protected]",
"frequenz-api-common >= 0.8.0, < 1",
"frequenz-client-base >= 0.11.0, < 0.12.0",
"frequenz-client-common >= 0.3.2, < 0.4.0",
"frequenz-client-common >= 0.3.6, < 0.4.0",
"grpcio >= 1.73.1, < 2",
]
dynamic = ["version"]
Expand All @@ -53,7 +53,7 @@ name = "Frequenz Energy-as-a-Service GmbH"
email = "[email protected]"

[project.optional-dependencies]
cli = ["asyncclick == 8.1.8"]
cli = ["asyncclick == 8.2.2.2"]
dev-flake8 = [
"flake8 == 7.3.0",
"flake8-docstrings == 1.7.0",
Expand All @@ -68,16 +68,17 @@ dev-mkdocs = [
"mike == 2.1.3",
"mkdocs-gen-files == 0.5.0",
"mkdocs-literate-nav == 0.6.2",
"mkdocs-macros-plugin == 1.3.7",
"mkdocs-material == 9.6.15",
"mkdocstrings[python] == 0.29.1",
"mkdocstrings-python == 1.16.12",
"mkdocs-macros-plugin == 1.3.9",
"mkdocs-material == 9.6.17",
"mkdocstrings[python] == 0.30.0",
"mkdocstrings-python == 1.17.0",
"frequenz-repo-config[lib] == 0.13.5",
]
dev-mypy = [
"mypy == 1.16.1",
"mypy == 1.17.1",
"grpc-stubs == 1.53.0.6",
"types-Markdown == 3.8.0.20250415",
"types-Markdown == 3.8.0.20250809",
"types-protobuf == 6.30.2.20250516",
# For checking the noxfile, docs/ script, and tests
"frequenz-client-assets[dev-mkdocs,dev-noxfile,dev-pytest,cli]",
]
Expand All @@ -89,11 +90,11 @@ dev-pylint = [
]
dev-pytest = [
"pytest == 8.4.1",
"pylint == 3.3.7", # We need this to check for the examples
"pylint == 3.3.8", # We need this to check for the examples
"frequenz-repo-config[extra-lint-examples] == 0.13.5",
"pytest-mock == 3.14.1",
"pytest-asyncio == 1.0.0",
"async-solipsism == 0.7",
"pytest-asyncio == 1.1.0",
"async-solipsism == 0.8",
]
dev = [
"frequenz-client-assets[dev-mkdocs,dev-flake8,dev-formatting,dev-mkdocs,dev-mypy,dev-noxfile,dev-pylint,dev-pytest]",
Expand Down
12 changes: 11 additions & 1 deletion src/frequenz/client/assets/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,15 @@
"""Assets API client."""

from ._client import AssetsApiClient
from ._delivery_area import DeliveryArea, EnergyMarketCodeType
from ._location import Location
from ._microgrid import Microgrid, MicrogridStatus

__all__ = ["AssetsApiClient"]
__all__ = [
"AssetsApiClient",
"DeliveryArea",
"EnergyMarketCodeType",
"Microgrid",
"MicrogridStatus",
"Location",
]
77 changes: 65 additions & 12 deletions src/frequenz/client/assets/_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,36 +10,60 @@
from __future__ import annotations

from frequenz.api.assets.v1 import assets_pb2, assets_pb2_grpc
from frequenz.client.base import channel
from frequenz.client.base.client import BaseApiClient, call_stub_method

from frequenz.client.assets.types import Microgrid
from frequenz.client.assets.electrical_component._electrical_component import (
ElectricalComponent,
)

from ._microgrid import Microgrid
from ._microgrid_proto import microgrid_from_proto
from .electrical_component._electrical_component_proto import electrical_component_proto
from .exceptions import ClientNotConnected

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

class AssetsApiClient(BaseApiClient[assets_pb2_grpc.PlatformAssetsStub]):

class AssetsApiClient(
BaseApiClient[assets_pb2_grpc.PlatformAssetsStub]
): # pylint: disable=too-many-arguments
"""A client for the Assets API."""

def __init__(
self,
server_url: str,
auth_key: str | None,
sign_secret: str | None,
*,
auth_key: str | None = None,
sign_secret: str | None = None,
channel_defaults: channel.ChannelOptions = channel.ChannelOptions(),
connect: bool = True,
) -> None:
"""
Initialize the AssetsApiClient.

Args:
server_url: The URL of the server to connect to.
auth_key: The API key to use when connecting to the service.
sign_secret: The secret to use when creating message HMAC.
connect: Whether to connect to the server as soon as a client instance is created.
server_url: The location of the microgrid API server in the form of a URL.
The following format is expected:
"grpc://hostname{:`port`}{?ssl=`ssl`}",
where the `port` should be an int between 0 and 65535 (defaulting to
9090) and `ssl` should be a boolean (defaulting to `true`).
For example: `grpc://localhost:1090?ssl=true`.
auth_key: The authentication key to use for the connection.
sign_secret: The secret to use for signing requests.
channel_defaults: The default options use to create the channel when not
specified in the URL.
connect: Whether to connect to the server as soon as a client instance is
created. If `False`, the client will not connect to the server until
[connect()][frequenz.client.base.client.BaseApiClient.connect] is
called.
"""
super().__init__(
server_url,
assets_pb2_grpc.PlatformAssetsStub,
connect=connect,
channel_defaults=channel_defaults,
auth_key=auth_key,
sign_secret=sign_secret,
)
Expand All @@ -61,7 +85,7 @@ def stub(self) -> assets_pb2_grpc.PlatformAssetsAsyncStub:
# use the async stub, so we cast the sync stub to the async stub.
return self._stub # type: ignore

async def get_microgrid_details( # noqa: DOC502 (raises ApiClientError indirectly)
async def get_microgrid( # noqa: DOC502 (raises ApiClientError indirectly)
self, microgrid_id: int
) -> Microgrid:
"""
Expand All @@ -77,11 +101,40 @@ async def get_microgrid_details( # noqa: DOC502 (raises ApiClientError indirect
ApiClientError: If there are any errors communicating with the Assets API,
most likely a subclass of [GrpcError][frequenz.client.base.exception.GrpcError].
"""
request = assets_pb2.GetMicrogridRequest(microgrid_id=microgrid_id)
response = await call_stub_method(
self,
lambda: self.stub.GetMicrogrid(request),
lambda: self.stub.GetMicrogrid(
assets_pb2.GetMicrogridRequest(microgrid_id=microgrid_id),
timeout=DEFAULT_GRPC_CALL_TIMEOUT,
),
method_name="GetMicrogrid",
)

return Microgrid.from_protobuf(response.microgrid)
return microgrid_from_proto(response.microgrid)

async def list_microgrid_electrical_components(
self, microgrid_id: int
) -> list[ElectricalComponent]:
"""
Get the electrical components of a microgrid.

Args:
microgrid_id: The ID of the microgrid to get the electrical components of.

Returns:
The electrical components of the microgrid.
"""
response = await call_stub_method(
self,
lambda: self.stub.ListMicrogridElectricalComponents(
assets_pb2.ListMicrogridElectricalComponentsRequest(
microgrid_id=microgrid_id,
),
timeout=DEFAULT_GRPC_CALL_TIMEOUT,
),
method_name="ListMicrogridElectricalComponents",
)

return [
electrical_component_proto(component) for component in response.components
]
89 changes: 89 additions & 0 deletions src/frequenz/client/assets/_delivery_area.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# License: MIT
# Copyright © 2025 Frequenz Energy-as-a-Service GmbH

"""Delivery area information for the energy market."""

import enum
from dataclasses import dataclass

from frequenz.api.common.v1alpha8.grid import delivery_area_pb2


@enum.unique
class EnergyMarketCodeType(enum.Enum):
"""The identification code types used in the energy market.

CodeType specifies the type of identification code used for uniquely
identifying various entities such as delivery areas, market participants,
and grid components within the energy market.

This enumeration aims to
offer compatibility across different jurisdictional standards.

Note: Understanding Code Types
Different regions or countries may have their own standards for uniquely
identifying various entities within the energy market. For example, in
Europe, the Energy Identification Code (EIC) is commonly used for this
purpose.

Note: Extensibility
New code types can be added to this enum to accommodate additional regional
standards, enhancing the API's adaptability.

Danger: Validation Required
The chosen code type should correspond correctly with the `code` field in
the relevant message objects, such as `DeliveryArea` or `Counterparty`.
Failure to match the code type with the correct code could lead to
processing errors.
"""

UNSPECIFIED = delivery_area_pb2.ENERGY_MARKET_CODE_TYPE_UNSPECIFIED
"""Unspecified type. This value is a placeholder and should not be used."""

EUROPE_EIC = delivery_area_pb2.ENERGY_MARKET_CODE_TYPE_EUROPE_EIC
"""European Energy Identification Code Standard."""

US_NERC = delivery_area_pb2.ENERGY_MARKET_CODE_TYPE_US_NERC
"""North American Electric Reliability Corporation identifiers."""


@dataclass(frozen=True, kw_only=True)
class DeliveryArea:
"""A geographical or administrative region where electricity deliveries occur.

DeliveryArea represents the geographical or administrative region, usually defined
and maintained by a Transmission System Operator (TSO), where electricity deliveries
for a contract occur.

The concept is important to energy trading as it delineates the agreed-upon delivery
location. Delivery areas can have different codes based on the jurisdiction in
which they operate.

Note: Jurisdictional Differences
This is typically represented by specific codes according to local jurisdiction.

In Europe, this is represented by an
[EIC](https://en.wikipedia.org/wiki/Energy_Identification_Code) (Energy
Identification Code). [List of
EICs](https://www.entsoe.eu/data/energy-identification-codes-eic/eic-approved-codes/).
"""

code: str | None
"""The code representing the unique identifier for the delivery area."""

code_type: EnergyMarketCodeType | int
"""Type of code used for identifying the delivery area itself.

This code could be extended in the future, in case an unknown code type is
encountered, a plain integer value is used to represent it.
"""

def __str__(self) -> str:
"""Return a human-readable string representation of this instance."""
code = self.code or "<NO CODE>"
code_type = (
f"type={self.code_type}"
if isinstance(self.code_type, int)
else self.code_type.name
)
return f"{code}[{code_type}]"
Loading