Skip to content

Update frequenz-api-microgrid to v0.15.1 #416

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Aug 4, 2023
Merged
2 changes: 1 addition & 1 deletion RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

## Upgrading

<!-- Here goes notes on how to upgrade from previous versions, including deprecations and what they should be replaced with -->
- Upgrade to microgrid API v0.15.1. If you're using any of the lower level microgrid interfaces, you will need to upgrade your code.

## New Features

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ classifiers = [
]
requires-python = ">= 3.11, < 4"
dependencies = [
"frequenz-api-microgrid >= 0.11.0, < 0.12.0",
"frequenz-api-microgrid >= 0.15.1, < 0.16.0",
# Make sure to update the mkdocs.yml file when
# changing the version
# (plugins.mkdocstrings.handlers.python.import)
Expand Down
16 changes: 12 additions & 4 deletions src/frequenz/sdk/actor/_data_sourcing/microgrid_api_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,15 +82,23 @@ def get_channel_name(self) -> str:
ComponentMetricId.SOC_LOWER_BOUND: lambda msg: msg.soc_lower_bound,
ComponentMetricId.SOC_UPPER_BOUND: lambda msg: msg.soc_upper_bound,
ComponentMetricId.CAPACITY: lambda msg: msg.capacity,
ComponentMetricId.POWER_LOWER_BOUND: lambda msg: msg.power_lower_bound,
ComponentMetricId.POWER_UPPER_BOUND: lambda msg: msg.power_upper_bound,
ComponentMetricId.POWER_INCLUSION_LOWER_BOUND: lambda msg: (
msg.power_inclusion_lower_bound
),
ComponentMetricId.POWER_INCLUSION_UPPER_BOUND: lambda msg: (
msg.power_inclusion_upper_bound
),
ComponentMetricId.TEMPERATURE: lambda msg: msg.temperature,
}

_InverterDataMethods: Dict[ComponentMetricId, Callable[[InverterData], float]] = {
ComponentMetricId.ACTIVE_POWER: lambda msg: msg.active_power,
ComponentMetricId.ACTIVE_POWER_LOWER_BOUND: lambda msg: msg.active_power_lower_bound,
ComponentMetricId.ACTIVE_POWER_UPPER_BOUND: lambda msg: msg.active_power_upper_bound,
ComponentMetricId.ACTIVE_POWER_INCLUSION_LOWER_BOUND: lambda msg: (
msg.active_power_inclusion_lower_bound
),
ComponentMetricId.ACTIVE_POWER_INCLUSION_UPPER_BOUND: lambda msg: (
msg.active_power_inclusion_upper_bound
),
}

_EVChargerDataMethods: Dict[ComponentMetricId, Callable[[EVChargerData], float]] = {
Expand Down
3 changes: 3 additions & 0 deletions src/frequenz/sdk/actor/power_distributing/_battery_status.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,13 @@
from enum import Enum
from typing import Iterable, Optional, Set, TypeVar, Union

# pylint: disable=no-name-in-module
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any ideas why is this suddenly failing? It would be good to find out where this regressions comes from (and ideally fix it)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, I'm guessing because some protobuf dep version changed in the api repo. That's why I put them all in a single comment, so it can be removed easily once we figure out the issue 🤞🏽

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually have a theory now:

Pylint doesn't look at the type annotations, it just looks at the code other other modules to see if the desired functions are there. This works only if the names are defined in python code in the modules. But protoc has not been doing that for a while now, and is just using dynamic module creation during import, and we appear to have to just use the type hints to know what's going on. If you look at one of the generated files, this becomes clear, like .nox/pylint/lib/python3.11/site-packages/frequenz/api/microgrid/inverter_pb2.py

And it was working until now, because we were using a very old version of protoc in the api repo.

from frequenz.api.microgrid.battery_pb2 import ComponentState as BatteryComponentState
from frequenz.api.microgrid.battery_pb2 import RelayState as BatteryRelayState
from frequenz.api.microgrid.common_pb2 import ErrorLevel
from frequenz.api.microgrid.inverter_pb2 import ComponentState as InverterComponentState

# pylint: enable=no-name-in-module
from frequenz.channels import Receiver, Sender
from frequenz.channels.util import Timer, select, selected_from

Expand Down
22 changes: 14 additions & 8 deletions src/frequenz/sdk/actor/power_distributing/power_distributing.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,10 @@ def _get_upper_bound(self, batteries: abc.Set[int], include_broken: bool) -> flo
batteries, include_broken
)
return sum(
min(battery.power_upper_bound, inverter.active_power_upper_bound)
min(
battery.power_inclusion_upper_bound,
inverter.active_power_inclusion_upper_bound,
)
for battery, inverter in pairs_data
)

Expand All @@ -266,7 +269,10 @@ def _get_lower_bound(self, batteries: abc.Set[int], include_broken: bool) -> flo
batteries, include_broken
)
return sum(
max(battery.power_lower_bound, inverter.active_power_lower_bound)
max(
battery.power_inclusion_lower_bound,
inverter.active_power_inclusion_lower_bound,
)
for battery, inverter in pairs_data
)

Expand Down Expand Up @@ -622,10 +628,10 @@ def _get_battery_inverter_data(
return None

replaceable_metrics = [
battery_data.power_lower_bound,
battery_data.power_upper_bound,
inverter_data.active_power_lower_bound,
inverter_data.active_power_upper_bound,
battery_data.power_inclusion_lower_bound,
battery_data.power_inclusion_upper_bound,
inverter_data.active_power_inclusion_lower_bound,
inverter_data.active_power_inclusion_upper_bound,
]

# If all values are ok then return them.
Expand All @@ -637,8 +643,8 @@ def _get_battery_inverter_data(
# Replace NaN with the corresponding value in the adjacent component.
# If both metrics are None, return None to ignore this battery.
replaceable_pairs = [
("power_lower_bound", "active_power_lower_bound"),
("power_upper_bound", "active_power_upper_bound"),
("power_inclusion_lower_bound", "active_power_inclusion_lower_bound"),
("power_inclusion_upper_bound", "active_power_inclusion_upper_bound"),
]

battery_new_metrics = {}
Expand Down
48 changes: 15 additions & 33 deletions src/frequenz/sdk/microgrid/client/_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

import asyncio
import logging
import math
from abc import ABC, abstractmethod
from typing import (
Any,
Expand All @@ -20,11 +19,11 @@
)

import grpc
from frequenz.api.microgrid import common_pb2 as common_pb
from frequenz.api.common import components_pb2 as components_pb
from frequenz.api.common import metrics_pb2 as metrics_pb
from frequenz.api.microgrid import microgrid_pb2 as microgrid_pb
from frequenz.api.microgrid.microgrid_pb2_grpc import MicrogridStub
from frequenz.channels import Broadcast, Receiver, Sender
from google.protobuf.empty_pb2 import Empty # pylint: disable=no-name-in-module

from ..._internal._constants import RECEIVER_MAX_SIZE
from ..component import (
Expand Down Expand Up @@ -193,6 +192,9 @@ async def set_bounds(self, component_id: int, lower: float, upper: float) -> Non
"""


# pylint: disable=no-member


class MicrogridGrpcClient(MicrogridApiClient):
"""Microgrid API client implementation using gRPC as the underlying protocol."""

Expand Down Expand Up @@ -244,7 +246,7 @@ async def components(self) -> Iterable[Component]:
)
components_only = filter(
lambda c: c.category
is not microgrid_pb.ComponentCategory.COMPONENT_CATEGORY_SENSOR,
is not components_pb.ComponentCategory.COMPONENT_CATEGORY_SENSOR,
component_list.components,
)
result: Iterable[Component] = map(
Expand Down Expand Up @@ -340,7 +342,7 @@ async def _component_data_task(
"Making call to `GetComponentData`, for component_id=%d", component_id
)
try:
call = self.api.GetComponentData(
call = self.api.StreamComponentData(
microgrid_pb.ComponentIdParam(id=component_id),
)
# grpc.aio is missing types and mypy thinks this is not
Expand Down Expand Up @@ -578,25 +580,12 @@ async def set_power(self, component_id: int, power_w: float) -> None:
when the api call exceeded timeout
"""
try:
if power_w >= 0:
# grpc.aio is missing types and mypy thinks this is not
# async iterable, but it is
await self.api.Charge(
microgrid_pb.PowerLevelParam(
component_id=component_id, power_w=math.floor(power_w)
),
timeout=DEFAULT_GRPC_CALL_TIMEOUT, # type: ignore[arg-type]
) # type: ignore[misc]
else:
# grpc.aio is missing types and mypy thinks this is not
# async iterable, but it is
power_w *= -1
await self.api.Discharge(
microgrid_pb.PowerLevelParam(
component_id=component_id, power_w=math.floor(power_w)
),
timeout=DEFAULT_GRPC_CALL_TIMEOUT, # type: ignore[arg-type]
) # type: ignore[misc]
await self.api.SetPowerActive(
microgrid_pb.SetPowerActiveParam(
component_id=component_id, power=power_w
),
timeout=DEFAULT_GRPC_CALL_TIMEOUT, # type: ignore[arg-type]
) # type: ignore[misc]
except grpc.aio.AioRpcError as err:
msg = f"Failed to set power. Microgrid API: {self.target}. Err: {err.details()}"
raise grpc.aio.AioRpcError(
Expand Down Expand Up @@ -632,20 +621,13 @@ async def set_bounds(
if lower > 0:
raise ValueError(f"Lower bound {upper} must be less than or equal to 0.")

# grpc.aio is missing types and mypy thinks request_iterator is
# a required argument, but it is not
set_bounds_call = self.api.SetBounds(
timeout=DEFAULT_GRPC_CALL_TIMEOUT,
) # type: ignore[call-arg]
try:
# grpc.aio is missing types and mypy thinks set_bounds_call can be Empty
assert not isinstance(set_bounds_call, Empty)
await set_bounds_call.write(
self.api.AddInclusionBounds(
microgrid_pb.SetBoundsParam(
component_id=component_id,
# pylint: disable=no-member,line-too-long
target_metric=microgrid_pb.SetBoundsParam.TargetMetric.TARGET_METRIC_POWER_ACTIVE,
bounds=common_pb.Bounds(lower=lower, upper=upper),
bounds=metrics_pb.Bounds(lower=lower, upper=upper),
),
)
except grpc.aio.AioRpcError as err:
Expand Down
50 changes: 30 additions & 20 deletions src/frequenz/sdk/microgrid/component/_component.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,17 @@
from enum import Enum
from typing import Optional

import frequenz.api.common.components_pb2 as components_pb
import frequenz.api.microgrid.inverter_pb2 as inverter_pb
import frequenz.api.microgrid.microgrid_pb2 as microgrid_pb


class ComponentType(Enum):
"""A base class from which individual component types are derived."""


# pylint: disable=no-member


class InverterType(ComponentType):
"""Enum representing inverter types."""

Expand All @@ -27,50 +30,57 @@ class InverterType(ComponentType):


def _component_type_from_protobuf(
component_category: microgrid_pb.ComponentCategory.ValueType,
component_type: inverter_pb.Type.ValueType,
component_category: components_pb.ComponentCategory.ValueType,
component_metadata: inverter_pb.Metadata,
) -> Optional[ComponentType]:
"""Convert a protobuf InverterType message to Component enum.

For internal-only use by the `microgrid` package.

Args:
component_category: category the type belongs to.
component_type: protobuf enum to convert.
component_metadata: protobuf metadata to fetch type from.

Returns:
Enum value corresponding to the protobuf message.
"""
# ComponentType values in the protobuf definition are not unique across categories
# as of v0.11.0, so we need to check the component category first, before doing any
# component type checks.
if component_category == microgrid_pb.ComponentCategory.COMPONENT_CATEGORY_INVERTER:
if not any(t.value == component_type for t in InverterType):
if (
component_category
== components_pb.ComponentCategory.COMPONENT_CATEGORY_INVERTER
):
# mypy 1.4.1 crashes at this line, maybe it doesn't like the name of the "type"
# attribute in this context. Hence the "# type: ignore".
if not any(
t.value == component_metadata.type for t in InverterType # type: ignore
):
return None

return InverterType(component_type)
return InverterType(component_metadata.type)

return None


class ComponentCategory(Enum):
"""Possible types of microgrid component."""

NONE = microgrid_pb.ComponentCategory.COMPONENT_CATEGORY_UNSPECIFIED
GRID = microgrid_pb.ComponentCategory.COMPONENT_CATEGORY_GRID
METER = microgrid_pb.ComponentCategory.COMPONENT_CATEGORY_METER
INVERTER = microgrid_pb.ComponentCategory.COMPONENT_CATEGORY_INVERTER
BATTERY = microgrid_pb.ComponentCategory.COMPONENT_CATEGORY_BATTERY
EV_CHARGER = microgrid_pb.ComponentCategory.COMPONENT_CATEGORY_EV_CHARGER
CHP = microgrid_pb.ComponentCategory.COMPONENT_CATEGORY_CHP
NONE = components_pb.ComponentCategory.COMPONENT_CATEGORY_UNSPECIFIED
GRID = components_pb.ComponentCategory.COMPONENT_CATEGORY_GRID
METER = components_pb.ComponentCategory.COMPONENT_CATEGORY_METER
INVERTER = components_pb.ComponentCategory.COMPONENT_CATEGORY_INVERTER
BATTERY = components_pb.ComponentCategory.COMPONENT_CATEGORY_BATTERY
EV_CHARGER = components_pb.ComponentCategory.COMPONENT_CATEGORY_EV_CHARGER
CHP = components_pb.ComponentCategory.COMPONENT_CATEGORY_CHP

# types not yet supported by the API but which can be inferred
# from available graph info
PV_ARRAY = 1000001


def _component_category_from_protobuf(
component_category: microgrid_pb.ComponentCategory.ValueType,
component_category: components_pb.ComponentCategory.ValueType,
) -> ComponentCategory:
"""Convert a protobuf ComponentCategory message to ComponentCategory enum.

Expand All @@ -87,7 +97,7 @@ def _component_category_from_protobuf(
a valid component category as it does not form part of the
microgrid itself)
"""
if component_category == microgrid_pb.ComponentCategory.COMPONENT_CATEGORY_SENSOR:
if component_category == components_pb.ComponentCategory.COMPONENT_CATEGORY_SENSOR:
raise ValueError("Cannot create a component from a sensor!")

if not any(t.value == component_category for t in ComponentCategory):
Expand Down Expand Up @@ -134,10 +144,10 @@ class ComponentMetricId(Enum):
SOC_UPPER_BOUND = "soc_upper_bound"
CAPACITY = "capacity"

POWER_LOWER_BOUND = "power_lower_bound"
POWER_UPPER_BOUND = "power_upper_bound"
POWER_INCLUSION_LOWER_BOUND = "power_inclusion_lower_bound"
POWER_INCLUSION_UPPER_BOUND = "power_inclusion_upper_bound"

ACTIVE_POWER_LOWER_BOUND = "active_power_lower_bound"
ACTIVE_POWER_UPPER_BOUND = "active_power_upper_bound"
ACTIVE_POWER_INCLUSION_LOWER_BOUND = "active_power_inclusion_lower_bound"
ACTIVE_POWER_INCLUSION_UPPER_BOUND = "active_power_inclusion_upper_bound"

TEMPERATURE = "temperature"
Loading