diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 61ee6f2ad..93a201523 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -6,7 +6,7 @@ ## Upgrading - +- The SDK is now using the microgrid API client from [`frequenz-client-microgrid`](https://github.com/frequenz-floss/frequenz-client-microgrid-python/). You should update your code if you are using the microgrid API client directly. ## New Features @@ -14,4 +14,4 @@ ## Bug Fixes - +- A bug was fixed where the grid fuse was not created properly and would end up with a `max_current` with type `float` instead of `Current`. diff --git a/benchmarks/power_distribution/power_distributor.py b/benchmarks/power_distribution/power_distributor.py index bdc2885d8..b47cae1fa 100644 --- a/benchmarks/power_distribution/power_distributor.py +++ b/benchmarks/power_distribution/power_distributor.py @@ -12,6 +12,7 @@ from typing import Any from frequenz.channels import Broadcast +from frequenz.client.microgrid import Component, ComponentCategory from frequenz.sdk import microgrid from frequenz.sdk.actor import ResamplerConfig @@ -26,7 +27,6 @@ Success, ) from frequenz.sdk.microgrid import connection_manager -from frequenz.sdk.microgrid.component import Component, ComponentCategory from frequenz.sdk.timeseries._quantities import Power HOST = "microgrid.sandbox.api.frequenz.io" diff --git a/benchmarks/timeseries/benchmark_datasourcing.py b/benchmarks/timeseries/benchmark_datasourcing.py index f2a780e98..4771c566b 100644 --- a/benchmarks/timeseries/benchmark_datasourcing.py +++ b/benchmarks/timeseries/benchmark_datasourcing.py @@ -17,6 +17,7 @@ from typing import Any from frequenz.channels import Broadcast, Receiver, ReceiverStoppedError +from frequenz.client.microgrid import ComponentMetricId from frequenz.sdk import microgrid from frequenz.sdk.actor import ( @@ -24,7 +25,6 @@ ComponentMetricRequest, DataSourcingActor, ) -from frequenz.sdk.microgrid.component import ComponentMetricId try: from tests.timeseries.mock_microgrid import MockMicrogrid diff --git a/pyproject.toml b/pyproject.toml index 3089d72af..864477a39 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,6 +31,7 @@ dependencies = [ # changing the version # (plugins.mkdocstrings.handlers.python.import) "frequenz-channels == 1.0.0b2", + "frequenz-client-microgrid >= 0.1.2, < 0.2.0", "google-api-python-client >= 2.71, < 3", "grpcio >= 1.54.2, < 2", "grpcio-tools >= 1.54.2, < 2", diff --git a/src/frequenz/sdk/_api_client/__init__.py b/src/frequenz/sdk/_api_client/__init__.py deleted file mode 100644 index 4904564d3..000000000 --- a/src/frequenz/sdk/_api_client/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -# License: MIT -# Copyright © 2022 Frequenz Energy-as-a-Service GmbH - -"""Common items to be shared across all API clients.""" - -from .api_client import ApiClient, ApiProtocol - -__all__ = ["ApiClient", "ApiProtocol"] diff --git a/src/frequenz/sdk/_api_client/api_client.py b/src/frequenz/sdk/_api_client/api_client.py deleted file mode 100644 index ee1b156b6..000000000 --- a/src/frequenz/sdk/_api_client/api_client.py +++ /dev/null @@ -1,50 +0,0 @@ -# License: MIT -# Copyright © 2022 Frequenz Energy-as-a-Service GmbH - -"""An abstract API client.""" - -from abc import ABC, abstractmethod -from enum import Enum - - -class ApiProtocol(Enum): - """Enumerated values of supported API types.""" - - GRPC = 1 - """gRPC API.""" - - REST = 2 - """REST API.""" - - FILESYSTEM = 3 - """Filesystem API.""" - - -class ApiClient(ABC): - """An abstract API client, with general purpose functions that all APIs should implement. - - The methods defined here follow the principle that each client - implementation should clearly and consistently specify the following - information: - a. which minimum version of the API it intends to target, - b. what is the communication protocol. - """ - - @classmethod - @abstractmethod - def api_major_version(cls) -> int: - """Return the major version of the API supported by the client. - - Returns: - The major version of the API supported by the client. - """ - - @classmethod - @abstractmethod - def api_type(cls) -> ApiProtocol: - """Return the API type supported by the client. - - Returns: - The ApiProtocol value representing the API type being targeted in a - concrete implementation. - """ diff --git a/src/frequenz/sdk/actor/_data_sourcing/_component_metric_request.py b/src/frequenz/sdk/actor/_data_sourcing/_component_metric_request.py index 11eec9451..0ef40d241 100644 --- a/src/frequenz/sdk/actor/_data_sourcing/_component_metric_request.py +++ b/src/frequenz/sdk/actor/_data_sourcing/_component_metric_request.py @@ -6,7 +6,7 @@ from dataclasses import dataclass from datetime import datetime -from ...microgrid.component._component import ComponentMetricId +from frequenz.client.microgrid import ComponentMetricId @dataclass diff --git a/src/frequenz/sdk/actor/_data_sourcing/microgrid_api_source.py b/src/frequenz/sdk/actor/_data_sourcing/microgrid_api_source.py index 2c235336e..1c8ddc1b0 100644 --- a/src/frequenz/sdk/actor/_data_sourcing/microgrid_api_source.py +++ b/src/frequenz/sdk/actor/_data_sourcing/microgrid_api_source.py @@ -9,9 +9,7 @@ from typing import Any from frequenz.channels import Receiver, Sender - -from ...microgrid import connection_manager -from ...microgrid.component import ( +from frequenz.client.microgrid import ( BatteryData, ComponentCategory, ComponentMetricId, @@ -19,6 +17,8 @@ InverterData, MeterData, ) + +from ...microgrid import connection_manager from ...timeseries import Sample from ...timeseries._quantities import Quantity from .._channel_registry import ChannelRegistry diff --git a/src/frequenz/sdk/actor/_power_managing/_power_managing_actor.py b/src/frequenz/sdk/actor/_power_managing/_power_managing_actor.py index 394a7dadd..f7a536c5f 100644 --- a/src/frequenz/sdk/actor/_power_managing/_power_managing_actor.py +++ b/src/frequenz/sdk/actor/_power_managing/_power_managing_actor.py @@ -12,9 +12,9 @@ from frequenz.channels import Receiver, Sender from frequenz.channels.util import SkipMissedAndDrift, Timer, select, selected_from +from frequenz.client.microgrid import ComponentCategory from typing_extensions import override -from ...microgrid.component import ComponentCategory from ...timeseries._base_types import SystemBounds from .._actor import Actor from .._channel_registry import ChannelRegistry diff --git a/src/frequenz/sdk/actor/power_distributing/_component_managers/_battery_manager.py b/src/frequenz/sdk/actor/power_distributing/_component_managers/_battery_manager.py index 0eb981479..41d621d98 100644 --- a/src/frequenz/sdk/actor/power_distributing/_component_managers/_battery_manager.py +++ b/src/frequenz/sdk/actor/power_distributing/_component_managers/_battery_manager.py @@ -12,13 +12,13 @@ import grpc from frequenz.channels import Receiver, Sender +from frequenz.client.microgrid import BatteryData, ComponentCategory, InverterData from typing_extensions import override from .... import microgrid from ...._internal._channels import LatestValueCache from ...._internal._math import is_close_to_zero from ....microgrid import connection_manager -from ....microgrid.component import BatteryData, ComponentCategory, InverterData from ....timeseries._quantities import Power from .._component_pool_status_tracker import ComponentPoolStatusTracker from .._component_status import BatteryStatusTracker, ComponentPoolStatus diff --git a/src/frequenz/sdk/actor/power_distributing/_component_status/_battery_status_tracker.py b/src/frequenz/sdk/actor/power_distributing/_component_status/_battery_status_tracker.py index b14ebbab7..2c1a1da16 100644 --- a/src/frequenz/sdk/actor/power_distributing/_component_status/_battery_status_tracker.py +++ b/src/frequenz/sdk/actor/power_distributing/_component_status/_battery_status_tracker.py @@ -30,15 +30,15 @@ # pylint: enable=no-name-in-module from frequenz.channels import Receiver, Sender from frequenz.channels.util import Timer, select, selected_from -from typing_extensions import override - -from ....microgrid import connection_manager -from ....microgrid.component import ( +from frequenz.client.microgrid import ( BatteryData, ComponentCategory, ComponentData, InverterData, ) +from typing_extensions import override + +from ....microgrid import connection_manager from ..._background_service import BackgroundService from ._blocking_status import BlockingStatus from ._component_status import ( diff --git a/src/frequenz/sdk/actor/power_distributing/_distribution_algorithm/_battery_distribution_algorithm.py b/src/frequenz/sdk/actor/power_distributing/_distribution_algorithm/_battery_distribution_algorithm.py index cd491f7e3..fccdb828b 100644 --- a/src/frequenz/sdk/actor/power_distributing/_distribution_algorithm/_battery_distribution_algorithm.py +++ b/src/frequenz/sdk/actor/power_distributing/_distribution_algorithm/_battery_distribution_algorithm.py @@ -8,8 +8,9 @@ from dataclasses import dataclass from typing import NamedTuple, Sequence +from frequenz.client.microgrid import BatteryData, InverterData + from ...._internal._math import is_close_to_zero -from ....microgrid.component import BatteryData, InverterData from ..result import PowerBounds _logger = logging.getLogger(__name__) diff --git a/src/frequenz/sdk/microgrid/__init__.py b/src/frequenz/sdk/microgrid/__init__.py index c853d3920..35115aad0 100644 --- a/src/frequenz/sdk/microgrid/__init__.py +++ b/src/frequenz/sdk/microgrid/__init__.py @@ -122,7 +122,7 @@ """ # noqa: D205, D400 from ..actor import ResamplerConfig -from . import _data_pipeline, client, component, connection_manager, metadata +from . import _data_pipeline, connection_manager from ._data_pipeline import ( battery_pool, consumer, @@ -149,15 +149,12 @@ async def initialize(host: str, port: int, resampler_config: ResamplerConfig) -> __all__ = [ "initialize", - "client", - "component", "consumer", "battery_pool", "ev_charger_pool", "grid", "frequency", "logical_meter", - "metadata", "producer", "voltage", ] diff --git a/src/frequenz/sdk/microgrid/_power_wrapper.py b/src/frequenz/sdk/microgrid/_power_wrapper.py index cc2748737..9daa8af37 100644 --- a/src/frequenz/sdk/microgrid/_power_wrapper.py +++ b/src/frequenz/sdk/microgrid/_power_wrapper.py @@ -13,7 +13,7 @@ # pylint seems to think this is a cyclic import, but it is not. # # pylint: disable=cyclic-import -from .component import ComponentCategory +from frequenz.client.microgrid import ComponentCategory # A number of imports had to be done inside functions where they are used, to break # import cycles. diff --git a/src/frequenz/sdk/microgrid/client/__init__.py b/src/frequenz/sdk/microgrid/client/__init__.py deleted file mode 100644 index f52cf822e..000000000 --- a/src/frequenz/sdk/microgrid/client/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -# License: MIT -# Copyright © 2022 Frequenz Energy-as-a-Service GmbH - -"""Microgrid API client. - -This package provides a low-level interface for interacting with the microgrid API. -""" - -from ._client import MicrogridApiClient, MicrogridGrpcClient -from ._connection import Connection -from ._retry import ExponentialBackoff, LinearBackoff, RetryStrategy - -__all__ = [ - "Connection", - "LinearBackoff", - "MicrogridApiClient", - "MicrogridGrpcClient", - "RetryStrategy", - "ExponentialBackoff", -] diff --git a/src/frequenz/sdk/microgrid/client/_client.py b/src/frequenz/sdk/microgrid/client/_client.py deleted file mode 100644 index 5956710b0..000000000 --- a/src/frequenz/sdk/microgrid/client/_client.py +++ /dev/null @@ -1,657 +0,0 @@ -# License: MIT -# Copyright © 2022 Frequenz Energy-as-a-Service GmbH - -"""Client for requests to the Microgrid API.""" - -import asyncio -import logging -from abc import ABC, abstractmethod -from collections.abc import Awaitable, Callable, Iterable -from typing import Any, TypeVar, cast - -import grpc -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 ( - BatteryData, - Component, - ComponentCategory, - EVChargerData, - InverterData, - MeterData, -) -from ..component._component import ( - _component_category_from_protobuf, - _component_metadata_from_protobuf, - _component_type_from_protobuf, -) -from ..metadata import Location, Metadata -from ._connection import Connection -from ._retry import LinearBackoff, RetryStrategy - -DEFAULT_GRPC_CALL_TIMEOUT = 60.0 -"""The default timeout for gRPC calls made by this client (in seconds).""" - -# A generic type for representing various component data types, used in the -# generic function `MicrogridGrpcClient._component_data_task` that fetches -# component data and transforms it into one of the specific types. -_GenericComponentData = TypeVar( - "_GenericComponentData", - MeterData, - BatteryData, - InverterData, - EVChargerData, -) -"""Type variable for representing various component data types.""" - -_logger = logging.getLogger(__name__) - - -class MicrogridApiClient(ABC): - """Base interface for microgrid API clients to implement.""" - - @abstractmethod - async def components(self) -> Iterable[Component]: - """Fetch all the components present in the microgrid. - - Returns: - Iterator whose elements are all the components in the microgrid. - """ - - @abstractmethod - async def metadata(self) -> Metadata: - """Fetch the microgrid metadata. - - Returns: - the microgrid metadata. - """ - - @abstractmethod - async def connections( - self, - starts: set[int] | None = None, - ends: set[int] | None = None, - ) -> Iterable[Connection]: - """Fetch the connections between components in the microgrid. - - Args: - starts: if set and non-empty, only include connections whose start - value matches one of the provided component IDs - ends: if set and non-empty, only include connections whose end value - matches one of the provided component IDs - - Returns: - Microgrid connections matching the provided start and end filters. - """ - - @abstractmethod - async def meter_data( - self, - component_id: int, - maxsize: int = RECEIVER_MAX_SIZE, - ) -> Receiver[MeterData]: - """Return a channel receiver that provides a `MeterData` stream. - - Args: - component_id: id of the meter to get data for. - maxsize: Size of the receiver's buffer. - - Returns: - A channel receiver that provides realtime meter data. - """ - - @abstractmethod - async def battery_data( - self, - component_id: int, - maxsize: int = RECEIVER_MAX_SIZE, - ) -> Receiver[BatteryData]: - """Return a channel receiver that provides a `BatteryData` stream. - - Args: - component_id: id of the battery to get data for. - maxsize: Size of the receiver's buffer. - - Returns: - A channel receiver that provides realtime battery data. - """ - - @abstractmethod - async def inverter_data( - self, - component_id: int, - maxsize: int = RECEIVER_MAX_SIZE, - ) -> Receiver[InverterData]: - """Return a channel receiver that provides an `InverterData` stream. - - Args: - component_id: id of the inverter to get data for. - maxsize: Size of the receiver's buffer. - - Returns: - A channel receiver that provides realtime inverter data. - """ - - @abstractmethod - async def ev_charger_data( - self, - component_id: int, - maxsize: int = RECEIVER_MAX_SIZE, - ) -> Receiver[EVChargerData]: - """Return a channel receiver that provides an `EvChargeData` stream. - - Args: - component_id: id of the ev charger to get data for. - maxsize: Size of the receiver's buffer. - - Returns: - A channel receiver that provides realtime ev charger data. - """ - - @abstractmethod - async def set_power(self, component_id: int, power_w: float) -> None: - """Send request to the Microgrid to set power for component. - - If power > 0, then component will be charged with this power. - If power < 0, then component will be discharged with this power. - If power == 0, then stop charging or discharging component. - - - Args: - component_id: id of the component to set power. - power_w: power to set for the component. - """ - - @abstractmethod - async def set_bounds(self, component_id: int, lower: float, upper: float) -> None: - """Send `SetBoundsParam`s received from a channel to the Microgrid service. - - Args: - component_id: ID of the component to set bounds for. - lower: Lower bound to be set for the component. - upper: Upper bound to be set for the component. - """ - - -# pylint: disable=no-member - - -class MicrogridGrpcClient(MicrogridApiClient): - """Microgrid API client implementation using gRPC as the underlying protocol.""" - - def __init__( - self, - grpc_channel: grpc.aio.Channel, - target: str, - retry_spec: RetryStrategy = LinearBackoff(), - ) -> None: - """Initialize the class instance. - - Args: - grpc_channel: asyncio-supporting gRPC channel - target: server (host:port) to be used for asyncio-supporting gRPC - channel that the client should use to contact the API - retry_spec: Specs on how to retry if the connection to a streaming - method gets lost. - """ - self.target = target - """The location (as "host:port") of the microgrid API gRPC server.""" - - self.api = MicrogridStub(grpc_channel) - """The gRPC stub for the microgrid API.""" - - self._component_streams: dict[int, Broadcast[Any]] = {} - self._streaming_tasks: dict[int, asyncio.Task[None]] = {} - self._retry_spec = retry_spec - - async def components(self) -> Iterable[Component]: - """Fetch all the components present in the microgrid. - - Returns: - Iterator whose elements are all the components in the microgrid. - - Raises: - AioRpcError: if connection to Microgrid API cannot be established or - when the api call exceeded timeout - """ - try: - # grpc.aio is missing types and mypy thinks this is not awaitable, - # but it is - component_list = await cast( - Awaitable[microgrid_pb.ComponentList], - self.api.ListComponents( - microgrid_pb.ComponentFilter(), - timeout=int(DEFAULT_GRPC_CALL_TIMEOUT), - ), - ) - - except grpc.aio.AioRpcError as err: - msg = f"Failed to list components. Microgrid API: {self.target}. Err: {err.details()}" - raise grpc.aio.AioRpcError( - code=err.code(), - initial_metadata=err.initial_metadata(), - trailing_metadata=err.trailing_metadata(), - details=msg, - debug_error_string=err.debug_error_string(), - ) - components_only = filter( - lambda c: c.category - is not components_pb.ComponentCategory.COMPONENT_CATEGORY_SENSOR, - component_list.components, - ) - result: Iterable[Component] = map( - lambda c: Component( - c.id, - _component_category_from_protobuf(c.category), - _component_type_from_protobuf(c.category, c.inverter), - _component_metadata_from_protobuf(c.category, c.grid), - ), - components_only, - ) - - return result - - async def metadata(self) -> Metadata: - """Fetch the microgrid metadata. - - If there is an error fetching the metadata, the microgrid ID and - location will be set to None. - - Returns: - the microgrid metadata. - """ - microgrid_metadata: microgrid_pb.MicrogridMetadata | None = None - try: - microgrid_metadata = await cast( - Awaitable[microgrid_pb.MicrogridMetadata], - self.api.GetMicrogridMetadata( - Empty(), - timeout=int(DEFAULT_GRPC_CALL_TIMEOUT), - ), - ) - except grpc.aio.AioRpcError: - _logger.exception("The microgrid metadata is not available.") - - if not microgrid_metadata: - return Metadata() - - location: Location | None = None - if microgrid_metadata.location: - location = Location( - latitude=microgrid_metadata.location.latitude, - longitude=microgrid_metadata.location.longitude, - ) - - return Metadata(microgrid_id=microgrid_metadata.microgrid_id, location=location) - - async def connections( - self, - starts: set[int] | None = None, - ends: set[int] | None = None, - ) -> Iterable[Connection]: - """Fetch the connections between components in the microgrid. - - Args: - starts: if set and non-empty, only include connections whose start - value matches one of the provided component IDs - ends: if set and non-empty, only include connections whose end value - matches one of the provided component IDs - - Returns: - Microgrid connections matching the provided start and end filters. - - Raises: - AioRpcError: if connection to Microgrid API cannot be established or - when the api call exceeded timeout - """ - connection_filter = microgrid_pb.ConnectionFilter(starts=starts, ends=ends) - try: - valid_components, all_connections = await asyncio.gather( - self.components(), - # grpc.aio is missing types and mypy thinks this is not - # awaitable, but it is - cast( - Awaitable[microgrid_pb.ConnectionList], - self.api.ListConnections( - connection_filter, - timeout=int(DEFAULT_GRPC_CALL_TIMEOUT), - ), - ), - ) - except grpc.aio.AioRpcError as err: - msg = f"Failed to list connections. Microgrid API: {self.target}. Err: {err.details()}" - raise grpc.aio.AioRpcError( - code=err.code(), - initial_metadata=err.initial_metadata(), - trailing_metadata=err.trailing_metadata(), - details=msg, - debug_error_string=err.debug_error_string(), - ) - # Filter out the components filtered in `components` method. - # id=0 is an exception indicating grid component. - valid_ids = {c.component_id for c in valid_components} - valid_ids.add(0) - - connections = filter( - lambda c: (c.start in valid_ids and c.end in valid_ids), - all_connections.connections, - ) - - result: Iterable[Connection] = map( - lambda c: Connection(c.start, c.end), connections - ) - - return result - - async def _component_data_task( - self, - component_id: int, - transform: Callable[[microgrid_pb.ComponentData], _GenericComponentData], - sender: Sender[_GenericComponentData], - ) -> None: - """Read data from the microgrid API and send to a channel. - - Args: - component_id: id of the component to get data for. - transform: A method for transforming raw component data into the - desired output type. - sender: A channel sender, to send the component data to. - """ - retry_spec: RetryStrategy = self._retry_spec.copy() - while True: - _logger.debug( - "Making call to `GetComponentData`, for component_id=%d", component_id - ) - try: - call = self.api.StreamComponentData( - microgrid_pb.ComponentIdParam(id=component_id), - ) - # grpc.aio is missing types and mypy thinks this is not - # async iterable, but it is - async for msg in call: # type: ignore[attr-defined] - await sender.send(transform(msg)) - except grpc.aio.AioRpcError as err: - api_details = f"Microgrid API: {self.target}." - _logger.exception( - "`GetComponentData`, for component_id=%d: exception: %s api: %s", - component_id, - err, - api_details, - ) - - if interval := retry_spec.next_interval(): - _logger.warning( - "`GetComponentData`, for component_id=%d: connection ended, " - "retrying %s in %0.3f seconds.", - component_id, - retry_spec.get_progress(), - interval, - ) - await asyncio.sleep(interval) - else: - _logger.warning( - "`GetComponentData`, for component_id=%d: connection ended, " - "retry limit exceeded %s.", - component_id, - retry_spec.get_progress(), - ) - break - - def _get_component_data_channel( - self, - component_id: int, - transform: Callable[[microgrid_pb.ComponentData], _GenericComponentData], - ) -> Broadcast[_GenericComponentData]: - """Return the broadcast channel for a given component_id. - - If a broadcast channel for the given component_id doesn't exist, create - a new channel and a task for reading data from the microgrid api and - sending them to the channel. - - Args: - component_id: id of the component to get data for. - transform: A method for transforming raw component data into the - desired output type. - - Returns: - The channel for the given component_id. - """ - if component_id in self._component_streams: - return self._component_streams[component_id] - task_name = f"raw-component-data-{component_id}" - chan = Broadcast[_GenericComponentData](task_name, resend_latest=True) - self._component_streams[component_id] = chan - - self._streaming_tasks[component_id] = asyncio.create_task( - self._component_data_task( - component_id, - transform, - chan.new_sender(), - ), - name=task_name, - ) - return chan - - async def _expect_category( - self, - component_id: int, - expected_category: ComponentCategory, - ) -> None: - """Check if the given component_id is of the expected type. - - Raises: - ValueError: if the given id is unknown or has a different type. - - Args: - component_id: Component id to check. - expected_category: Component category that the given id is expected - to have. - """ - try: - comp = next( - comp - for comp in await self.components() - if comp.component_id == component_id - ) - except StopIteration as exc: - raise ValueError( - f"Unable to find component with id {component_id}" - ) from exc - - if comp.category != expected_category: - raise ValueError( - f"Component id {component_id} is a {comp.category}" - f", not a {expected_category}." - ) - - async def meter_data( # noqa: DOC502 (ValueError is raised indirectly by _expect_category) - self, - component_id: int, - maxsize: int = RECEIVER_MAX_SIZE, - ) -> Receiver[MeterData]: - """Return a channel receiver that provides a `MeterData` stream. - - Raises: - ValueError: if the given id is unknown or has a different type. - - Args: - component_id: id of the meter to get data for. - maxsize: Size of the receiver's buffer. - - Returns: - A channel receiver that provides realtime meter data. - """ - await self._expect_category( - component_id, - ComponentCategory.METER, - ) - return self._get_component_data_channel( - component_id, - MeterData.from_proto, - ).new_receiver(maxsize=maxsize) - - async def battery_data( # noqa: DOC502 (ValueError is raised indirectly by _expect_category) - self, - component_id: int, - maxsize: int = RECEIVER_MAX_SIZE, - ) -> Receiver[BatteryData]: - """Return a channel receiver that provides a `BatteryData` stream. - - Raises: - ValueError: if the given id is unknown or has a different type. - - Args: - component_id: id of the battery to get data for. - maxsize: Size of the receiver's buffer. - - Returns: - A channel receiver that provides realtime battery data. - """ - await self._expect_category( - component_id, - ComponentCategory.BATTERY, - ) - return self._get_component_data_channel( - component_id, - BatteryData.from_proto, - ).new_receiver(maxsize=maxsize) - - async def inverter_data( # noqa: DOC502 (ValueError is raised indirectly by _expect_category) - self, - component_id: int, - maxsize: int = RECEIVER_MAX_SIZE, - ) -> Receiver[InverterData]: - """Return a channel receiver that provides an `InverterData` stream. - - Raises: - ValueError: if the given id is unknown or has a different type. - - Args: - component_id: id of the inverter to get data for. - maxsize: Size of the receiver's buffer. - - Returns: - A channel receiver that provides realtime inverter data. - """ - await self._expect_category( - component_id, - ComponentCategory.INVERTER, - ) - return self._get_component_data_channel( - component_id, - InverterData.from_proto, - ).new_receiver(maxsize=maxsize) - - async def ev_charger_data( # noqa: DOC502 (ValueError is raised indirectly by _expect_category) - self, - component_id: int, - maxsize: int = RECEIVER_MAX_SIZE, - ) -> Receiver[EVChargerData]: - """Return a channel receiver that provides an `EvChargeData` stream. - - Raises: - ValueError: if the given id is unknown or has a different type. - - Args: - component_id: id of the ev charger to get data for. - maxsize: Size of the receiver's buffer. - - Returns: - A channel receiver that provides realtime ev charger data. - """ - await self._expect_category( - component_id, - ComponentCategory.EV_CHARGER, - ) - return self._get_component_data_channel( - component_id, - EVChargerData.from_proto, - ).new_receiver(maxsize=maxsize) - - async def set_power(self, component_id: int, power_w: float) -> None: - """Send request to the Microgrid to set power for component. - - If power > 0, then component will be charged with this power. - If power < 0, then component will be discharged with this power. - If power == 0, then stop charging or discharging component. - - - Args: - component_id: id of the component to set power. - power_w: power to set for the component. - - Raises: - AioRpcError: if connection to Microgrid API cannot be established or - when the api call exceeded timeout - """ - try: - await cast( - Awaitable[microgrid_pb.SetPowerActiveParam], - self.api.SetPowerActive( - microgrid_pb.SetPowerActiveParam( - component_id=component_id, power=power_w - ), - timeout=int(DEFAULT_GRPC_CALL_TIMEOUT), - ), - ) - except grpc.aio.AioRpcError as err: - msg = f"Failed to set power. Microgrid API: {self.target}. Err: {err.details()}" - raise grpc.aio.AioRpcError( - code=err.code(), - initial_metadata=err.initial_metadata(), - trailing_metadata=err.trailing_metadata(), - details=msg, - debug_error_string=err.debug_error_string(), - ) - - async def set_bounds( - self, - component_id: int, - lower: float, - upper: float, - ) -> None: - """Send `SetBoundsParam`s received from a channel to the Microgrid service. - - Args: - component_id: ID of the component to set bounds for. - lower: Lower bound to be set for the component. - upper: Upper bound to be set for the component. - - Raises: - ValueError: when upper bound is less than 0, or when lower bound is - greater than 0. - grpc.aio.AioRpcError: if connection to Microgrid API cannot be established - or when the api call exceeded timeout - """ - api_details = f"Microgrid API: {self.target}." - if upper < 0: - raise ValueError(f"Upper bound {upper} must be greater than or equal to 0.") - if lower > 0: - raise ValueError(f"Lower bound {upper} must be less than or equal to 0.") - - target_metric = ( - microgrid_pb.SetBoundsParam.TargetMetric.TARGET_METRIC_POWER_ACTIVE - ) - try: - self.api.AddInclusionBounds( - microgrid_pb.SetBoundsParam( - component_id=component_id, - target_metric=target_metric, - bounds=metrics_pb.Bounds(lower=lower, upper=upper), - ), - ) - except grpc.aio.AioRpcError as err: - _logger.error( - "set_bounds write failed: %s, for message: %s, api: %s. Err: %s", - err, - next, - api_details, - err.details(), - ) - raise diff --git a/src/frequenz/sdk/microgrid/client/_connection.py b/src/frequenz/sdk/microgrid/client/_connection.py deleted file mode 100644 index 1813c9950..000000000 --- a/src/frequenz/sdk/microgrid/client/_connection.py +++ /dev/null @@ -1,25 +0,0 @@ -# License: MIT -# Copyright © 2022 Frequenz Energy-as-a-Service GmbH - -"""Defines the connections between microgrid components.""" - -from typing import NamedTuple - - -class Connection(NamedTuple): - """Metadata for a connection between microgrid components.""" - - start: int - """The component ID that represents the start component of the connection.""" - - end: int - """The component ID that represents the end component of the connection.""" - - def is_valid(self) -> bool: - """Check if this instance contains valid data. - - Returns: - `True` if `start >= 0`, `end > 0`, and `start != end`, `False` - otherwise. - """ - return self.start >= 0 and self.end > 0 and self.start != self.end diff --git a/src/frequenz/sdk/microgrid/client/_retry.py b/src/frequenz/sdk/microgrid/client/_retry.py deleted file mode 100644 index f52c690a0..000000000 --- a/src/frequenz/sdk/microgrid/client/_retry.py +++ /dev/null @@ -1,169 +0,0 @@ -# License: MIT -# Copyright © 2022 Frequenz Energy-as-a-Service GmbH - -"""Implementations for retry strategies.""" - -from __future__ import annotations - -import random -from abc import ABC, abstractmethod -from collections.abc import Iterator -from copy import deepcopy - -DEFAULT_RETRY_INTERVAL = 3.0 -"""Default retry interval, in seconds.""" - -DEFAULT_RETRY_JITTER = 1.0 -"""Default retry jitter, in seconds.""" - - -class RetryStrategy(ABC): - """Interface for implementing retry strategies.""" - - _limit: int | None - _count: int - - @abstractmethod - def next_interval(self) -> float | None: - """Return the time to wait before the next retry. - - Returns `None` if the retry limit has been reached, and no more retries - are possible. - - Returns: - Time until next retry when below retry limit, and None otherwise. - """ - - def get_progress(self) -> str: - """Return a string denoting the retry progress. - - Returns: - String denoting retry progress in the form "(count/limit)" - """ - if self._limit is None: - return f"({self._count}/∞)" - - return f"({self._count}/{self._limit})" - - def reset(self) -> None: - """Reset the retry counter. - - To be called as soon as a connection is successful. - """ - self._count = 0 - - def copy(self) -> RetryStrategy: - """Create a new instance of `self`. - - Returns: - A deepcopy of `self`. - """ - ret = deepcopy(self) - ret.reset() - return ret - - def __iter__(self) -> Iterator[float]: - """Return an iterator over the retry intervals. - - Yields: - Next retry interval in seconds. - """ - while True: - interval = self.next_interval() - if interval is None: - break - yield interval - - -class LinearBackoff(RetryStrategy): - """Provides methods for calculating the interval between retries.""" - - def __init__( - self, - interval: float = DEFAULT_RETRY_INTERVAL, - jitter: float = DEFAULT_RETRY_JITTER, - limit: int | None = None, - ) -> None: - """Create a `LinearBackoff` instance. - - Args: - interval: time to wait for before the next retry, in seconds. - jitter: a jitter to add to the retry interval. - limit: max number of retries before giving up. `None` means no - limit, and `0` means no retry. - """ - self._interval = interval - self._jitter = jitter - self._limit = limit - - self._count = 0 - - def next_interval(self) -> float | None: - """Return the time to wait before the next retry. - - Returns `None` if the retry limit has been reached, and no more retries - are possible. - - Returns: - Time until next retry when below retry limit, and None otherwise. - """ - if self._limit is not None and self._count >= self._limit: - return None - self._count += 1 - return self._interval + random.uniform(0.0, self._jitter) - - -class ExponentialBackoff(RetryStrategy): - """Provides methods for calculating the exponential interval between retries.""" - - DEFAULT_INTERVAL = DEFAULT_RETRY_INTERVAL - """Default retry interval, in seconds.""" - - DEFAULT_MAX_INTERVAL = 60.0 - """Default maximum retry interval, in seconds.""" - - DEFAULT_MULTIPLIER = 2.0 - """Default multiplier for exponential increment.""" - - # pylint: disable=too-many-arguments - def __init__( - self, - initial_interval: float = DEFAULT_INTERVAL, - max_interval: float = DEFAULT_MAX_INTERVAL, - multiplier: float = DEFAULT_MULTIPLIER, - jitter: float = DEFAULT_RETRY_JITTER, - limit: int | None = None, - ) -> None: - """Create a `ExponentialBackoff` instance. - - Args: - initial_interval: time to wait for before the first retry, in - seconds. - max_interval: maximum interval, in seconds. - multiplier: exponential increment for interval. - jitter: a jitter to add to the retry interval. - limit: max number of retries before giving up. `None` means no - limit, and `0` means no retry. - """ - self._initial = initial_interval - self._max = max_interval - self._multiplier = multiplier - self._jitter = jitter - self._limit = limit - - self._count = 0 - - def next_interval(self) -> float | None: - """Return the time to wait before the next retry. - - Returns `None` if the retry limit has been reached, and no more retries - are possible. - - Returns: - Time until next retry when below retry limit, and None otherwise. - """ - if self._limit is not None and self._count >= self._limit: - return None - self._count += 1 - exp_backoff_interval = self._initial * self._multiplier ** (self._count - 1) - return min(exp_backoff_interval + random.uniform(0.0, self._jitter), self._max) diff --git a/src/frequenz/sdk/microgrid/component/__init__.py b/src/frequenz/sdk/microgrid/component/__init__.py deleted file mode 100644 index f1c58851d..000000000 --- a/src/frequenz/sdk/microgrid/component/__init__.py +++ /dev/null @@ -1,40 +0,0 @@ -# License: MIT -# Copyright © 2022 Frequenz Energy-as-a-Service GmbH - -"""Microgrid component abstractions. - -This package provides classes to operate con microgrid components. -""" - -from ._component import ( - Component, - ComponentCategory, - ComponentMetadata, - ComponentMetricId, - GridMetadata, - InverterType, -) -from ._component_data import ( - BatteryData, - ComponentData, - EVChargerData, - InverterData, - MeterData, -) -from ._component_states import EVChargerCableState, EVChargerComponentState - -__all__ = [ - "BatteryData", - "Component", - "ComponentData", - "ComponentCategory", - "ComponentMetadata", - "ComponentMetricId", - "EVChargerCableState", - "EVChargerComponentState", - "EVChargerData", - "GridMetadata", - "InverterData", - "InverterType", - "MeterData", -] diff --git a/src/frequenz/sdk/microgrid/component/_component.py b/src/frequenz/sdk/microgrid/component/_component.py deleted file mode 100644 index da68238e8..000000000 --- a/src/frequenz/sdk/microgrid/component/_component.py +++ /dev/null @@ -1,249 +0,0 @@ -# License: MIT -# Copyright © 2022 Frequenz Energy-as-a-Service GmbH - -"""Defines the components that can be used in a microgrid.""" -from __future__ import annotations - -from dataclasses import dataclass -from enum import Enum -from typing import TYPE_CHECKING - -import frequenz.api.common.components_pb2 as components_pb -import frequenz.api.microgrid.grid_pb2 as grid_pb -import frequenz.api.microgrid.inverter_pb2 as inverter_pb - -if TYPE_CHECKING: - # Break circular import - from ...timeseries import Fuse - - -class ComponentType(Enum): - """A base class from which individual component types are derived.""" - - -# pylint: disable=no-member - - -class InverterType(ComponentType): - """Enum representing inverter types.""" - - NONE = inverter_pb.Type.TYPE_UNSPECIFIED - """Unspecified inverter type.""" - - BATTERY = inverter_pb.Type.TYPE_BATTERY - """Battery inverter.""" - - SOLAR = inverter_pb.Type.TYPE_SOLAR - """Solar inverter.""" - - HYBRID = inverter_pb.Type.TYPE_HYBRID - """Hybrid inverter.""" - - -def _component_type_from_protobuf( - component_category: components_pb.ComponentCategory.ValueType, - component_metadata: inverter_pb.Metadata, -) -> ComponentType | None: - """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_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 - == 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_metadata.type) - - return None - - -class ComponentCategory(Enum): - """Possible types of microgrid component.""" - - NONE = components_pb.ComponentCategory.COMPONENT_CATEGORY_UNSPECIFIED - """Unspecified component category.""" - - GRID = components_pb.ComponentCategory.COMPONENT_CATEGORY_GRID - """Grid component.""" - - METER = components_pb.ComponentCategory.COMPONENT_CATEGORY_METER - """Meter component.""" - - INVERTER = components_pb.ComponentCategory.COMPONENT_CATEGORY_INVERTER - """Inverter component.""" - - BATTERY = components_pb.ComponentCategory.COMPONENT_CATEGORY_BATTERY - """Battery component.""" - - EV_CHARGER = components_pb.ComponentCategory.COMPONENT_CATEGORY_EV_CHARGER - """EV charger component.""" - - CHP = components_pb.ComponentCategory.COMPONENT_CATEGORY_CHP - """CHP component.""" - - -def _component_category_from_protobuf( - component_category: components_pb.ComponentCategory.ValueType, -) -> ComponentCategory: - """Convert a protobuf ComponentCategory message to ComponentCategory enum. - - For internal-only use by the `microgrid` package. - - Args: - component_category: protobuf enum to convert - - Returns: - Enum value corresponding to the protobuf message. - - Raises: - ValueError: if `component_category` is a sensor (this is not considered - a valid component category as it does not form part of the - microgrid itself) - """ - 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): - return ComponentCategory.NONE - - return ComponentCategory(component_category) - - -@dataclass(frozen=True) -class ComponentMetadata: - """Base class for component metadata classes.""" - - fuse: Fuse | None = None - """The fuse at the grid connection point.""" - - -@dataclass(frozen=True) -class GridMetadata(ComponentMetadata): - """Metadata for a grid connection point.""" - - -def _component_metadata_from_protobuf( - component_category: components_pb.ComponentCategory.ValueType, - component_metadata: grid_pb.Metadata, -) -> GridMetadata | None: - from ...timeseries import Current, Fuse # pylint: disable=import-outside-toplevel - - if component_category == components_pb.ComponentCategory.COMPONENT_CATEGORY_GRID: - max_current = Current.from_amperes(component_metadata.rated_fuse_current) - fuse = Fuse(max_current) - return GridMetadata(fuse) - - return None - - -@dataclass(frozen=True) -class Component: - """Metadata for a single microgrid component.""" - - component_id: int - """The ID of this component.""" - - category: ComponentCategory - """The category of this component.""" - - type: ComponentType | None = None - """The type of this component.""" - - metadata: ComponentMetadata | None = None - """The metadata of this component.""" - - def is_valid(self) -> bool: - """Check if this instance contains valid data. - - Returns: - `True` if `id > 0` and `type` is a valid `ComponentCategory`, or if `id - == 0` and `type` is `GRID`, `False` otherwise - """ - return ( - self.component_id > 0 and any(t == self.category for t in ComponentCategory) - ) or (self.component_id == 0 and self.category == ComponentCategory.GRID) - - def __hash__(self) -> int: - """Compute a hash of this instance, obtained by hashing the `component_id` field. - - Returns: - Hash of this instance. - """ - return hash(self.component_id) - - -class ComponentMetricId(Enum): - """An enum representing the various metrics available in the microgrid.""" - - ACTIVE_POWER = "active_power" - """Active power.""" - - ACTIVE_POWER_PHASE_1 = "active_power_phase_1" - """Active power in phase 1.""" - ACTIVE_POWER_PHASE_2 = "active_power_phase_2" - """Active power in phase 2.""" - ACTIVE_POWER_PHASE_3 = "active_power_phase_3" - """Active power in phase 3.""" - - CURRENT_PHASE_1 = "current_phase_1" - """Current in phase 1.""" - CURRENT_PHASE_2 = "current_phase_2" - """Current in phase 2.""" - CURRENT_PHASE_3 = "current_phase_3" - """Current in phase 3.""" - - VOLTAGE_PHASE_1 = "voltage_phase_1" - """Voltage in phase 1.""" - VOLTAGE_PHASE_2 = "voltage_phase_2" - """Voltage in phase 2.""" - VOLTAGE_PHASE_3 = "voltage_phase_3" - """Voltage in phase 3.""" - - FREQUENCY = "frequency" - - SOC = "soc" - """State of charge.""" - SOC_LOWER_BOUND = "soc_lower_bound" - """Lower bound of state of charge.""" - SOC_UPPER_BOUND = "soc_upper_bound" - """Upper bound of state of charge.""" - CAPACITY = "capacity" - """Capacity.""" - - POWER_INCLUSION_LOWER_BOUND = "power_inclusion_lower_bound" - """Power inclusion lower bound.""" - POWER_EXCLUSION_LOWER_BOUND = "power_exclusion_lower_bound" - """Power exclusion lower bound.""" - POWER_EXCLUSION_UPPER_BOUND = "power_exclusion_upper_bound" - """Power exclusion upper bound.""" - POWER_INCLUSION_UPPER_BOUND = "power_inclusion_upper_bound" - """Power inclusion upper bound.""" - - ACTIVE_POWER_INCLUSION_LOWER_BOUND = "active_power_inclusion_lower_bound" - """Active power inclusion lower bound.""" - ACTIVE_POWER_EXCLUSION_LOWER_BOUND = "active_power_exclusion_lower_bound" - """Active power exclusion lower bound.""" - ACTIVE_POWER_EXCLUSION_UPPER_BOUND = "active_power_exclusion_upper_bound" - """Active power exclusion upper bound.""" - ACTIVE_POWER_INCLUSION_UPPER_BOUND = "active_power_inclusion_upper_bound" - """Active power inclusion upper bound.""" - - TEMPERATURE = "temperature" - """Temperature.""" diff --git a/src/frequenz/sdk/microgrid/component/_component_data.py b/src/frequenz/sdk/microgrid/component/_component_data.py deleted file mode 100644 index 05524cb88..000000000 --- a/src/frequenz/sdk/microgrid/component/_component_data.py +++ /dev/null @@ -1,487 +0,0 @@ -# License: MIT -# Copyright © 2022 Frequenz Energy-as-a-Service GmbH - -"""Component data types for data coming from a microgrid.""" -from __future__ import annotations - -from abc import ABC, abstractmethod -from dataclasses import dataclass, field -from datetime import datetime, timezone - -import frequenz.api.microgrid.battery_pb2 as battery_pb -import frequenz.api.microgrid.inverter_pb2 as inverter_pb -import frequenz.api.microgrid.microgrid_pb2 as microgrid_pb - -from ._component_states import EVChargerCableState, EVChargerComponentState - - -@dataclass(frozen=True) -class ComponentData(ABC): - """A private base class for strongly typed component data classes.""" - - component_id: int - """The ID identifying this component in the microgrid.""" - - timestamp: datetime - """The timestamp of when the data was measured.""" - - # The `raw` attribute is excluded from the constructor as it can only be provided - # when instantiating `ComponentData` using the `from_proto` method, which reads - # data from a protobuf message. The whole protobuf message is stored as the `raw` - # attribute. When `ComponentData` is not instantiated from a protobuf message, - # i.e. using the constructor, `raw` will be set to `None`. - raw: microgrid_pb.ComponentData | None = field(default=None, init=False) - """Raw component data as decoded from the wire.""" - - def _set_raw(self, raw: microgrid_pb.ComponentData) -> None: - """Store raw protobuf message. - - It is preferred to keep the dataclasses immutable (frozen) and make the `raw` - attribute read-only, which is why the approach of writing to `__dict__` - was used, instead of mutating the `self.raw = raw` attribute directly. - - Args: - raw: raw component data as decoded from the wire. - """ - self.__dict__["raw"] = raw - - @classmethod - @abstractmethod - def from_proto(cls, raw: microgrid_pb.ComponentData) -> ComponentData: - """Create ComponentData from a protobuf message. - - Args: - raw: raw component data as decoded from the wire. - - Returns: - The instance created from the protobuf message. - """ - - -@dataclass(frozen=True) -class MeterData(ComponentData): - """A wrapper class for holding meter data.""" - - active_power: float - """The 3-phase active power, in Watts, represented in the passive sign convention. - +ve current means consumption, away from the grid. - -ve current means supply into the grid. - """ - - active_power_per_phase: tuple[float, float, float] - """The AC active power for phase/line 1,2 and 3 respectively.""" - - current_per_phase: tuple[float, float, float] - """AC current in Amperes (A) for phase/line 1,2 and 3 respectively. - +ve current means consumption, away from the grid. - -ve current means supply into the grid. - """ - - voltage_per_phase: tuple[float, float, float] - """The ac voltage in volts (v) between the line and the neutral wire for phase/line - 1,2 and 3 respectively. - """ - - frequency: float - """The AC power frequency in Hertz (Hz).""" - - @classmethod - def from_proto(cls, raw: microgrid_pb.ComponentData) -> MeterData: - """Create MeterData from a protobuf message. - - Args: - raw: raw component data as decoded from the wire. - - Returns: - Instance of MeterData created from the protobuf message. - """ - meter_data = cls( - component_id=raw.id, - timestamp=raw.ts.ToDatetime(tzinfo=timezone.utc), - active_power=raw.meter.data.ac.power_active.value, - active_power_per_phase=( - raw.meter.data.ac.phase_1.power_active.value, - raw.meter.data.ac.phase_2.power_active.value, - raw.meter.data.ac.phase_3.power_active.value, - ), - current_per_phase=( - raw.meter.data.ac.phase_1.current.value, - raw.meter.data.ac.phase_2.current.value, - raw.meter.data.ac.phase_3.current.value, - ), - voltage_per_phase=( - raw.meter.data.ac.phase_1.voltage.value, - raw.meter.data.ac.phase_2.voltage.value, - raw.meter.data.ac.phase_3.voltage.value, - ), - frequency=raw.meter.data.ac.frequency.value, - ) - meter_data._set_raw(raw=raw) - return meter_data - - -@dataclass(frozen=True) -class BatteryData(ComponentData): - """A wrapper class for holding battery data.""" - - soc: float - """Battery's overall SoC in percent (%).""" - - soc_lower_bound: float - """The SoC below which discharge commands will be blocked by the system, - in percent (%). - """ - - soc_upper_bound: float - """The SoC above which charge commands will be blocked by the system, - in percent (%). - """ - - capacity: float - """The capacity of the battery in Wh (Watt-hour).""" - - # pylint: disable=line-too-long - power_inclusion_lower_bound: float - """Lower inclusion bound for battery power in watts. - - This is the lower limit of the range within which power requests are allowed for the - battery. - - See [`frequenz.api.common.metrics_pb2.Metric.system_inclusion_bounds`][] and - [`frequenz.api.common.metrics_pb2.Metric.system_exclusion_bounds`][] for more - details. - """ - - power_exclusion_lower_bound: float - """Lower exclusion bound for battery power in watts. - - This is the lower limit of the range within which power requests are not allowed for - the battery. - - See [`frequenz.api.common.metrics_pb2.Metric.system_inclusion_bounds`][] and - [`frequenz.api.common.metrics_pb2.Metric.system_exclusion_bounds`][] for more - details. - """ - - power_inclusion_upper_bound: float - """Upper inclusion bound for battery power in watts. - - This is the upper limit of the range within which power requests are allowed for the - battery. - - See [`frequenz.api.common.metrics_pb2.Metric.system_inclusion_bounds`][] and - [`frequenz.api.common.metrics_pb2.Metric.system_exclusion_bounds`][] for more - details. - """ - - power_exclusion_upper_bound: float - """Upper exclusion bound for battery power in watts. - - This is the upper limit of the range within which power requests are not allowed for - the battery. - - See [`frequenz.api.common.metrics_pb2.Metric.system_inclusion_bounds`][] and - [`frequenz.api.common.metrics_pb2.Metric.system_exclusion_bounds`][] for more - details. - """ - # pylint: enable=line-too-long - - temperature: float - """The (average) temperature reported by the battery, in Celsius (°C).""" - - _relay_state: battery_pb.RelayState.ValueType - """State of the battery relay.""" - - _component_state: battery_pb.ComponentState.ValueType - """State of the battery.""" - - _errors: list[battery_pb.Error] - """List of errors in protobuf struct.""" - - @classmethod - def from_proto(cls, raw: microgrid_pb.ComponentData) -> BatteryData: - """Create BatteryData from a protobuf message. - - Args: - raw: raw component data as decoded from the wire. - - Returns: - Instance of BatteryData created from the protobuf message. - """ - raw_power = raw.battery.data.dc.power - battery_data = cls( - component_id=raw.id, - timestamp=raw.ts.ToDatetime(tzinfo=timezone.utc), - soc=raw.battery.data.soc.avg, - soc_lower_bound=raw.battery.data.soc.system_inclusion_bounds.lower, - soc_upper_bound=raw.battery.data.soc.system_inclusion_bounds.upper, - capacity=raw.battery.properties.capacity, - power_inclusion_lower_bound=raw_power.system_inclusion_bounds.lower, - power_exclusion_lower_bound=raw_power.system_exclusion_bounds.lower, - power_inclusion_upper_bound=raw_power.system_inclusion_bounds.upper, - power_exclusion_upper_bound=raw_power.system_exclusion_bounds.upper, - temperature=raw.battery.data.temperature.avg, - _relay_state=raw.battery.state.relay_state, - _component_state=raw.battery.state.component_state, - _errors=list(raw.battery.errors), - ) - battery_data._set_raw(raw=raw) - return battery_data - - -@dataclass(frozen=True) -class InverterData(ComponentData): - """A wrapper class for holding inverter data.""" - - active_power: float - """The 3-phase active power, in Watts, represented in the passive sign convention. - +ve current means consumption, away from the grid. - -ve current means supply into the grid. - """ - - active_power_per_phase: tuple[float, float, float] - """The AC active power for phase/line 1, 2 and 3 respectively.""" - - current_per_phase: tuple[float, float, float] - """AC current in Amperes (A) for phase/line 1, 2 and 3 respectively. - +ve current means consumption, away from the grid. - -ve current means supply into the grid. - """ - - voltage_per_phase: tuple[float, float, float] - """The AC voltage in Volts (V) between the line and the neutral wire for - phase/line 1, 2 and 3 respectively. - """ - - # pylint: disable=line-too-long - active_power_inclusion_lower_bound: float - """Lower inclusion bound for inverter power in watts. - - This is the lower limit of the range within which power requests are allowed for the - inverter. - - See [`frequenz.api.common.metrics_pb2.Metric.system_inclusion_bounds`][] and - [`frequenz.api.common.metrics_pb2.Metric.system_exclusion_bounds`][] for more - details. - """ - - active_power_exclusion_lower_bound: float - """Lower exclusion bound for inverter power in watts. - - This is the lower limit of the range within which power requests are not allowed for - the inverter. - - See [`frequenz.api.common.metrics_pb2.Metric.system_inclusion_bounds`][] and - [`frequenz.api.common.metrics_pb2.Metric.system_exclusion_bounds`][] for more - details. - """ - - active_power_inclusion_upper_bound: float - """Upper inclusion bound for inverter power in watts. - - This is the upper limit of the range within which power requests are allowed for the - inverter. - - See [`frequenz.api.common.metrics_pb2.Metric.system_inclusion_bounds`][] and - [`frequenz.api.common.metrics_pb2.Metric.system_exclusion_bounds`][] for more - details. - """ - - active_power_exclusion_upper_bound: float - """Upper exclusion bound for inverter power in watts. - - This is the upper limit of the range within which power requests are not allowed for - the inverter. - - See [`frequenz.api.common.metrics_pb2.Metric.system_inclusion_bounds`][] and - [`frequenz.api.common.metrics_pb2.Metric.system_exclusion_bounds`][] for more - details. - """ - # pylint: enable=line-too-long - - frequency: float - """AC frequency, in Hertz (Hz).""" - - _component_state: inverter_pb.ComponentState.ValueType - """State of the inverter.""" - - _errors: list[inverter_pb.Error] - """List of errors from the component.""" - - @classmethod - def from_proto(cls, raw: microgrid_pb.ComponentData) -> InverterData: - """Create InverterData from a protobuf message. - - Args: - raw: raw component data as decoded from the wire. - - Returns: - Instance of InverterData created from the protobuf message. - """ - raw_power = raw.inverter.data.ac.power_active - inverter_data = cls( - component_id=raw.id, - timestamp=raw.ts.ToDatetime(tzinfo=timezone.utc), - active_power=raw.inverter.data.ac.power_active.value, - active_power_per_phase=( - raw.inverter.data.ac.phase_1.power_active.value, - raw.inverter.data.ac.phase_2.power_active.value, - raw.inverter.data.ac.phase_3.power_active.value, - ), - current_per_phase=( - raw.inverter.data.ac.phase_1.current.value, - raw.inverter.data.ac.phase_2.current.value, - raw.inverter.data.ac.phase_3.current.value, - ), - voltage_per_phase=( - raw.inverter.data.ac.phase_1.voltage.value, - raw.inverter.data.ac.phase_2.voltage.value, - raw.inverter.data.ac.phase_3.voltage.value, - ), - active_power_inclusion_lower_bound=raw_power.system_inclusion_bounds.lower, - active_power_exclusion_lower_bound=raw_power.system_exclusion_bounds.lower, - active_power_inclusion_upper_bound=raw_power.system_inclusion_bounds.upper, - active_power_exclusion_upper_bound=raw_power.system_exclusion_bounds.upper, - frequency=raw.inverter.data.ac.frequency.value, - _component_state=raw.inverter.state.component_state, - _errors=list(raw.inverter.errors), - ) - - inverter_data._set_raw(raw=raw) - return inverter_data - - -@dataclass(frozen=True) -class EVChargerData(ComponentData): - """A wrapper class for holding ev_charger data.""" - - active_power: float - """The 3-phase active power, in Watts, represented in the passive sign convention. - +ve current means consumption, away from the grid. - -ve current means supply into the grid. - """ - - active_power_per_phase: tuple[float, float, float] - """The AC active power for phase/line 1,2 and 3 respectively.""" - - current_per_phase: tuple[float, float, float] - """AC current in Amperes (A) for phase/line 1,2 and 3 respectively. - +ve current means consumption, away from the grid. - -ve current means supply into the grid. - """ - - voltage_per_phase: tuple[float, float, float] - """The AC voltage in Volts (V) between the line and the neutral - wire for phase/line 1,2 and 3 respectively. - """ - - active_power_inclusion_lower_bound: float - """Lower inclusion bound for EV charger power in watts. - - This is the lower limit of the range within which power requests are allowed for the - EV charger. - - See [`frequenz.api.common.metrics_pb2.Metric.system_inclusion_bounds`][] and - [`frequenz.api.common.metrics_pb2.Metric.system_exclusion_bounds`][] for more - details. - """ - - active_power_exclusion_lower_bound: float - """Lower exclusion bound for EV charger power in watts. - - This is the lower limit of the range within which power requests are not allowed for - the EV charger. - - See [`frequenz.api.common.metrics_pb2.Metric.system_inclusion_bounds`][] and - [`frequenz.api.common.metrics_pb2.Metric.system_exclusion_bounds`][] for more - details. - """ - - active_power_inclusion_upper_bound: float - """Upper inclusion bound for EV charger power in watts. - - This is the upper limit of the range within which power requests are allowed for the - EV charger. - - See [`frequenz.api.common.metrics_pb2.Metric.system_inclusion_bounds`][] and - [`frequenz.api.common.metrics_pb2.Metric.system_exclusion_bounds`][] for more - details. - """ - - active_power_exclusion_upper_bound: float - """Upper exclusion bound for EV charger power in watts. - - This is the upper limit of the range within which power requests are not allowed for - the EV charger. - - See [`frequenz.api.common.metrics_pb2.Metric.system_inclusion_bounds`][] and - [`frequenz.api.common.metrics_pb2.Metric.system_exclusion_bounds`][] for more - details. - """ - - frequency: float - """AC frequency, in Hertz (Hz).""" - - cable_state: EVChargerCableState - """The state of the ev charger's cable.""" - - component_state: EVChargerComponentState - """The state of the ev charger.""" - - @classmethod - def from_proto(cls, raw: microgrid_pb.ComponentData) -> EVChargerData: - """Create EVChargerData from a protobuf message. - - Args: - raw: raw component data as decoded from the wire. - - Returns: - Instance of EVChargerData created from the protobuf message. - """ - raw_power = raw.ev_charger.data.ac.power_active - ev_charger_data = cls( - component_id=raw.id, - timestamp=raw.ts.ToDatetime(tzinfo=timezone.utc), - active_power=raw_power.value, - active_power_per_phase=( - raw.ev_charger.data.ac.phase_1.power_active.value, - raw.ev_charger.data.ac.phase_2.power_active.value, - raw.ev_charger.data.ac.phase_3.power_active.value, - ), - current_per_phase=( - raw.ev_charger.data.ac.phase_1.current.value, - raw.ev_charger.data.ac.phase_2.current.value, - raw.ev_charger.data.ac.phase_3.current.value, - ), - voltage_per_phase=( - raw.ev_charger.data.ac.phase_1.voltage.value, - raw.ev_charger.data.ac.phase_2.voltage.value, - raw.ev_charger.data.ac.phase_3.voltage.value, - ), - active_power_inclusion_lower_bound=raw_power.system_inclusion_bounds.lower, - active_power_exclusion_lower_bound=raw_power.system_exclusion_bounds.lower, - active_power_inclusion_upper_bound=raw_power.system_inclusion_bounds.upper, - active_power_exclusion_upper_bound=raw_power.system_exclusion_bounds.upper, - cable_state=EVChargerCableState.from_pb(raw.ev_charger.state.cable_state), - component_state=EVChargerComponentState.from_pb( - raw.ev_charger.state.component_state - ), - frequency=raw.ev_charger.data.ac.frequency.value, - ) - ev_charger_data._set_raw(raw=raw) - return ev_charger_data - - def is_ev_connected(self) -> bool: - """Check whether an EV is connected to the charger. - - Returns: - When the charger is not in an error state, whether an EV is connected to - the charger. - """ - return self.component_state not in ( - EVChargerComponentState.AUTHORIZATION_REJECTED, - EVChargerComponentState.ERROR, - ) and self.cable_state in ( - EVChargerCableState.EV_LOCKED, - EVChargerCableState.EV_PLUGGED, - ) diff --git a/src/frequenz/sdk/microgrid/component/_component_states.py b/src/frequenz/sdk/microgrid/component/_component_states.py deleted file mode 100644 index 3b100dcbf..000000000 --- a/src/frequenz/sdk/microgrid/component/_component_states.py +++ /dev/null @@ -1,104 +0,0 @@ -# License: MIT -# Copyright © 2022 Frequenz Energy-as-a-Service GmbH - -"""Defines states of components that can be used in a microgrid.""" -from __future__ import annotations - -from enum import Enum - -from frequenz.api.microgrid import ev_charger_pb2 as ev_charger_pb - -# pylint: disable=no-member - - -class EVChargerCableState(Enum): - """Cable states of an EV Charger.""" - - UNSPECIFIED = ev_charger_pb.CableState.CABLE_STATE_UNSPECIFIED - """Unspecified cable state.""" - - UNPLUGGED = ev_charger_pb.CableState.CABLE_STATE_UNPLUGGED - """The cable is unplugged.""" - - CHARGING_STATION_PLUGGED = ( - ev_charger_pb.CableState.CABLE_STATE_CHARGING_STATION_PLUGGED - ) - """The cable is plugged into the charging station.""" - - CHARGING_STATION_LOCKED = ( - ev_charger_pb.CableState.CABLE_STATE_CHARGING_STATION_LOCKED - ) - """The cable is plugged into the charging station and locked.""" - - EV_PLUGGED = ev_charger_pb.CableState.CABLE_STATE_EV_PLUGGED - """The cable is plugged into the EV.""" - - EV_LOCKED = ev_charger_pb.CableState.CABLE_STATE_EV_LOCKED - """The cable is plugged into the EV and locked.""" - - @classmethod - def from_pb( - cls, evc_state: ev_charger_pb.CableState.ValueType - ) -> EVChargerCableState: - """Convert a protobuf CableState value to EVChargerCableState enum. - - Args: - evc_state: protobuf cable state to convert. - - Returns: - Enum value corresponding to the protobuf message. - """ - if not any(t.value == evc_state for t in EVChargerCableState): - return cls.UNSPECIFIED - - return EVChargerCableState(evc_state) - - -class EVChargerComponentState(Enum): - """Component State of an EV Charger.""" - - UNSPECIFIED = ev_charger_pb.ComponentState.COMPONENT_STATE_UNSPECIFIED - """Unspecified component state.""" - - STARTING = ev_charger_pb.ComponentState.COMPONENT_STATE_STARTING - """The component is starting.""" - - NOT_READY = ev_charger_pb.ComponentState.COMPONENT_STATE_NOT_READY - """The component is not ready.""" - - READY = ev_charger_pb.ComponentState.COMPONENT_STATE_READY - """The component is ready.""" - - CHARGING = ev_charger_pb.ComponentState.COMPONENT_STATE_CHARGING - """The component is charging.""" - - DISCHARGING = ev_charger_pb.ComponentState.COMPONENT_STATE_DISCHARGING - """The component is discharging.""" - - ERROR = ev_charger_pb.ComponentState.COMPONENT_STATE_ERROR - """The component is in error state.""" - - AUTHORIZATION_REJECTED = ( - ev_charger_pb.ComponentState.COMPONENT_STATE_AUTHORIZATION_REJECTED - ) - """The component rejected authorization.""" - - INTERRUPTED = ev_charger_pb.ComponentState.COMPONENT_STATE_INTERRUPTED - """The component is interrupted.""" - - @classmethod - def from_pb( - cls, evc_state: ev_charger_pb.ComponentState.ValueType - ) -> EVChargerComponentState: - """Convert a protobuf ComponentState value to EVChargerComponentState enum. - - Args: - evc_state: protobuf component state to convert. - - Returns: - Enum value corresponding to the protobuf message. - """ - if not any(t.value == evc_state for t in EVChargerComponentState): - return cls.UNSPECIFIED - - return EVChargerComponentState(evc_state) diff --git a/src/frequenz/sdk/microgrid/component_graph.py b/src/frequenz/sdk/microgrid/component_graph.py index cb13ff8a7..f29700737 100644 --- a/src/frequenz/sdk/microgrid/component_graph.py +++ b/src/frequenz/sdk/microgrid/component_graph.py @@ -22,15 +22,20 @@ """ import asyncio +import dataclasses import logging from abc import ABC, abstractmethod from collections.abc import Callable, Iterable from dataclasses import asdict import networkx as nx - -from .client import Connection, MicrogridApiClient -from .component import Component, ComponentCategory, InverterType +from frequenz.client.microgrid import ( + ApiClient, + Component, + ComponentCategory, + Connection, + InverterType, +) _logger = logging.getLogger(__name__) @@ -509,7 +514,7 @@ def refresh_from( for component in components: new_graph.add_node(component.component_id, **asdict(component)) - new_graph.add_edges_from(connections) + new_graph.add_edges_from(dataclasses.astuple(c) for c in connections) # check if we can construct a valid ComponentGraph # from the new NetworkX graph data @@ -536,7 +541,7 @@ def refresh_from( async def refresh_from_api( self, - api: MicrogridApiClient, + api: ApiClient, correct_errors: Callable[["_MicrogridComponentGraph"], None] | None = None, ) -> None: """Refresh the contents of a component graph from the remote API. diff --git a/src/frequenz/sdk/microgrid/connection_manager.py b/src/frequenz/sdk/microgrid/connection_manager.py index 7136ab5e7..f9dd3a654 100644 --- a/src/frequenz/sdk/microgrid/connection_manager.py +++ b/src/frequenz/sdk/microgrid/connection_manager.py @@ -12,11 +12,9 @@ from abc import ABC, abstractmethod import grpc.aio as grpcaio +from frequenz.client.microgrid import ApiClient, Location, Metadata -from .client import MicrogridApiClient -from .client._client import MicrogridGrpcClient from .component_graph import ComponentGraph, _MicrogridComponentGraph -from .metadata import Location, Metadata # Not public default host and port _DEFAULT_MICROGRID_HOST = "[::1]" @@ -59,8 +57,8 @@ def port(self) -> int: @property @abstractmethod - def api_client(self) -> MicrogridApiClient: - """Get MicrogridApiClient. + def api_client(self) -> ApiClient: + """Get ApiClient. Returns: api client @@ -117,7 +115,7 @@ def __init__( super().__init__(host, port) target = f"{host}:{port}" grpc_channel = grpcaio.insecure_channel(target) - self._api = MicrogridGrpcClient(grpc_channel, target) + self._api = ApiClient(grpc_channel, target) # To create graph from the api we need await. # So create empty graph here, and update it in `run` method. self._graph = _MicrogridComponentGraph() @@ -126,8 +124,8 @@ def __init__( """The metadata of the microgrid.""" @property - def api_client(self) -> MicrogridApiClient: - """Get MicrogridApiClient. + def api_client(self) -> ApiClient: + """Get ApiClient. Returns: api client @@ -172,7 +170,7 @@ async def _update_api(self, host: str, port: int) -> None: target = f"{host}:{port}" grpc_channel = grpcaio.insecure_channel(target) - self._api = MicrogridGrpcClient(grpc_channel, target) + self._api = ApiClient(grpc_channel, target) self._metadata = await self._api.metadata() await self._graph.refresh_from_api(self._api) diff --git a/src/frequenz/sdk/microgrid/metadata.py b/src/frequenz/sdk/microgrid/metadata.py deleted file mode 100644 index 804844430..000000000 --- a/src/frequenz/sdk/microgrid/metadata.py +++ /dev/null @@ -1,50 +0,0 @@ -# License: MIT -# Copyright © 2023 Frequenz Energy-as-a-Service GmbH - -"""Metadata that describes a microgrid.""" - -from dataclasses import dataclass -from zoneinfo import ZoneInfo - -from timezonefinder import TimezoneFinder - -_timezone_finder = TimezoneFinder() - - -@dataclass(frozen=True, kw_only=True) -class Location: - """Metadata for the location of microgrid.""" - - latitude: float | None = None - """The latitude of the microgrid in degree.""" - - longitude: float | None = None - """The longitude of the microgrid in degree.""" - - timezone: ZoneInfo | None = None - """The timezone of the microgrid. - - The timezone will be set to None if the latitude or longitude points - are not set or the timezone cannot be found given the location points. - """ - - def __post_init__(self) -> None: - """Initialize the timezone of the microgrid.""" - if self.latitude is None or self.longitude is None or self.timezone is not None: - return - - timezone = _timezone_finder.timezone_at(lat=self.latitude, lng=self.longitude) - if timezone: - # The dataclass is frozen, so it needs to use __setattr__ to set the timezone. - object.__setattr__(self, "timezone", ZoneInfo(key=timezone)) - - -@dataclass(frozen=True, kw_only=True) -class Metadata: - """Metadata for the microgrid.""" - - microgrid_id: int | None = None - """The ID of the microgrid.""" - - location: Location | None = None - """The location of the microgrid.""" diff --git a/src/frequenz/sdk/timeseries/_grid_frequency.py b/src/frequenz/sdk/timeseries/_grid_frequency.py index 624287d2d..a2de3c0c6 100644 --- a/src/frequenz/sdk/timeseries/_grid_frequency.py +++ b/src/frequenz/sdk/timeseries/_grid_frequency.py @@ -10,10 +10,10 @@ from typing import TYPE_CHECKING from frequenz.channels import Receiver, Sender +from frequenz.client.microgrid import Component, ComponentCategory, ComponentMetricId from ..actor import ChannelRegistry from ..microgrid import connection_manager -from ..microgrid.component import Component, ComponentCategory, ComponentMetricId from ..timeseries._base_types import Sample from ..timeseries._quantities import Frequency, Quantity diff --git a/src/frequenz/sdk/timeseries/_voltage_streamer.py b/src/frequenz/sdk/timeseries/_voltage_streamer.py index d7fee8c91..2c7eb85be 100644 --- a/src/frequenz/sdk/timeseries/_voltage_streamer.py +++ b/src/frequenz/sdk/timeseries/_voltage_streamer.py @@ -14,10 +14,10 @@ from typing import TYPE_CHECKING from frequenz.channels import Receiver, Sender +from frequenz.client.microgrid import Component, ComponentCategory, ComponentMetricId from ..actor import ChannelRegistry from ..microgrid import connection_manager -from ..microgrid.component import Component, ComponentCategory, ComponentMetricId from ..timeseries._base_types import Sample, Sample3Phase from ..timeseries._quantities import Quantity, Voltage diff --git a/src/frequenz/sdk/timeseries/battery_pool/_battery_pool_reference_store.py b/src/frequenz/sdk/timeseries/battery_pool/_battery_pool_reference_store.py index e4773ae98..553a8b335 100644 --- a/src/frequenz/sdk/timeseries/battery_pool/_battery_pool_reference_store.py +++ b/src/frequenz/sdk/timeseries/battery_pool/_battery_pool_reference_store.py @@ -11,6 +11,7 @@ from typing import Any from frequenz.channels import Receiver, Sender +from frequenz.client.microgrid import ComponentCategory from ..._internal._asyncio import cancel_and_await from ...actor._channel_registry import ChannelRegistry @@ -18,7 +19,6 @@ from ...actor._power_managing._base_classes import Proposal, ReportRequest from ...actor.power_distributing._component_status import ComponentPoolStatus from ...microgrid import connection_manager -from ...microgrid.component import ComponentCategory from ..formula_engine._formula_engine_pool import FormulaEnginePool from ._methods import MetricAggregator diff --git a/src/frequenz/sdk/timeseries/battery_pool/_component_metric_fetcher.py b/src/frequenz/sdk/timeseries/battery_pool/_component_metric_fetcher.py index 409a7d9cb..97c46f74e 100644 --- a/src/frequenz/sdk/timeseries/battery_pool/_component_metric_fetcher.py +++ b/src/frequenz/sdk/timeseries/battery_pool/_component_metric_fetcher.py @@ -14,6 +14,13 @@ from typing import Any, Generic, Self, TypeVar from frequenz.channels import ChannelClosedError, Receiver +from frequenz.client.microgrid import ( + BatteryData, + ComponentCategory, + ComponentData, + ComponentMetricId, + InverterData, +) from ..._internal._asyncio import AsyncConstructible from ..._internal._constants import MAX_BATTERY_DATA_AGE_SEC @@ -22,13 +29,6 @@ _InverterDataMethods, ) from ...microgrid import connection_manager -from ...microgrid.component import ( - BatteryData, - ComponentCategory, - ComponentData, - ComponentMetricId, - InverterData, -) from ._component_metrics import ComponentMetricsData _logger = logging.getLogger(__name__) diff --git a/src/frequenz/sdk/timeseries/battery_pool/_component_metrics.py b/src/frequenz/sdk/timeseries/battery_pool/_component_metrics.py index 2c9b3677c..5155b1b46 100644 --- a/src/frequenz/sdk/timeseries/battery_pool/_component_metrics.py +++ b/src/frequenz/sdk/timeseries/battery_pool/_component_metrics.py @@ -7,7 +7,7 @@ from collections.abc import Mapping from datetime import datetime -from ...microgrid.component import ComponentMetricId +from frequenz.client.microgrid import ComponentMetricId class ComponentMetricsData: diff --git a/src/frequenz/sdk/timeseries/battery_pool/_metric_calculator.py b/src/frequenz/sdk/timeseries/battery_pool/_metric_calculator.py index dc7cdb191..1b5dd9b16 100644 --- a/src/frequenz/sdk/timeseries/battery_pool/_metric_calculator.py +++ b/src/frequenz/sdk/timeseries/battery_pool/_metric_calculator.py @@ -11,6 +11,8 @@ from datetime import datetime, timezone from typing import Generic, TypeVar +from frequenz.client.microgrid import ComponentMetricId + from ... import timeseries from ..._internal import _math from ...actor.power_distributing._component_managers._battery_manager import ( @@ -20,7 +22,6 @@ _aggregate_battery_power_bounds, ) from ...actor.power_distributing.result import PowerBounds -from ...microgrid.component import ComponentMetricId from .._base_types import Sample, SystemBounds from .._quantities import Energy, Percentage, Power, Temperature from ._component_metrics import ComponentMetricsData diff --git a/src/frequenz/sdk/timeseries/ev_charger_pool/_ev_charger_pool.py b/src/frequenz/sdk/timeseries/ev_charger_pool/_ev_charger_pool.py index 940ee7602..9be3c44e8 100644 --- a/src/frequenz/sdk/timeseries/ev_charger_pool/_ev_charger_pool.py +++ b/src/frequenz/sdk/timeseries/ev_charger_pool/_ev_charger_pool.py @@ -13,11 +13,11 @@ from datetime import timedelta from frequenz.channels import Broadcast, ChannelClosedError, Receiver, Sender +from frequenz.client.microgrid import ComponentCategory, ComponentMetricId from ..._internal._asyncio import cancel_and_await from ...actor import ChannelRegistry, ComponentMetricRequest from ...microgrid import connection_manager -from ...microgrid.component import ComponentCategory, ComponentMetricId from .. import Sample, Sample3Phase from .._quantities import Current, Power, Quantity from ..formula_engine import FormulaEngine, FormulaEngine3Phase diff --git a/src/frequenz/sdk/timeseries/ev_charger_pool/_set_current_bounds.py b/src/frequenz/sdk/timeseries/ev_charger_pool/_set_current_bounds.py index de216b22c..a4b7f5292 100644 --- a/src/frequenz/sdk/timeseries/ev_charger_pool/_set_current_bounds.py +++ b/src/frequenz/sdk/timeseries/ev_charger_pool/_set_current_bounds.py @@ -10,11 +10,11 @@ from frequenz.channels import Broadcast, Sender from frequenz.channels.util import Timer, select, selected_from +from frequenz.client.microgrid import ComponentCategory from ..._internal._asyncio import cancel_and_await from ..._internal._channels import LatestValueCache from ...microgrid import connection_manager -from ...microgrid.component import ComponentCategory _logger = logging.getLogger(__name__) diff --git a/src/frequenz/sdk/timeseries/ev_charger_pool/_state_tracker.py b/src/frequenz/sdk/timeseries/ev_charger_pool/_state_tracker.py index abbf6c629..5acef5f9b 100644 --- a/src/frequenz/sdk/timeseries/ev_charger_pool/_state_tracker.py +++ b/src/frequenz/sdk/timeseries/ev_charger_pool/_state_tracker.py @@ -10,15 +10,15 @@ from frequenz.channels import Receiver from frequenz.channels.util import Merge - -from ... import microgrid -from ..._internal._asyncio import cancel_and_await -from ...microgrid.component import ( +from frequenz.client.microgrid import ( EVChargerCableState, EVChargerComponentState, EVChargerData, ) +from ... import microgrid +from ..._internal._asyncio import cancel_and_await + class EVChargerState(Enum): """State of individual EV charger.""" diff --git a/src/frequenz/sdk/timeseries/formula_engine/_formula_engine_pool.py b/src/frequenz/sdk/timeseries/formula_engine/_formula_engine_pool.py index 2ecfe21e6..4ddca096b 100644 --- a/src/frequenz/sdk/timeseries/formula_engine/_formula_engine_pool.py +++ b/src/frequenz/sdk/timeseries/formula_engine/_formula_engine_pool.py @@ -8,8 +8,8 @@ from typing import TYPE_CHECKING from frequenz.channels import Sender +from frequenz.client.microgrid import ComponentMetricId -from ...microgrid.component import ComponentMetricId from .._quantities import Current, Power, Quantity from ._formula_generators._formula_generator import ( FormulaGenerator, diff --git a/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_battery_power_formula.py b/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_battery_power_formula.py index 77d23ed01..f9f1af13c 100644 --- a/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_battery_power_formula.py +++ b/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_battery_power_formula.py @@ -5,8 +5,9 @@ import logging +from frequenz.client.microgrid import ComponentMetricId + from ....microgrid import connection_manager -from ....microgrid.component import ComponentMetricId from ..._quantities import Power from ...formula_engine import FormulaEngine from ._formula_generator import ( diff --git a/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_chp_power_formula.py b/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_chp_power_formula.py index 3a85b3964..755022d57 100644 --- a/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_chp_power_formula.py +++ b/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_chp_power_formula.py @@ -7,8 +7,9 @@ import logging from collections import abc +from frequenz.client.microgrid import ComponentCategory, ComponentMetricId + from ....microgrid import connection_manager -from ....microgrid.component import ComponentCategory, ComponentMetricId from ..._quantities import Power from ...formula_engine import FormulaEngine from ._formula_generator import ( diff --git a/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_consumer_power_formula.py b/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_consumer_power_formula.py index 393a4d82c..87c65689b 100644 --- a/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_consumer_power_formula.py +++ b/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_consumer_power_formula.py @@ -5,8 +5,9 @@ import logging +from frequenz.client.microgrid import Component, ComponentCategory, ComponentMetricId + from ....microgrid import connection_manager -from ....microgrid.component import Component, ComponentCategory, ComponentMetricId from ..._quantities import Power from .._formula_engine import FormulaEngine from .._resampled_formula_builder import ResampledFormulaBuilder diff --git a/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_ev_charger_current_formula.py b/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_ev_charger_current_formula.py index 0046d689a..bbcd17655 100644 --- a/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_ev_charger_current_formula.py +++ b/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_ev_charger_current_formula.py @@ -7,7 +7,8 @@ import logging from collections import abc -from ....microgrid.component import ComponentMetricId +from frequenz.client.microgrid import ComponentMetricId + from ..._quantities import Current from .._formula_engine import FormulaEngine, FormulaEngine3Phase from ._formula_generator import NON_EXISTING_COMPONENT_ID, FormulaGenerator diff --git a/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_ev_charger_power_formula.py b/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_ev_charger_power_formula.py index f738ed8c9..80ce5c624 100644 --- a/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_ev_charger_power_formula.py +++ b/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_ev_charger_power_formula.py @@ -5,7 +5,8 @@ import logging -from ....microgrid.component import ComponentMetricId +from frequenz.client.microgrid import ComponentMetricId + from ..._quantities import Power from .._formula_engine import FormulaEngine from ._formula_generator import NON_EXISTING_COMPONENT_ID, FormulaGenerator diff --git a/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_formula_generator.py b/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_formula_generator.py index 33c1c4b06..b766ae7c2 100644 --- a/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_formula_generator.py +++ b/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_formula_generator.py @@ -13,9 +13,9 @@ from typing import TYPE_CHECKING, Generic from frequenz.channels import Sender +from frequenz.client.microgrid import Component, ComponentCategory, ComponentMetricId -from ....microgrid import component, connection_manager -from ....microgrid.component import ComponentMetricId +from ....microgrid import connection_manager from ..._quantities import QuantityT from .._formula_engine import FormulaEngine, FormulaEngine3Phase from .._resampled_formula_builder import ResampledFormulaBuilder @@ -95,7 +95,7 @@ def _get_builder( ) return builder - def _get_grid_component(self) -> component.Component: + def _get_grid_component(self) -> Component: """ Get the grid component in the component graph. @@ -109,7 +109,7 @@ def _get_grid_component(self) -> component.Component: grid_component = next( iter( component_graph.components( - component_categories={component.ComponentCategory.GRID} + component_categories={ComponentCategory.GRID} ) ), None, @@ -119,7 +119,7 @@ def _get_grid_component(self) -> component.Component: return grid_component - def _get_grid_component_successors(self) -> set[component.Component]: + def _get_grid_component_successors(self) -> set[Component]: """Get the set of grid component successors in the component graph. Returns: diff --git a/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_grid_current_formula.py b/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_grid_current_formula.py index cc7f3fd7d..59e37309c 100644 --- a/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_grid_current_formula.py +++ b/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_grid_current_formula.py @@ -3,7 +3,8 @@ """Formula generator from component graph for 3-phase Grid Current.""" -from ....microgrid.component import Component, ComponentCategory, ComponentMetricId +from frequenz.client.microgrid import Component, ComponentCategory, ComponentMetricId + from ..._quantities import Current from .._formula_engine import FormulaEngine, FormulaEngine3Phase from ._formula_generator import FormulaGenerator diff --git a/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_grid_power_3_phase_formula.py b/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_grid_power_3_phase_formula.py index d69436fa3..e6fd279b8 100644 --- a/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_grid_power_3_phase_formula.py +++ b/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_grid_power_3_phase_formula.py @@ -3,7 +3,8 @@ """Formula generator from component graph for 3-phase Grid Power.""" -from ....microgrid.component import Component, ComponentCategory, ComponentMetricId +from frequenz.client.microgrid import Component, ComponentCategory, ComponentMetricId + from ..._quantities import Power from .._formula_engine import FormulaEngine, FormulaEngine3Phase from ._formula_generator import FormulaGenerator diff --git a/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_grid_power_formula.py b/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_grid_power_formula.py index 404d788e6..3d76800e3 100644 --- a/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_grid_power_formula.py +++ b/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_grid_power_formula.py @@ -3,7 +3,8 @@ """Formula generator from component graph for Grid Power.""" -from ....microgrid.component import ComponentCategory, ComponentMetricId +from frequenz.client.microgrid import ComponentCategory, ComponentMetricId + from ..._quantities import Power from .._formula_engine import FormulaEngine from ._formula_generator import FormulaGenerator diff --git a/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_producer_power_formula.py b/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_producer_power_formula.py index 84bbca7c4..a0433192e 100644 --- a/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_producer_power_formula.py +++ b/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_producer_power_formula.py @@ -5,8 +5,9 @@ import logging +from frequenz.client.microgrid import ComponentCategory, ComponentMetricId + from ....microgrid import connection_manager -from ....microgrid.component import ComponentCategory, ComponentMetricId from ..._quantities import Power from .._formula_engine import FormulaEngine from ._formula_generator import NON_EXISTING_COMPONENT_ID, FormulaGenerator diff --git a/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_pv_power_formula.py b/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_pv_power_formula.py index d207e8f70..b859f21d6 100644 --- a/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_pv_power_formula.py +++ b/src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_pv_power_formula.py @@ -5,8 +5,9 @@ import logging +from frequenz.client.microgrid import ComponentCategory, ComponentMetricId + from ....microgrid import connection_manager -from ....microgrid.component import ComponentCategory, ComponentMetricId from ..._quantities import Power from .._formula_engine import FormulaEngine from ._formula_generator import NON_EXISTING_COMPONENT_ID, FormulaGenerator diff --git a/src/frequenz/sdk/timeseries/formula_engine/_resampled_formula_builder.py b/src/frequenz/sdk/timeseries/formula_engine/_resampled_formula_builder.py index 792615b10..5da6a1a7b 100644 --- a/src/frequenz/sdk/timeseries/formula_engine/_resampled_formula_builder.py +++ b/src/frequenz/sdk/timeseries/formula_engine/_resampled_formula_builder.py @@ -9,8 +9,8 @@ from typing import TYPE_CHECKING from frequenz.channels import Receiver, Sender +from frequenz.client.microgrid import ComponentMetricId -from ...microgrid.component import ComponentMetricId from .. import Sample from .._quantities import Quantity, QuantityT from ._formula_engine import FormulaBuilder, FormulaEngine diff --git a/src/frequenz/sdk/timeseries/grid.py b/src/frequenz/sdk/timeseries/grid.py index c104c94ec..e3f99e5c3 100644 --- a/src/frequenz/sdk/timeseries/grid.py +++ b/src/frequenz/sdk/timeseries/grid.py @@ -14,10 +14,10 @@ from typing import TYPE_CHECKING from frequenz.channels import Sender +from frequenz.client.microgrid._component import ComponentCategory from ..microgrid import connection_manager -from ..microgrid.component._component import ComponentCategory -from . import Fuse +from ._fuse import Fuse from ._quantities import Current, Power from .formula_engine import FormulaEngine, FormulaEngine3Phase from .formula_engine._formula_engine_pool import FormulaEnginePool @@ -195,8 +195,10 @@ def initialize( # instead of the expected ComponentMetadata type. metadata = grid_connections[0].metadata if isinstance(metadata, dict): - fuse_dict = metadata.get("fuse", None) - fuse = Fuse(**fuse_dict) if fuse_dict else None + if fuse_dict := metadata.get("fuse", None): + fuse = Fuse( + max_current=Current.from_amperes(fuse_dict.get("max_current", 0.0)) + ) if fuse is None: _logger.warning("The grid connection point does not have a fuse") diff --git a/src/frequenz/sdk/timeseries/logical_meter/_logical_meter.py b/src/frequenz/sdk/timeseries/logical_meter/_logical_meter.py index 41122b083..ca511c108 100644 --- a/src/frequenz/sdk/timeseries/logical_meter/_logical_meter.py +++ b/src/frequenz/sdk/timeseries/logical_meter/_logical_meter.py @@ -7,9 +7,9 @@ import uuid from frequenz.channels import Sender +from frequenz.client.microgrid import ComponentMetricId from ...actor import ChannelRegistry, ComponentMetricRequest -from ...microgrid.component import ComponentMetricId from .._quantities import Power, Quantity from ..formula_engine import FormulaEngine from ..formula_engine._formula_engine_pool import FormulaEnginePool diff --git a/tests/actor/power_distributing/test_battery_distribution_algorithm.py b/tests/actor/power_distributing/test_battery_distribution_algorithm.py index 8e04bf5a8..2bb64136f 100644 --- a/tests/actor/power_distributing/test_battery_distribution_algorithm.py +++ b/tests/actor/power_distributing/test_battery_distribution_algorithm.py @@ -7,6 +7,7 @@ from dataclasses import dataclass from datetime import datetime, timezone +from frequenz.client.microgrid import BatteryData, InverterData from pytest import approx, raises from frequenz.sdk.actor.power_distributing._distribution_algorithm import ( @@ -16,7 +17,6 @@ InvBatPair, ) from frequenz.sdk.actor.power_distributing.result import PowerBounds -from frequenz.sdk.microgrid.component import BatteryData, InverterData from ...utils.component_data_wrapper import BatteryDataWrapper, InverterDataWrapper diff --git a/tests/actor/power_distributing/test_power_distributing.py b/tests/actor/power_distributing/test_power_distributing.py index 56bf0636f..2a04db734 100644 --- a/tests/actor/power_distributing/test_power_distributing.py +++ b/tests/actor/power_distributing/test_power_distributing.py @@ -16,6 +16,7 @@ from unittest.mock import MagicMock from frequenz.channels import Broadcast, Sender +from frequenz.client.microgrid import ComponentCategory from pytest_mock import MockerFixture from frequenz.sdk import microgrid @@ -39,7 +40,6 @@ Result, Success, ) -from frequenz.sdk.microgrid.component import ComponentCategory from frequenz.sdk.microgrid.component_graph import _MicrogridComponentGraph from frequenz.sdk.timeseries import Power diff --git a/tests/actor/test_battery_pool_status.py b/tests/actor/test_battery_pool_status.py index 17fbe9398..b3a56eda3 100644 --- a/tests/actor/test_battery_pool_status.py +++ b/tests/actor/test_battery_pool_status.py @@ -6,6 +6,7 @@ from datetime import timedelta from frequenz.channels import Broadcast +from frequenz.client.microgrid import ComponentCategory from pytest_mock import MockerFixture from frequenz.sdk.actor.power_distributing._component_pool_status_tracker import ( @@ -15,7 +16,6 @@ BatteryStatusTracker, ComponentPoolStatus, ) -from frequenz.sdk.microgrid.component import ComponentCategory from tests.timeseries.mock_microgrid import MockMicrogrid from .test_battery_status import battery_data, inverter_data diff --git a/tests/actor/test_battery_status.py b/tests/actor/test_battery_status.py index 6b8fff040..7529e0a8a 100644 --- a/tests/actor/test_battery_status.py +++ b/tests/actor/test_battery_status.py @@ -25,6 +25,7 @@ # pylint: enable=no-name-in-module from frequenz.channels import Broadcast, Receiver +from frequenz.client.microgrid import BatteryData, InverterData from pytest_mock import MockerFixture from time_machine import TimeMachineFixture @@ -34,7 +35,6 @@ ComponentStatusEnum, SetPowerResult, ) -from frequenz.sdk.microgrid.component import BatteryData, InverterData from tests.timeseries.mock_microgrid import MockMicrogrid from ..utils.component_data_wrapper import BatteryDataWrapper, InverterDataWrapper diff --git a/tests/actor/test_data_sourcing.py b/tests/actor/test_data_sourcing.py index 491fe5780..3901bd02c 100644 --- a/tests/actor/test_data_sourcing.py +++ b/tests/actor/test_data_sourcing.py @@ -5,6 +5,7 @@ from frequenz.api.common import components_pb2 as components_pb from frequenz.channels import Broadcast +from frequenz.client.microgrid import ComponentMetricId from frequenz.sdk.actor import ( ChannelRegistry, @@ -12,7 +13,6 @@ DataSourcingActor, ) from frequenz.sdk.microgrid import connection_manager -from frequenz.sdk.microgrid.component import ComponentMetricId from frequenz.sdk.timeseries import Quantity, Sample from tests.microgrid import mock_api diff --git a/tests/actor/test_resampling.py b/tests/actor/test_resampling.py index 43f2164a0..aa37fd0c1 100644 --- a/tests/actor/test_resampling.py +++ b/tests/actor/test_resampling.py @@ -11,6 +11,7 @@ import pytest import time_machine from frequenz.channels import Broadcast +from frequenz.client.microgrid import ComponentMetricId from frequenz.sdk.actor import ( ChannelRegistry, @@ -18,7 +19,6 @@ ComponentMetricsResamplingActor, ResamplerConfig, ) -from frequenz.sdk.microgrid.component import ComponentMetricId from frequenz.sdk.timeseries import Sample from frequenz.sdk.timeseries._quantities import Quantity diff --git a/tests/api_client/__init__.py b/tests/api_client/__init__.py deleted file mode 100644 index aa5b88cca..000000000 --- a/tests/api_client/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# License: MIT -# Copyright © 2022 Frequenz Energy-as-a-Service GmbH - -"""Tests for the api client.""" diff --git a/tests/api_client/test_api_client.py b/tests/api_client/test_api_client.py deleted file mode 100644 index 12a587477..000000000 --- a/tests/api_client/test_api_client.py +++ /dev/null @@ -1,120 +0,0 @@ -# License: MIT -# Copyright © 2022 Frequenz Energy-as-a-Service GmbH - -"""Tests for the `ApiClient` class.""" - -from abc import abstractmethod -from typing import Any - -from frequenz.sdk._api_client import ApiClient, ApiProtocol - - -class FakeApiClient(ApiClient): - """An abstract mock api client.""" - - @classmethod - def api_major_version(cls) -> int: - """Return the major version of the API supported by the client. - - Returns: - The major version of the API supported by the client. - """ - # Specifying the targeted API version here. - return 1 - - @classmethod - @abstractmethod - def api_type(cls) -> ApiProtocol: - """Return the API type.""" - - @abstractmethod - async def connect(self, connection_params: Any) -> None: - """Connect to the API.""" - - @abstractmethod - async def disconnect(self) -> None: - """Disconnect from the API.""" - - @abstractmethod - def get_data(self) -> str: - """Get data from the API.""" - - -class FakeGrpcClient(FakeApiClient): - """Supported API version is defined in the `FakeApiClient` class.""" - - is_connected: bool - - @classmethod - def api_type(cls) -> ApiProtocol: - """Return the API type.""" - # Specifying the API protocol here as gRPC. - return ApiProtocol.GRPC - - async def connect(self, connection_params: str) -> None: - """Connect to the API.""" - self.is_connected = True - - async def disconnect(self) -> None: - """Disconnect from the API.""" - self.is_connected = False - - def get_data(self) -> str: - """Get data from the API.""" - return "grpc data" - - -class FakeRestClient(FakeApiClient): - """Supported API version is defined in the `FakeApiClient` class.""" - - is_connected: bool - - @classmethod - def api_type(cls) -> ApiProtocol: - """Return the API type.""" - # Same as `FakeGrpcClient`, but targeting REST protocol here. - return ApiProtocol.REST - - async def connect(self, connection_params: str) -> None: - """Connect to the API.""" - self.is_connected = True - - async def disconnect(self) -> None: - """Disconnect from the API.""" - self.is_connected = False - - def get_data(self) -> str: - """Get data from the API.""" - return "rest data" - - -async def test_fake_grpc_client() -> None: - """Test fake grpc client.""" - assert FakeGrpcClient.api_major_version() == 1 - assert FakeGrpcClient.api_type() == ApiProtocol.GRPC - - client = FakeGrpcClient() - - await client.connect("[::1]:80") - assert client.is_connected - - await client.disconnect() - assert not client.is_connected - - assert client.get_data() == "grpc data" - - -async def test_fake_rest_client() -> None: - """Test fake rest client.""" - assert FakeRestClient.api_major_version() == 1 - assert FakeRestClient.api_type() == ApiProtocol.REST - - client = FakeRestClient() - - await client.connect("[::1]:80") - assert client.is_connected - - await client.disconnect() - assert not client.is_connected - - assert client.get_data() == "rest data" diff --git a/tests/microgrid/test_client.py b/tests/microgrid/test_client.py deleted file mode 100644 index f16f99b99..000000000 --- a/tests/microgrid/test_client.py +++ /dev/null @@ -1,546 +0,0 @@ -# License: MIT -# Copyright © 2022 Frequenz Energy-as-a-Service GmbH - -"""Tests for the microgrid client thin wrapper.""" - -import asyncio -import contextlib -from collections.abc import AsyncIterator - -import grpc -import pytest -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 google.protobuf.empty_pb2 import Empty # pylint: disable=no-name-in-module - -from frequenz.sdk.microgrid import client -from frequenz.sdk.microgrid.client import Connection, LinearBackoff -from frequenz.sdk.microgrid.component import ( - BatteryData, - Component, - ComponentCategory, - EVChargerData, - GridMetadata, - InverterData, - InverterType, - MeterData, -) -from frequenz.sdk.timeseries import Current, Fuse - -from . import mock_api - -# pylint: disable=missing-function-docstring,use-implicit-booleaness-not-comparison -# pylint: disable=missing-class-docstring,no-member - - -# This incrementing port is a hack to avoid the inherent flakiness of the approach of -# using a real GRPC (mock) server. The server seems to stay alive for a short time after -# the test is finished, which causes the next test to fail because the port is already -# in use. -# This is a workaround until we have a better solution. -# See https://github.com/frequenz-floss/frequenz-sdk-python/issues/662 -_CURRENT_PORT: int = 57897 - - -@contextlib.asynccontextmanager -async def _gprc_server( - servicer: mock_api.MockMicrogridServicer | None = None, -) -> AsyncIterator[tuple[mock_api.MockMicrogridServicer, client.MicrogridApiClient]]: - global _CURRENT_PORT # pylint: disable=global-statement - port = _CURRENT_PORT - _CURRENT_PORT += 1 - if servicer is None: - servicer = mock_api.MockMicrogridServicer() - server = mock_api.MockGrpcServer(servicer, port=port) - microgrid = client.MicrogridGrpcClient( - grpc.aio.insecure_channel(f"[::]:{port}"), - f"[::]:{port}", - retry_spec=LinearBackoff(interval=0.0, jitter=0.05), - ) - await server.start() - try: - yield servicer, microgrid - finally: - assert await server.graceful_shutdown() - - -class TestMicrogridGrpcClient: - """Tests for the microgrid client thin wrapper.""" - - async def test_components(self) -> None: - """Test the components() method.""" - async with _gprc_server() as (servicer, microgrid): - assert set(await microgrid.components()) == set() - - servicer.add_component( - 0, components_pb.ComponentCategory.COMPONENT_CATEGORY_METER - ) - assert set(await microgrid.components()) == { - Component(0, ComponentCategory.METER) - } - - servicer.add_component( - 0, components_pb.ComponentCategory.COMPONENT_CATEGORY_BATTERY - ) - assert set(await microgrid.components()) == { - Component(0, ComponentCategory.METER), - Component(0, ComponentCategory.BATTERY), - } - - servicer.add_component( - 0, components_pb.ComponentCategory.COMPONENT_CATEGORY_METER - ) - assert set(await microgrid.components()) == { - Component(0, ComponentCategory.METER), - Component(0, ComponentCategory.BATTERY), - Component(0, ComponentCategory.METER), - } - - # sensors are not counted as components by the API client - servicer.add_component( - 1, components_pb.ComponentCategory.COMPONENT_CATEGORY_SENSOR - ) - assert set(await microgrid.components()) == { - Component(0, ComponentCategory.METER), - Component(0, ComponentCategory.BATTERY), - Component(0, ComponentCategory.METER), - } - - servicer.set_components( - [ - (9, components_pb.ComponentCategory.COMPONENT_CATEGORY_METER), - (99, components_pb.ComponentCategory.COMPONENT_CATEGORY_INVERTER), - (666, components_pb.ComponentCategory.COMPONENT_CATEGORY_SENSOR), - (999, components_pb.ComponentCategory.COMPONENT_CATEGORY_BATTERY), - ] - ) - assert set(await microgrid.components()) == { - Component(9, ComponentCategory.METER), - Component(99, ComponentCategory.INVERTER, InverterType.NONE), - Component(999, ComponentCategory.BATTERY), - } - - servicer.set_components( - [ - (99, components_pb.ComponentCategory.COMPONENT_CATEGORY_SENSOR), - ( - 100, - components_pb.ComponentCategory.COMPONENT_CATEGORY_UNSPECIFIED, - ), - (104, components_pb.ComponentCategory.COMPONENT_CATEGORY_METER), - (105, components_pb.ComponentCategory.COMPONENT_CATEGORY_INVERTER), - (106, components_pb.ComponentCategory.COMPONENT_CATEGORY_BATTERY), - ( - 107, - components_pb.ComponentCategory.COMPONENT_CATEGORY_EV_CHARGER, - ), - (999, components_pb.ComponentCategory.COMPONENT_CATEGORY_SENSOR), - ] - ) - - servicer.add_component( - 101, - components_pb.ComponentCategory.COMPONENT_CATEGORY_GRID, - Current.from_amperes(123.0), - ) - - grid_max_current = Current.from_amperes(123.0) - grid_fuse = Fuse(grid_max_current) - - assert set(await microgrid.components()) == { - Component(100, ComponentCategory.NONE), - Component( - 101, - ComponentCategory.GRID, - None, - GridMetadata(fuse=grid_fuse), - ), - Component(104, ComponentCategory.METER), - Component(105, ComponentCategory.INVERTER, InverterType.NONE), - Component(106, ComponentCategory.BATTERY), - Component(107, ComponentCategory.EV_CHARGER), - } - - servicer.set_components( - [ - (9, components_pb.ComponentCategory.COMPONENT_CATEGORY_METER), - (666, components_pb.ComponentCategory.COMPONENT_CATEGORY_SENSOR), - (999, components_pb.ComponentCategory.COMPONENT_CATEGORY_BATTERY), - ] - ) - servicer.add_component( - 99, - components_pb.ComponentCategory.COMPONENT_CATEGORY_INVERTER, - None, - components_pb.InverterType.INVERTER_TYPE_BATTERY, - ) - - assert set(await microgrid.components()) == { - Component(9, ComponentCategory.METER), - Component(99, ComponentCategory.INVERTER, InverterType.BATTERY), - Component(999, ComponentCategory.BATTERY), - } - - async def test_connections(self) -> None: - """Test the connections() method.""" - async with _gprc_server() as (servicer, microgrid): - assert set(await microgrid.connections()) == set() - - servicer.add_connection(0, 0) - assert set(await microgrid.connections()) == {Connection(0, 0)} - - servicer.add_connection(7, 9) - servicer.add_component( - 7, - component_category=components_pb.ComponentCategory.COMPONENT_CATEGORY_BATTERY, - ) - servicer.add_component( - 9, - component_category=components_pb.ComponentCategory.COMPONENT_CATEGORY_INVERTER, - ) - assert set(await microgrid.connections()) == { - Connection(0, 0), - Connection(7, 9), - } - - servicer.add_connection(0, 0) - assert set(await microgrid.connections()) == { - Connection(0, 0), - Connection(7, 9), - Connection(0, 0), - } - - servicer.set_connections([(999, 9), (99, 19), (909, 101), (99, 91)]) - for component_id in [999, 99, 19, 909, 101, 91]: - servicer.add_component( - component_id, - components_pb.ComponentCategory.COMPONENT_CATEGORY_BATTERY, - ) - - assert set(await microgrid.connections()) == { - Connection(999, 9), - Connection(99, 19), - Connection(909, 101), - Connection(99, 91), - } - - for component_id in [1, 2, 3, 4, 5, 6, 7, 8]: - servicer.add_component( - component_id, - components_pb.ComponentCategory.COMPONENT_CATEGORY_BATTERY, - ) - - servicer.set_connections( - [ - (1, 2), - (2, 3), - (2, 4), - (2, 5), - (4, 3), - (4, 5), - (4, 6), - (5, 4), - (5, 7), - (5, 8), - ] - ) - assert set(await microgrid.connections()) == { - Connection(1, 2), - Connection(2, 3), - Connection(2, 4), - Connection(2, 5), - Connection(4, 3), - Connection(4, 5), - Connection(4, 6), - Connection(5, 4), - Connection(5, 7), - Connection(5, 8), - } - - # passing empty sets is the same as passing `None`, - # filter is ignored - assert set(await microgrid.connections(starts=set(), ends=set())) == { - Connection(1, 2), - Connection(2, 3), - Connection(2, 4), - Connection(2, 5), - Connection(4, 3), - Connection(4, 5), - Connection(4, 6), - Connection(5, 4), - Connection(5, 7), - Connection(5, 8), - } - - # include filter for connection start - assert set(await microgrid.connections(starts={1})) == {Connection(1, 2)} - - assert set(await microgrid.connections(starts={2})) == { - Connection(2, 3), - Connection(2, 4), - Connection(2, 5), - } - assert set(await microgrid.connections(starts={3})) == set() - - assert set(await microgrid.connections(starts={4, 5})) == { - Connection(4, 3), - Connection(4, 5), - Connection(4, 6), - Connection(5, 4), - Connection(5, 7), - Connection(5, 8), - } - - # include filter for connection end - assert set(await microgrid.connections(ends={1})) == set() - - assert set(await microgrid.connections(ends={3})) == { - Connection(2, 3), - Connection(4, 3), - } - - assert set(await microgrid.connections(ends={2, 4, 5})) == { - Connection(1, 2), - Connection(2, 4), - Connection(2, 5), - Connection(4, 5), - Connection(5, 4), - } - - # different filters combine with AND logic - assert set( - await microgrid.connections(starts={1, 2, 4}, ends={4, 5, 6}) - ) == { - Connection(2, 4), - Connection(2, 5), - Connection(4, 5), - Connection(4, 6), - } - - assert set(await microgrid.connections(starts={3, 5}, ends={7, 8})) == { - Connection(5, 7), - Connection(5, 8), - } - - assert set(await microgrid.connections(starts={1, 5}, ends={2, 7})) == { - Connection(1, 2), - Connection(5, 7), - } - - async def test_bad_connections(self) -> None: - """Validate that the client does not apply connection filters itself.""" - - class BadServicer(mock_api.MockMicrogridServicer): - # pylint: disable=unused-argument,invalid-name - def ListConnections( - self, - request: microgrid_pb.ConnectionFilter, - context: grpc.ServicerContext, - ) -> microgrid_pb.ConnectionList: - """Ignores supplied `ConnectionFilter`.""" - return microgrid_pb.ConnectionList(connections=self._connections) - - def ListAllComponents( - self, request: Empty, context: grpc.ServicerContext - ) -> microgrid_pb.ComponentList: - return microgrid_pb.ComponentList(components=self._components) - - async with _gprc_server(BadServicer()) as (servicer, microgrid): - assert list(await microgrid.connections()) == [] - for component_id in [1, 2, 3, 4, 5, 6, 7, 8, 9]: - servicer.add_component( - component_id, - components_pb.ComponentCategory.COMPONENT_CATEGORY_BATTERY, - ) - servicer.set_connections( - [ - (1, 2), - (1, 9), - (2, 3), - (3, 4), - (4, 5), - (5, 6), - (6, 7), - (7, 6), - (7, 9), - ] - ) - - unfiltered = { - Connection(1, 2), - Connection(1, 9), - Connection(2, 3), - Connection(3, 4), - Connection(4, 5), - Connection(5, 6), - Connection(6, 7), - Connection(7, 6), - Connection(7, 9), - } - - # because the application of filters is left to the server side, - # it doesn't matter what filters we set in the client if the - # server doesn't do its part - assert set(await microgrid.connections()) == unfiltered - assert set(await microgrid.connections(starts={1})) == unfiltered - assert set(await microgrid.connections(ends={9})) == unfiltered - assert ( - set(await microgrid.connections(starts={1, 7}, ends={3, 9})) - == unfiltered - ) - - async def test_meter_data(self) -> None: - """Test the meter_data() method.""" - async with _gprc_server() as (servicer, microgrid): - servicer.add_component( - 83, components_pb.ComponentCategory.COMPONENT_CATEGORY_METER - ) - servicer.add_component( - 38, components_pb.ComponentCategory.COMPONENT_CATEGORY_BATTERY - ) - - with pytest.raises(ValueError): - # should raise a ValueError for missing component_id - await microgrid.meter_data(20) - - with pytest.raises(ValueError): - # should raise a ValueError for wrong component category - await microgrid.meter_data(38) - receiver = await microgrid.meter_data(83) - await asyncio.sleep(0.2) - - latest = await anext(receiver) - assert isinstance(latest, MeterData) - assert latest.component_id == 83 - - async def test_battery_data(self) -> None: - """Test the battery_data() method.""" - async with _gprc_server() as (servicer, microgrid): - servicer.add_component( - 83, components_pb.ComponentCategory.COMPONENT_CATEGORY_BATTERY - ) - servicer.add_component( - 38, components_pb.ComponentCategory.COMPONENT_CATEGORY_INVERTER - ) - - with pytest.raises(ValueError): - # should raise a ValueError for missing component_id - await microgrid.meter_data(20) - - with pytest.raises(ValueError): - # should raise a ValueError for wrong component category - await microgrid.meter_data(38) - receiver = await microgrid.battery_data(83) - await asyncio.sleep(0.2) - - latest = await anext(receiver) - assert isinstance(latest, BatteryData) - assert latest.component_id == 83 - - async def test_inverter_data(self) -> None: - """Test the inverter_data() method.""" - async with _gprc_server() as (servicer, microgrid): - servicer.add_component( - 83, components_pb.ComponentCategory.COMPONENT_CATEGORY_INVERTER - ) - servicer.add_component( - 38, components_pb.ComponentCategory.COMPONENT_CATEGORY_BATTERY - ) - - with pytest.raises(ValueError): - # should raise a ValueError for missing component_id - await microgrid.meter_data(20) - - with pytest.raises(ValueError): - # should raise a ValueError for wrong component category - await microgrid.meter_data(38) - receiver = await microgrid.inverter_data(83) - await asyncio.sleep(0.2) - - latest = await anext(receiver) - assert isinstance(latest, InverterData) - assert latest.component_id == 83 - - async def test_ev_charger_data(self) -> None: - """Test the ev_charger_data() method.""" - async with _gprc_server() as (servicer, microgrid): - servicer.add_component( - 83, components_pb.ComponentCategory.COMPONENT_CATEGORY_EV_CHARGER - ) - servicer.add_component( - 38, components_pb.ComponentCategory.COMPONENT_CATEGORY_BATTERY - ) - - with pytest.raises(ValueError): - # should raise a ValueError for missing component_id - await microgrid.meter_data(20) - - with pytest.raises(ValueError): - # should raise a ValueError for wrong component category - await microgrid.meter_data(38) - receiver = await microgrid.ev_charger_data(83) - await asyncio.sleep(0.2) - - latest = await anext(receiver) - assert isinstance(latest, EVChargerData) - assert latest.component_id == 83 - - async def test_charge(self) -> None: - """Check if charge is able to charge component.""" - async with _gprc_server() as (servicer, microgrid): - servicer.add_component( - 83, components_pb.ComponentCategory.COMPONENT_CATEGORY_METER - ) - - await microgrid.set_power(component_id=83, power_w=12) - - assert servicer.latest_power is not None - assert servicer.latest_power.component_id == 83 - assert servicer.latest_power.power == 12 - - async def test_discharge(self) -> None: - """Check if discharge is able to discharge component.""" - async with _gprc_server() as (servicer, microgrid): - servicer.add_component( - 73, components_pb.ComponentCategory.COMPONENT_CATEGORY_METER - ) - - await microgrid.set_power(component_id=73, power_w=-15) - - assert servicer.latest_power is not None - assert servicer.latest_power.component_id == 73 - assert servicer.latest_power.power == -15 - - async def test_set_bounds(self) -> None: - """Check if set_bounds is able to set bounds for component.""" - async with _gprc_server() as (servicer, microgrid): - servicer.add_component( - 38, components_pb.ComponentCategory.COMPONENT_CATEGORY_INVERTER - ) - - num_calls = 4 - - target_metric = microgrid_pb.SetBoundsParam.TargetMetric - expected_bounds = [ - microgrid_pb.SetBoundsParam( - component_id=comp_id, - target_metric=target_metric.TARGET_METRIC_POWER_ACTIVE, - bounds=metrics_pb.Bounds(lower=-10, upper=2), - ) - for comp_id in range(num_calls) - ] - for cid in range(num_calls): - await microgrid.set_bounds(cid, -10.0, 2.0) - await asyncio.sleep(0.1) - - assert len(expected_bounds) == len(servicer.get_bounds()) - - def sort_key( - bound: microgrid_pb.SetBoundsParam, - ) -> microgrid_pb.SetBoundsParam.TargetMetric.ValueType: - return bound.target_metric - - assert sorted(servicer.get_bounds(), key=sort_key) == sorted( - expected_bounds, key=sort_key - ) diff --git a/tests/microgrid/test_component.py b/tests/microgrid/test_component.py deleted file mode 100644 index 1dda260c2..000000000 --- a/tests/microgrid/test_component.py +++ /dev/null @@ -1,95 +0,0 @@ -# License: MIT -# Copyright © 2022 Frequenz Energy-as-a-Service GmbH - -"""Tests for the microgrid component wrapper.""" - -import frequenz.api.common.components_pb2 as components_pb -import pytest - -import frequenz.sdk.microgrid.component._component as cp - -# pylint:disable=no-member - - -# pylint: disable=protected-access -def test_component_category_from_protobuf() -> None: - """Test the creating component category from protobuf.""" - assert ( - cp._component_category_from_protobuf( - components_pb.ComponentCategory.COMPONENT_CATEGORY_UNSPECIFIED - ) - == cp.ComponentCategory.NONE - ) - - assert ( - cp._component_category_from_protobuf( - components_pb.ComponentCategory.COMPONENT_CATEGORY_GRID - ) - == cp.ComponentCategory.GRID - ) - - assert ( - cp._component_category_from_protobuf( - components_pb.ComponentCategory.COMPONENT_CATEGORY_METER - ) - == cp.ComponentCategory.METER - ) - - assert ( - cp._component_category_from_protobuf( - components_pb.ComponentCategory.COMPONENT_CATEGORY_INVERTER - ) - == cp.ComponentCategory.INVERTER - ) - - assert ( - cp._component_category_from_protobuf( - components_pb.ComponentCategory.COMPONENT_CATEGORY_BATTERY - ) - == cp.ComponentCategory.BATTERY - ) - - assert ( - cp._component_category_from_protobuf( - components_pb.ComponentCategory.COMPONENT_CATEGORY_EV_CHARGER - ) - == cp.ComponentCategory.EV_CHARGER - ) - - assert cp._component_category_from_protobuf(666) == cp.ComponentCategory.NONE # type: ignore - - with pytest.raises(ValueError): - cp._component_category_from_protobuf( - components_pb.ComponentCategory.COMPONENT_CATEGORY_SENSOR - ) - - -# pylint: disable=invalid-name -def test_Component() -> None: - """Test the component category.""" - c0 = cp.Component(0, cp.ComponentCategory.GRID) - assert c0.is_valid() - - c1 = cp.Component(1, cp.ComponentCategory.GRID) - assert c1.is_valid() - - c4 = cp.Component(4, cp.ComponentCategory.METER) - assert c4.is_valid() - - c5 = cp.Component(5, cp.ComponentCategory.INVERTER) - assert c5.is_valid() - - c6 = cp.Component(6, cp.ComponentCategory.BATTERY) - assert c6.is_valid() - - c7 = cp.Component(7, cp.ComponentCategory.EV_CHARGER) - assert c7.is_valid() - - invalid_grid_id = cp.Component(-1, cp.ComponentCategory.GRID) - assert not invalid_grid_id.is_valid() - - invalid_type = cp.Component(666, -1) # type: ignore - assert not invalid_type.is_valid() - - another_invalid_type = cp.Component(666, 666) # type: ignore - assert not another_invalid_type.is_valid() diff --git a/tests/microgrid/test_component_data.py b/tests/microgrid/test_component_data.py deleted file mode 100644 index f30ba85c9..000000000 --- a/tests/microgrid/test_component_data.py +++ /dev/null @@ -1,90 +0,0 @@ -# License: MIT -# Copyright © 2022 Frequenz Energy-as-a-Service GmbH - -"""Tests for the microgrid component data.""" - -from datetime import datetime, timezone - -import pytest -from frequenz.api.common import metrics_pb2 -from frequenz.api.common.metrics import electrical_pb2 -from frequenz.api.microgrid import inverter_pb2, microgrid_pb2 -from google.protobuf import timestamp_pb2 - -from frequenz.sdk.microgrid.component import ComponentData, InverterData - -# pylint: disable=no-member - - -def test_component_data_abstract_class() -> None: - """Verify the base class ComponentData may not be instantiated.""" - with pytest.raises(TypeError): - # pylint: disable=abstract-class-instantiated - ComponentData(0, datetime.now(timezone.utc)) # type: ignore - - -def test_inverter_data() -> None: - """Verify the constructor for the InverterData class.""" - seconds = 1234567890 - - raw = microgrid_pb2.ComponentData( - id=5, - ts=timestamp_pb2.Timestamp(seconds=seconds), - inverter=inverter_pb2.Inverter( - state=inverter_pb2.State( - component_state=inverter_pb2.COMPONENT_STATE_DISCHARGING - ), - errors=[inverter_pb2.Error(msg="error message")], - data=inverter_pb2.Data( - dc_battery=None, - dc_solar=None, - temperature=None, - ac=electrical_pb2.AC( - frequency=metrics_pb2.Metric(value=50.1), - power_active=metrics_pb2.Metric( - value=100.2, - system_exclusion_bounds=metrics_pb2.Bounds( - lower=-501.0, upper=501.0 - ), - system_inclusion_bounds=metrics_pb2.Bounds( - lower=-51_000.0, upper=51_000.0 - ), - ), - phase_1=electrical_pb2.AC.ACPhase( - current=metrics_pb2.Metric(value=12.3), - voltage=metrics_pb2.Metric(value=229.8), - power_active=metrics_pb2.Metric(value=33.1), - ), - phase_2=electrical_pb2.AC.ACPhase( - current=metrics_pb2.Metric(value=23.4), - voltage=metrics_pb2.Metric(value=230.0), - power_active=metrics_pb2.Metric(value=33.3), - ), - phase_3=electrical_pb2.AC.ACPhase( - current=metrics_pb2.Metric(value=34.5), - voltage=metrics_pb2.Metric(value=230.2), - power_active=metrics_pb2.Metric(value=33.8), - ), - ), - ), - ), - ) - - inv_data = InverterData.from_proto(raw) - assert inv_data.component_id == 5 - assert inv_data.timestamp == datetime.fromtimestamp(seconds, timezone.utc) - assert ( # pylint: disable=protected-access - inv_data._component_state == inverter_pb2.COMPONENT_STATE_DISCHARGING - ) - assert inv_data._errors == [ # pylint: disable=protected-access - inverter_pb2.Error(msg="error message") - ] - assert inv_data.frequency == pytest.approx(50.1) - assert inv_data.active_power == pytest.approx(100.2) - assert inv_data.active_power_per_phase == pytest.approx((33.1, 33.3, 33.8)) - assert inv_data.current_per_phase == pytest.approx((12.3, 23.4, 34.5)) - assert inv_data.voltage_per_phase == pytest.approx((229.8, 230.0, 230.2)) - assert inv_data.active_power_inclusion_lower_bound == pytest.approx(-51_000.0) - assert inv_data.active_power_inclusion_upper_bound == pytest.approx(51_000.0) - assert inv_data.active_power_exclusion_lower_bound == pytest.approx(-501.0) - assert inv_data.active_power_exclusion_upper_bound == pytest.approx(501.0) diff --git a/tests/microgrid/test_connection.py b/tests/microgrid/test_connection.py deleted file mode 100644 index fc4898eeb..000000000 --- a/tests/microgrid/test_connection.py +++ /dev/null @@ -1,28 +0,0 @@ -# License: MIT -# Copyright © 2022 Frequenz Energy-as-a-Service GmbH - -"""Tests for the microgrid Connection type.""" - -from frequenz.sdk.microgrid import client - - -# pylint: disable=invalid-name -def test_Connection() -> None: - """Test the microgrid Connection type.""" - c00 = client.Connection(0, 0) - assert not c00.is_valid() - - c01 = client.Connection(0, 1) - assert c01.is_valid() - - c10 = client.Connection(1, 0) - assert not c10.is_valid() - - c11 = client.Connection(1, 1) - assert not c11.is_valid() - - c12 = client.Connection(1, 2) - assert c12.is_valid() - - c21 = client.Connection(2, 1) - assert c21.is_valid() diff --git a/tests/microgrid/test_datapipeline.py b/tests/microgrid/test_datapipeline.py index ba4e9e39d..5d85f8f04 100644 --- a/tests/microgrid/test_datapipeline.py +++ b/tests/microgrid/test_datapipeline.py @@ -10,11 +10,15 @@ import async_solipsism import pytest import time_machine +from frequenz.client.microgrid import ( + Component, + ComponentCategory, + Connection, + InverterType, +) from pytest_mock import MockerFixture from frequenz.sdk.microgrid._data_pipeline import _DataPipeline -from frequenz.sdk.microgrid.client import Connection -from frequenz.sdk.microgrid.component import Component, ComponentCategory, InverterType from frequenz.sdk.timeseries._resampling import ResamplerConfig from ..utils.mock_microgrid_client import MockMicrogridClient diff --git a/tests/microgrid/test_graph.py b/tests/microgrid/test_graph.py index 4cac1bf89..8829d3d51 100644 --- a/tests/microgrid/test_graph.py +++ b/tests/microgrid/test_graph.py @@ -12,16 +12,17 @@ import frequenz.api.common.components_pb2 as components_pb import grpc import pytest - -import frequenz.sdk.microgrid.component_graph as gr -from frequenz.sdk.microgrid.client import Connection, MicrogridGrpcClient -from frequenz.sdk.microgrid.component import ( +from frequenz.client.microgrid import ( + ApiClient, Component, ComponentCategory, + Connection, + Fuse, GridMetadata, InverterType, ) -from frequenz.sdk.timeseries import Current, Fuse + +import frequenz.sdk.microgrid.component_graph as gr from .mock_api import MockGrpcServer, MockMicrogridServicer @@ -860,7 +861,7 @@ async def test_refresh_from_api(self) -> None: await server.start() target = "[::]:58765" - client = MicrogridGrpcClient(grpc.aio.insecure_channel(target), target) + client = ApiClient(grpc.aio.insecure_channel(target), target) # both components and connections must be non-empty servicer.set_components([]) @@ -905,8 +906,7 @@ async def test_refresh_from_api(self) -> None: servicer.set_connections([(101, 111), (111, 131)]) await graph.refresh_from_api(client) - grid_max_current = Current.zero() - grid_fuse = Fuse(grid_max_current) + grid_fuse = Fuse(max_current=0.0) # Note: we need to add GriMetadata as a dict here, because that's what # the ComponentGraph does too, and we need to be able to compare the @@ -1430,7 +1430,7 @@ def test_graph_correction(self) -> None: } assert len(graph.components()) == len(expected) assert set(graph.components()) == expected - assert list(graph.connections()) == [(1, 2)] + assert list(graph.connections()) == [Connection(1, 2)] # invalid graph data that (for now at least) # cannot be corrected @@ -1444,7 +1444,7 @@ def test_graph_correction(self) -> None: # graph is still in last known good state assert len(graph.components()) == len(expected) assert set(graph.components()) == expected - assert list(graph.connections()) == [(1, 2)] + assert list(graph.connections()) == [Connection(1, 2)] # invalid graph data where there is no grid # endpoint but a node has the magic value 0 @@ -1461,7 +1461,7 @@ def test_graph_correction(self) -> None: # graph is still in last known good state assert len(graph.components()) == len(expected) assert set(graph.components()) == expected - assert list(graph.connections()) == [(1, 2)] + assert list(graph.connections()) == [Connection(1, 2)] # with the callback, this can be corrected graph.refresh_from( @@ -1476,4 +1476,4 @@ def test_graph_correction(self) -> None: assert len(graph.components()) == len(expected) assert set(graph.components()) == expected - assert list(graph.connections()) == [(0, 8)] + assert list(graph.connections()) == [Connection(0, 8)] diff --git a/tests/microgrid/test_grid.py b/tests/microgrid/test_grid.py index 51f5550e2..c25006fd3 100644 --- a/tests/microgrid/test_grid.py +++ b/tests/microgrid/test_grid.py @@ -5,17 +5,11 @@ from contextlib import AsyncExitStack +import frequenz.client.microgrid as client from pytest_mock import MockerFixture import frequenz.sdk.microgrid.component_graph as gr from frequenz.sdk import microgrid -from frequenz.sdk.microgrid.client import Connection -from frequenz.sdk.microgrid.component import ( - Component, - ComponentCategory, - ComponentMetricId, - GridMetadata, -) from frequenz.sdk.timeseries import Current, Fuse, Power, Quantity from ..timeseries._formula_engine.utils import equal_float_lists, get_resampled_stream @@ -30,11 +24,11 @@ async def test_grid_1(mocker: MockerFixture) -> None: # validate that islands with no grid connection are accepted. components = { - Component(1, ComponentCategory.NONE), - Component(2, ComponentCategory.METER), + client.Component(1, client.ComponentCategory.NONE), + client.Component(2, client.ComponentCategory.METER), } connections = { - Connection(1, 2), + client.Connection(1, 2), } graph = gr._MicrogridComponentGraph( # pylint: disable=protected-access @@ -51,25 +45,19 @@ async def test_grid_1(mocker: MockerFixture) -> None: assert grid.fuse.max_current == Current.from_amperes(0.0) -def _create_fuse() -> Fuse: - """Create a fuse with a fixed current. - - Returns: - Fuse: The fuse. - """ - fuse_current = Current.from_amperes(123.0) - fuse = Fuse(fuse_current) - return fuse - - async def test_grid_2(mocker: MockerFixture) -> None: """Validate that microgrids with one grid connection are accepted.""" components = { - Component(1, ComponentCategory.GRID, None, GridMetadata(_create_fuse())), - Component(2, ComponentCategory.METER), + client.Component( + 1, + client.ComponentCategory.GRID, + None, + client.GridMetadata(client.Fuse(123.0)), + ), + client.Component(2, client.ComponentCategory.METER), } connections = { - Connection(1, 2), + client.Connection(1, 2), } graph = gr._MicrogridComponentGraph( # pylint: disable=protected-access @@ -81,20 +69,19 @@ async def test_grid_2(mocker: MockerFixture) -> None: assert grid is not None stack.push_async_callback(grid.stop) - expected_fuse_current = Current.from_amperes(123.0) - expected_fuse = Fuse(expected_fuse_current) - - assert grid.fuse == expected_fuse + assert grid.fuse == Fuse(max_current=Current.from_amperes(123.0)) async def test_grid_3(mocker: MockerFixture) -> None: """Validate that microgrids with a grid connection without a fuse are instantiated.""" components = { - Component(1, ComponentCategory.GRID, None, GridMetadata(None)), - Component(2, ComponentCategory.METER), + client.Component( + 1, client.ComponentCategory.GRID, None, client.GridMetadata(None) + ), + client.Component(2, client.ComponentCategory.METER), } connections = { - Connection(1, 2), + client.Connection(1, 2), } graph = gr._MicrogridComponentGraph( # pylint: disable=protected-access @@ -126,7 +113,7 @@ async def test_grid_power_1(mocker: MockerFixture) -> None: grid_meter_recv = get_resampled_stream( grid._formula_pool._namespace, # pylint: disable=protected-access mockgrid.meter_ids[0], - ComponentMetricId.ACTIVE_POWER, + client.ComponentMetricId.ACTIVE_POWER, Power.from_watts, ) @@ -170,7 +157,7 @@ async def test_grid_power_2(mocker: MockerFixture) -> None: get_resampled_stream( grid._formula_pool._namespace, # pylint: disable=protected-access component_id, - ComponentMetricId.ACTIVE_POWER, + client.ComponentMetricId.ACTIVE_POWER, Power.from_watts, ) for component_id in [ diff --git a/tests/microgrid/test_microgrid_api.py b/tests/microgrid/test_microgrid_api.py index d74ae3f38..8491b595a 100644 --- a/tests/microgrid/test_microgrid_api.py +++ b/tests/microgrid/test_microgrid_api.py @@ -4,16 +4,21 @@ """Tests of MicrogridApi.""" import asyncio +import zoneinfo from asyncio.tasks import ALL_COMPLETED from unittest import mock from unittest.mock import AsyncMock, MagicMock import pytest +from frequenz.client.microgrid import ( + Component, + ComponentCategory, + Connection, + Location, + Metadata, +) from frequenz.sdk.microgrid import connection_manager -from frequenz.sdk.microgrid import metadata as meta -from frequenz.sdk.microgrid.client import Connection -from frequenz.sdk.microgrid.component import Component, ComponentCategory class TestMicrogridApi: @@ -84,19 +89,19 @@ def connections(self) -> list[list[Connection]]: return connections @pytest.fixture - def metadata(self) -> meta.Metadata: + def metadata(self) -> Metadata: """Fetch the microgrid metadata. Returns: the microgrid metadata. """ - mock_timezone_finder = MagicMock() - mock_timezone_finder.timezone_at.return_value = "Europe/Berlin" - meta._timezone_finder = mock_timezone_finder # pylint: disable=protected-access - - return meta.Metadata( + return Metadata( microgrid_id=8, - location=meta.Location(latitude=52.520008, longitude=13.404954), + location=Location( + latitude=52.520008, + longitude=13.404954, + timezone=zoneinfo.ZoneInfo("Europe/Berlin"), + ), ) @mock.patch("grpc.aio.insecure_channel") @@ -105,7 +110,7 @@ async def test_connection_manager( _insecure_channel_mock: MagicMock, components: list[list[Component]], connections: list[list[Connection]], - metadata: meta.Metadata, + metadata: Metadata, ) -> None: """Test microgrid api. @@ -121,7 +126,7 @@ async def test_connection_manager( microgrid_client.metadata = AsyncMock(return_value=metadata) with mock.patch( - "frequenz.sdk.microgrid.connection_manager.MicrogridGrpcClient", + "frequenz.sdk.microgrid.connection_manager.ApiClient", return_value=microgrid_client, ): # Get instance without initializing git first. @@ -182,7 +187,7 @@ async def test_connection_manager_another_method( _insecure_channel_mock: MagicMock, components: list[list[Component]], connections: list[list[Connection]], - metadata: meta.Metadata, + metadata: Metadata, ) -> None: """Test if the api was not deallocated. diff --git a/tests/microgrid/test_retry.py b/tests/microgrid/test_retry.py deleted file mode 100644 index 776bc6b55..000000000 --- a/tests/microgrid/test_retry.py +++ /dev/null @@ -1,209 +0,0 @@ -# License: MIT -# Copyright © 2022 Frequenz Energy-as-a-Service GmbH - -"""Tests for retry strategies.""" - -# pylint: disable=chained-comparison - -from frequenz.sdk.microgrid.client import ( - ExponentialBackoff, - LinearBackoff, - RetryStrategy, -) - - -class TestLinearBackoff: - """Tests for the linear backoff retry strategy.""" - - def test_no_limit(self) -> None: - """Test base case.""" - interval = 3 - jitter = 0 - limit = None - retry = LinearBackoff(interval=interval, jitter=jitter, limit=limit) - - for _ in range(10): - assert retry.next_interval() == interval - - def test_iter(self) -> None: - """Test iterator.""" - assert list(LinearBackoff(1, 0, 3)) == [1, 1, 1] - - def test_with_limit(self) -> None: - """Test limit works.""" - interval = 3 - jitter = 0 - limit = 5 - retry: RetryStrategy = LinearBackoff( - interval=interval, jitter=jitter, limit=limit - ) - - for _ in range(limit): - assert retry.next_interval() == interval - assert retry.next_interval() is None - - retry.reset() - for _ in range(limit - 1): - assert retry.next_interval() == interval - retry.reset() - for _ in range(limit): - assert retry.next_interval() == interval - assert retry.next_interval() is None - - def test_with_jitter_no_limit(self) -> None: - """Test with jitter but no limit.""" - interval = 3 - jitter = 1 - limit = None - retry: RetryStrategy = LinearBackoff( - interval=interval, jitter=jitter, limit=limit - ) - - prev = 0.0 - for _ in range(5): - next_val = retry.next_interval() - assert next_val is not None - assert next_val > interval and next_val < (interval + jitter) - assert next_val != prev - prev = next_val - - def test_with_jitter_with_limit(self) -> None: - """Test with jitter and limit.""" - interval = 3 - jitter = 1 - limit = 2 - retry: RetryStrategy = LinearBackoff( - interval=interval, jitter=jitter, limit=limit - ) - - prev = 0.0 - for _ in range(2): - next_val = retry.next_interval() - assert next_val is not None - assert next_val > interval and next_val < (interval + jitter) - assert next_val != prev - prev = next_val - assert retry.next_interval() is None - - retry.reset() - next_val = retry.next_interval() - assert next_val is not None - assert next_val > interval and next_val < (interval + jitter) - assert next_val != prev - - def test_deep_copy(self) -> None: - """Test if deep copies are really deep copies.""" - retry = LinearBackoff(1.0, 0.0, 2) - - copy1 = retry.copy() - assert copy1.next_interval() == 1.0 - assert copy1.next_interval() == 1.0 - assert copy1.next_interval() is None - - copy2 = copy1.copy() - assert copy1.next_interval() is None - assert copy2.next_interval() == 1.0 - assert copy2.next_interval() == 1.0 - assert copy2.next_interval() is None - - -class TestExponentialBackoff: - """Tests for the exponential backoff retry strategy.""" - - def test_no_limit(self) -> None: - """Test base case.""" - retry = ExponentialBackoff(3, 30, 2, 0.0) - - assert retry.next_interval() == 3.0 - assert retry.next_interval() == 6.0 - assert retry.next_interval() == 12.0 - assert retry.next_interval() == 24.0 - assert retry.next_interval() == 30.0 - assert retry.next_interval() == 30.0 - - def test_with_limit(self) -> None: - """Test limit works.""" - retry = ExponentialBackoff(3, jitter=0.0, limit=3) - - assert retry.next_interval() == 3.0 - assert retry.next_interval() == 6.0 - assert retry.next_interval() == 12.0 - assert retry.next_interval() is None - - def test_deep_copy(self) -> None: - """Test if deep copies are really deep copies.""" - retry = ExponentialBackoff(3.0, 30.0, 2, 0.0, 2) - - copy1 = retry.copy() - assert copy1.next_interval() == 3.0 - assert copy1.next_interval() == 6.0 - assert copy1.next_interval() is None - - copy2 = copy1.copy() - assert copy1.next_interval() is None - assert copy2.next_interval() == 3.0 - assert copy2.next_interval() == 6.0 - assert copy2.next_interval() is None - - def test_with_jitter_no_limit(self) -> None: - """Test with jitter but no limit.""" - initial_interval = 3 - max_interval = 100 - jitter = 1 - multiplier = 2 - limit = None - retry: RetryStrategy = ExponentialBackoff( - initial_interval=initial_interval, - max_interval=max_interval, - multiplier=multiplier, - jitter=jitter, - limit=limit, - ) - - prev = 0.0 - for count in range(5): - next_val = retry.next_interval() - exp_backoff_interval = initial_interval * multiplier**count - assert next_val is not None - assert initial_interval <= next_val <= max_interval - assert next_val >= min(exp_backoff_interval, max_interval) - assert next_val <= min(exp_backoff_interval + jitter, max_interval) - assert next_val != prev - prev = next_val - - def test_with_jitter_with_limit(self) -> None: - """Test with jitter and limit.""" - initial_interval = 3 - max_interval = 100 - jitter = 1 - multiplier = 2 - limit = 2 - retry: RetryStrategy = ExponentialBackoff( - initial_interval=initial_interval, - max_interval=max_interval, - multiplier=multiplier, - jitter=jitter, - limit=limit, - ) - - prev = 0.0 - for count in range(2): - next_val = retry.next_interval() - exp_backoff_interval = initial_interval * multiplier**count - assert next_val is not None - assert initial_interval <= next_val <= max_interval - assert next_val >= min(exp_backoff_interval, max_interval) - assert next_val <= min(exp_backoff_interval + jitter, max_interval) - assert next_val != prev - prev = next_val - assert retry.next_interval() is None - - retry.reset() - next_val = retry.next_interval() - count = 0 - exp_backoff_interval = initial_interval * multiplier**count - assert next_val is not None - assert initial_interval <= next_val <= max_interval - assert next_val >= min(exp_backoff_interval, max_interval) - assert next_val <= min(exp_backoff_interval + jitter, max_interval) - assert next_val != prev diff --git a/tests/microgrid/test_timeout.py b/tests/microgrid/test_timeout.py deleted file mode 100644 index af9c4affd..000000000 --- a/tests/microgrid/test_timeout.py +++ /dev/null @@ -1,119 +0,0 @@ -# License: MIT -# Copyright © 2022 Frequenz Energy-as-a-Service GmbH - -"""Benchmark for microgrid data.""" -import time -from typing import Any -from unittest.mock import patch - -import grpc -import pytest - -# pylint: disable=no-name-in-module -from frequenz.api.microgrid.microgrid_pb2 import ( - ComponentFilter, - ComponentList, - ConnectionFilter, - ConnectionList, - PowerLevelParam, -) -from google.protobuf.empty_pb2 import Empty - -# pylint: enable=no-name-in-module -from pytest_mock import MockerFixture - -from frequenz.sdk.microgrid.client import MicrogridGrpcClient - -from .mock_api import MockGrpcServer, MockMicrogridServicer - -# Timeout applied to all gRPC calls under test. It is expected after that the gRPC -# calls will raise an AioRpcError with status code equal to DEADLINE_EXCEEDED. -GRPC_CALL_TIMEOUT: float = 0.1 - -# How much late a response to a gRPC call should be. It is used to trigger a timeout -# error and needs to be greater than `GRPC_CALL_TIMEOUT`. -GRPC_SERVER_DELAY: float = 0.3 - - -@patch( - "frequenz.sdk.microgrid.client._client.DEFAULT_GRPC_CALL_TIMEOUT", GRPC_CALL_TIMEOUT -) -async def test_components_timeout(mocker: MockerFixture) -> None: - """Test if the components() method properly raises AioRpcError.""" - servicer = MockMicrogridServicer() - - def mock_list_components( - request: ComponentFilter, context: Any # pylint: disable=unused-argument - ) -> ComponentList: - time.sleep(GRPC_SERVER_DELAY) - return ComponentList(components=[]) - - mocker.patch.object(servicer, "ListComponents", mock_list_components) - server = MockGrpcServer(servicer, port=57809) - await server.start() - - target = "[::]:57809" - grpc_channel = grpc.aio.insecure_channel(target) - client = MicrogridGrpcClient(grpc_channel=grpc_channel, target=target) - - with pytest.raises(grpc.aio.AioRpcError) as err_ctx: - _ = await client.components() - assert err_ctx.value.code() == grpc.StatusCode.DEADLINE_EXCEEDED - assert await server.graceful_shutdown() - - -@patch( - "frequenz.sdk.microgrid.client._client.DEFAULT_GRPC_CALL_TIMEOUT", GRPC_CALL_TIMEOUT -) -async def test_connections_timeout(mocker: MockerFixture) -> None: - """Test if the connections() method properly raises AioRpcError.""" - servicer = MockMicrogridServicer() - - def mock_list_connections( - request: ConnectionFilter, context: Any # pylint: disable=unused-argument - ) -> ConnectionList: - time.sleep(GRPC_SERVER_DELAY) - return ConnectionList(connections=[]) - - mocker.patch.object(servicer, "ListConnections", mock_list_connections) - server = MockGrpcServer(servicer, port=57809) - await server.start() - - target = "[::]:57809" - grpc_channel = grpc.aio.insecure_channel(target) - client = MicrogridGrpcClient(grpc_channel=grpc_channel, target=target) - - with pytest.raises(grpc.aio.AioRpcError) as err_ctx: - _ = await client.connections() - assert err_ctx.value.code() == grpc.StatusCode.DEADLINE_EXCEEDED - assert await server.graceful_shutdown() - - -@patch( - "frequenz.sdk.microgrid.client._client.DEFAULT_GRPC_CALL_TIMEOUT", GRPC_CALL_TIMEOUT -) -async def test_set_power_timeout(mocker: MockerFixture) -> None: - """Test if the set_power() method properly raises AioRpcError.""" - servicer = MockMicrogridServicer() - - def mock_set_power( - request: PowerLevelParam, context: Any # pylint: disable=unused-argument - ) -> Empty: - time.sleep(GRPC_SERVER_DELAY) - return Empty() - - mocker.patch.object(servicer, "SetPowerActive", mock_set_power) - server = MockGrpcServer(servicer, port=57809) - await server.start() - - target = "[::]:57809" - grpc_channel = grpc.aio.insecure_channel(target) - client = MicrogridGrpcClient(grpc_channel=grpc_channel, target=target) - - power_values = [-100, 100] - for power_w in power_values: - with pytest.raises(grpc.aio.AioRpcError) as err_ctx: - await client.set_power(component_id=1, power_w=power_w) - assert err_ctx.value.code() == grpc.StatusCode.DEADLINE_EXCEEDED - - assert await server.graceful_shutdown() diff --git a/tests/timeseries/_battery_pool/test_battery_pool.py b/tests/timeseries/_battery_pool/test_battery_pool.py index d65a3c7ab..1c2408fe9 100644 --- a/tests/timeseries/_battery_pool/test_battery_pool.py +++ b/tests/timeseries/_battery_pool/test_battery_pool.py @@ -19,6 +19,7 @@ import pytest import time_machine from frequenz.channels import Receiver, Sender +from frequenz.client.microgrid import ComponentCategory from pytest_mock import MockerFixture from frequenz.sdk import microgrid @@ -31,7 +32,6 @@ from frequenz.sdk.actor.power_distributing._component_managers._battery_manager import ( _get_battery_inverter_mappings, ) -from frequenz.sdk.microgrid.component import ComponentCategory from frequenz.sdk.timeseries import ( Bounds, Energy, diff --git a/tests/timeseries/_formula_engine/test_formula_composition.py b/tests/timeseries/_formula_engine/test_formula_composition.py index 6950789e1..5c0d13d63 100644 --- a/tests/timeseries/_formula_engine/test_formula_composition.py +++ b/tests/timeseries/_formula_engine/test_formula_composition.py @@ -8,10 +8,10 @@ from contextlib import AsyncExitStack import pytest +from frequenz.client.microgrid import ComponentMetricId from pytest_mock import MockerFixture from frequenz.sdk import microgrid -from frequenz.sdk.microgrid.component import ComponentMetricId from frequenz.sdk.timeseries._quantities import Power from ..mock_microgrid import MockMicrogrid diff --git a/tests/timeseries/_formula_engine/utils.py b/tests/timeseries/_formula_engine/utils.py index 921403ef6..3bbcd828f 100644 --- a/tests/timeseries/_formula_engine/utils.py +++ b/tests/timeseries/_formula_engine/utils.py @@ -8,9 +8,9 @@ from math import isclose from frequenz.channels import Receiver +from frequenz.client.microgrid import ComponentMetricId from frequenz.sdk.microgrid import _data_pipeline -from frequenz.sdk.microgrid.component import ComponentMetricId from frequenz.sdk.timeseries import Sample from frequenz.sdk.timeseries._quantities import QuantityT from frequenz.sdk.timeseries.formula_engine._resampled_formula_builder import ( diff --git a/tests/timeseries/mock_microgrid.py b/tests/timeseries/mock_microgrid.py index 983758242..1d1eb164f 100644 --- a/tests/timeseries/mock_microgrid.py +++ b/tests/timeseries/mock_microgrid.py @@ -10,24 +10,24 @@ from datetime import datetime, timedelta, timezone from typing import Coroutine -from pytest_mock import MockerFixture - -from frequenz.sdk import microgrid -from frequenz.sdk._internal._asyncio import cancel_and_await -from frequenz.sdk.actor import ResamplerConfig -from frequenz.sdk.microgrid import _data_pipeline -from frequenz.sdk.microgrid.client import Connection -from frequenz.sdk.microgrid.component import ( +from frequenz.client.microgrid import ( Component, ComponentCategory, ComponentData, + Connection, EVChargerCableState, EVChargerComponentState, + Fuse, GridMetadata, InverterType, ) +from pytest_mock import MockerFixture + +from frequenz.sdk import microgrid +from frequenz.sdk._internal._asyncio import cancel_and_await +from frequenz.sdk.actor import ResamplerConfig +from frequenz.sdk.microgrid import _data_pipeline from frequenz.sdk.microgrid.component_graph import _MicrogridComponentGraph -from frequenz.sdk.timeseries import Current, Fuse from ..utils import MockMicrogridClient from ..utils.component_data_wrapper import ( @@ -61,7 +61,7 @@ def __init__( # pylint: disable=too-many-arguments num_values: int = 2000, sample_rate_s: float = 0.01, num_namespaces: int = 1, - fuse: Fuse | None = Fuse(Current.from_amperes(10_000.0)), + fuse: Fuse | None = Fuse(10_000.0), graph: _MicrogridComponentGraph | None = None, mocker: MockerFixture | None = None, ): diff --git a/tests/timeseries/mock_resampler.py b/tests/timeseries/mock_resampler.py index 5d19ab0eb..91538a23f 100644 --- a/tests/timeseries/mock_resampler.py +++ b/tests/timeseries/mock_resampler.py @@ -9,12 +9,12 @@ from datetime import datetime from frequenz.channels import Broadcast, Receiver, Sender +from frequenz.client.microgrid import ComponentMetricId from pytest_mock import MockerFixture from frequenz.sdk._internal._asyncio import cancel_and_await from frequenz.sdk.actor import ComponentMetricRequest, ResamplerConfig from frequenz.sdk.microgrid._data_pipeline import _DataPipeline -from frequenz.sdk.microgrid.component import ComponentMetricId from frequenz.sdk.timeseries import Sample from frequenz.sdk.timeseries._quantities import Quantity from frequenz.sdk.timeseries.formula_engine._formula_generators._formula_generator import ( diff --git a/tests/timeseries/test_ev_charger_pool.py b/tests/timeseries/test_ev_charger_pool.py index 5e6620a14..d6afb260e 100644 --- a/tests/timeseries/test_ev_charger_pool.py +++ b/tests/timeseries/test_ev_charger_pool.py @@ -8,13 +8,10 @@ from contextlib import AsyncExitStack, asynccontextmanager from typing import Any, AsyncIterator +from frequenz.client.microgrid import EVChargerCableState, EVChargerComponentState from pytest_mock import MockerFixture from frequenz.sdk import microgrid -from frequenz.sdk.microgrid.component import ( - EVChargerCableState, - EVChargerComponentState, -) from frequenz.sdk.timeseries._quantities import Current, Power from frequenz.sdk.timeseries.ev_charger_pool._state_tracker import ( EVChargerState, diff --git a/tests/utils/component_data_streamer.py b/tests/utils/component_data_streamer.py index 16daf0423..f059a238d 100644 --- a/tests/utils/component_data_streamer.py +++ b/tests/utils/component_data_streamer.py @@ -8,8 +8,9 @@ from dataclasses import replace from datetime import datetime, timezone +from frequenz.client.microgrid import ComponentData + from frequenz.sdk._internal._asyncio import cancel_and_await -from frequenz.sdk.microgrid.component import ComponentData from .mock_microgrid_client import MockMicrogridClient diff --git a/tests/utils/component_data_wrapper.py b/tests/utils/component_data_wrapper.py index 8e5d5d9fc..f39176236 100644 --- a/tests/utils/component_data_wrapper.py +++ b/tests/utils/component_data_wrapper.py @@ -18,8 +18,7 @@ import frequenz.api.microgrid.battery_pb2 as battery_pb import frequenz.api.microgrid.inverter_pb2 as inverter_pb - -from frequenz.sdk.microgrid.component import ( +from frequenz.client.microgrid import ( BatteryData, EVChargerCableState, EVChargerComponentState, diff --git a/tests/utils/component_graph_utils.py b/tests/utils/component_graph_utils.py index b1cf40cac..380870485 100644 --- a/tests/utils/component_graph_utils.py +++ b/tests/utils/component_graph_utils.py @@ -6,8 +6,12 @@ from dataclasses import dataclass -from frequenz.sdk.microgrid.client import Connection -from frequenz.sdk.microgrid.component import Component, ComponentCategory, InverterType +from frequenz.client.microgrid import ( + Component, + ComponentCategory, + Connection, + InverterType, +) @dataclass diff --git a/tests/utils/graph_generator.py b/tests/utils/graph_generator.py index 87e1b8041..d35f45b0c 100644 --- a/tests/utils/graph_generator.py +++ b/tests/utils/graph_generator.py @@ -6,9 +6,14 @@ from dataclasses import replace from typing import Any, overload -from frequenz.sdk.microgrid.client import Connection -from frequenz.sdk.microgrid.component import Component, ComponentCategory, InverterType -from frequenz.sdk.microgrid.component._component import ComponentType +from frequenz.client.microgrid import ( + Component, + ComponentCategory, + ComponentType, + Connection, + InverterType, +) + from frequenz.sdk.microgrid.component_graph import _MicrogridComponentGraph diff --git a/tests/utils/mock_microgrid_client.py b/tests/utils/mock_microgrid_client.py index 434205366..21cc08f46 100644 --- a/tests/utils/mock_microgrid_client.py +++ b/tests/utils/mock_microgrid_client.py @@ -7,26 +7,26 @@ from unittest.mock import AsyncMock, MagicMock from frequenz.channels import Broadcast, Receiver -from google.protobuf.empty_pb2 import Empty # pylint: disable=no-name-in-module -from pytest_mock import MockerFixture - -from frequenz.sdk._internal._constants import RECEIVER_MAX_SIZE -from frequenz.sdk.microgrid.client import Connection -from frequenz.sdk.microgrid.component import ( +from frequenz.client.microgrid import ( BatteryData, Component, ComponentCategory, ComponentData, + Connection, EVChargerData, InverterData, + Location, MeterData, ) +from google.protobuf.empty_pb2 import Empty # pylint: disable=no-name-in-module +from pytest_mock import MockerFixture + +from frequenz.sdk._internal._constants import RECEIVER_MAX_SIZE from frequenz.sdk.microgrid.component_graph import ( ComponentGraph, _MicrogridComponentGraph, ) from frequenz.sdk.microgrid.connection_manager import ConnectionManager -from frequenz.sdk.microgrid.metadata import Location class MockMicrogridClient: