From 75e2e41096f68d580c9fce6134f6a0eb016655bb Mon Sep 17 00:00:00 2001 From: Leandro Lucarella Date: Thu, 7 Mar 2024 13:48:21 +0100 Subject: [PATCH 01/11] Add dependency to `frequenz-client-microgrid` We'll start to use the new external microgrid client instead of the internal one. Signed-off-by: Leandro Lucarella --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) 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", From 7847a8843d18b531ab37c2e3f222281f4aaeaae8 Mon Sep 17 00:00:00 2001 From: Leandro Lucarella Date: Thu, 7 Mar 2024 14:49:24 +0100 Subject: [PATCH 02/11] Use the new external microgrid API client Signed-off-by: Leandro Lucarella --- .../power_distribution/power_distributor.py | 2 +- .../timeseries/benchmark_datasourcing.py | 2 +- .../_component_metric_request.py | 2 +- .../_data_sourcing/microgrid_api_source.py | 6 +-- .../_power_managing/_power_managing_actor.py | 2 +- .../_component_managers/_battery_manager.py | 2 +- .../_battery_status_tracker.py | 8 ++-- .../_battery_distribution_algorithm.py | 3 +- src/frequenz/sdk/microgrid/_power_wrapper.py | 2 +- src/frequenz/sdk/microgrid/component_graph.py | 12 ++++-- .../sdk/microgrid/connection_manager.py | 16 ++++---- .../sdk/timeseries/_grid_frequency.py | 2 +- .../sdk/timeseries/_voltage_streamer.py | 2 +- .../_battery_pool_reference_store.py | 2 +- .../battery_pool/_component_metric_fetcher.py | 14 +++---- .../battery_pool/_component_metrics.py | 2 +- .../battery_pool/_metric_calculator.py | 3 +- .../ev_charger_pool/_ev_charger_pool.py | 2 +- .../ev_charger_pool/_set_current_bounds.py | 2 +- .../ev_charger_pool/_state_tracker.py | 8 ++-- .../formula_engine/_formula_engine_pool.py | 2 +- .../_battery_power_formula.py | 3 +- .../_formula_generators/_chp_power_formula.py | 3 +- .../_consumer_power_formula.py | 3 +- .../_ev_charger_current_formula.py | 3 +- .../_ev_charger_power_formula.py | 3 +- .../_formula_generators/_formula_generator.py | 10 ++--- .../_grid_current_formula.py | 3 +- .../_grid_power_3_phase_formula.py | 3 +- .../_grid_power_formula.py | 3 +- .../_producer_power_formula.py | 3 +- .../_formula_generators/_pv_power_formula.py | 3 +- .../_resampled_formula_builder.py | 2 +- src/frequenz/sdk/timeseries/grid.py | 2 +- .../logical_meter/_logical_meter.py | 2 +- .../test_battery_distribution_algorithm.py | 2 +- .../test_power_distributing.py | 2 +- tests/actor/test_battery_pool_status.py | 2 +- tests/actor/test_battery_status.py | 2 +- tests/actor/test_data_sourcing.py | 2 +- tests/actor/test_resampling.py | 2 +- tests/microgrid/test_datapipeline.py | 8 +++- tests/microgrid/test_graph.py | 13 ++++--- tests/microgrid/test_grid.py | 37 ++++++++++--------- tests/microgrid/test_microgrid_api.py | 20 ++++++---- .../_battery_pool/test_battery_pool.py | 2 +- .../test_formula_composition.py | 2 +- tests/timeseries/_formula_engine/utils.py | 2 +- tests/timeseries/mock_microgrid.py | 18 ++++----- tests/timeseries/mock_resampler.py | 2 +- tests/timeseries/test_ev_charger_pool.py | 5 +-- tests/utils/component_data_streamer.py | 3 +- tests/utils/component_data_wrapper.py | 3 +- tests/utils/component_graph_utils.py | 8 +++- tests/utils/graph_generator.py | 11 ++++-- tests/utils/mock_microgrid_client.py | 14 +++---- 56 files changed, 166 insertions(+), 136 deletions(-) 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/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/_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/component_graph.py b/src/frequenz/sdk/microgrid/component_graph.py index cb13ff8a7..2100ac316 100644 --- a/src/frequenz/sdk/microgrid/component_graph.py +++ b/src/frequenz/sdk/microgrid/component_graph.py @@ -28,9 +28,13 @@ 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__) @@ -536,7 +540,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/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..5f634bd64 100644 --- a/src/frequenz/sdk/timeseries/grid.py +++ b/src/frequenz/sdk/timeseries/grid.py @@ -14,9 +14,9 @@ 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 ._quantities import Current, Power from .formula_engine import FormulaEngine, FormulaEngine3Phase 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/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..bfc5c0574 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([]) diff --git a/tests/microgrid/test_grid.py b/tests/microgrid/test_grid.py index 51f5550e2..7d160abe8 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 @@ -65,11 +59,16 @@ def _create_fuse() -> 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(_create_fuse()), + ), + client.Component(2, client.ComponentCategory.METER), } connections = { - Connection(1, 2), + client.Connection(1, 2), } graph = gr._MicrogridComponentGraph( # pylint: disable=protected-access @@ -90,11 +89,13 @@ async def test_grid_2(mocker: MockerFixture) -> None: 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 +127,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 +171,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..1c4af15b4 100644 --- a/tests/microgrid/test_microgrid_api.py +++ b/tests/microgrid/test_microgrid_api.py @@ -9,11 +9,15 @@ 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,7 +88,7 @@ def connections(self) -> list[list[Connection]]: return connections @pytest.fixture - def metadata(self) -> meta.Metadata: + def metadata(self) -> Metadata: """Fetch the microgrid metadata. Returns: @@ -94,7 +98,7 @@ def metadata(self) -> meta.Metadata: 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), ) @@ -105,7 +109,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 +125,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 +186,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/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..c25d4462f 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 ( 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: From a6027468ca8f5f14449ac66c80803c99d61436d6 Mon Sep 17 00:00:00 2001 From: Leandro Lucarella Date: Thu, 7 Mar 2024 14:51:09 +0100 Subject: [PATCH 03/11] Fix uses of `Connection` as if they were tuples The `Connection` type in the external microgrid API is a dataclass instead of a tuple, so we need to convert it when a tuple is expected. Signed-off-by: Leandro Lucarella --- src/frequenz/sdk/microgrid/component_graph.py | 3 ++- tests/microgrid/test_graph.py | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/frequenz/sdk/microgrid/component_graph.py b/src/frequenz/sdk/microgrid/component_graph.py index 2100ac316..f29700737 100644 --- a/src/frequenz/sdk/microgrid/component_graph.py +++ b/src/frequenz/sdk/microgrid/component_graph.py @@ -22,6 +22,7 @@ """ import asyncio +import dataclasses import logging from abc import ABC, abstractmethod from collections.abc import Callable, Iterable @@ -513,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 diff --git a/tests/microgrid/test_graph.py b/tests/microgrid/test_graph.py index bfc5c0574..596a2f62d 100644 --- a/tests/microgrid/test_graph.py +++ b/tests/microgrid/test_graph.py @@ -1431,7 +1431,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 @@ -1445,7 +1445,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 @@ -1462,7 +1462,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( @@ -1477,4 +1477,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)] From 88d47d55e6feaf0d04ca9ff057a4d3f4404f0045 Mon Sep 17 00:00:00 2001 From: Leandro Lucarella Date: Thu, 7 Mar 2024 14:53:26 +0100 Subject: [PATCH 04/11] Use the client `Fuse` types where appropriate In the tests we are sometimes mocking the client, so we need to use the client `Fuse` type instead of our internal `Fuse` type (that uses quantities). Signed-off-by: Leandro Lucarella --- tests/microgrid/test_graph.py | 3 +-- tests/microgrid/test_grid.py | 13 +------------ tests/timeseries/mock_microgrid.py | 2 +- 3 files changed, 3 insertions(+), 15 deletions(-) diff --git a/tests/microgrid/test_graph.py b/tests/microgrid/test_graph.py index 596a2f62d..8829d3d51 100644 --- a/tests/microgrid/test_graph.py +++ b/tests/microgrid/test_graph.py @@ -906,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 diff --git a/tests/microgrid/test_grid.py b/tests/microgrid/test_grid.py index 7d160abe8..38be5b3ca 100644 --- a/tests/microgrid/test_grid.py +++ b/tests/microgrid/test_grid.py @@ -45,17 +45,6 @@ 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 = { @@ -63,7 +52,7 @@ async def test_grid_2(mocker: MockerFixture) -> None: 1, client.ComponentCategory.GRID, None, - client.GridMetadata(_create_fuse()), + client.GridMetadata(client.Fuse(123.0)), ), client.Component(2, client.ComponentCategory.METER), } diff --git a/tests/timeseries/mock_microgrid.py b/tests/timeseries/mock_microgrid.py index c25d4462f..1d1eb164f 100644 --- a/tests/timeseries/mock_microgrid.py +++ b/tests/timeseries/mock_microgrid.py @@ -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, ): From 991ae71fa1239e646ad067d33cff46c87ad1e380 Mon Sep 17 00:00:00 2001 From: Leandro Lucarella Date: Thu, 7 Mar 2024 14:54:08 +0100 Subject: [PATCH 05/11] Import from the leaf module To avoid circular dependencies, it is always better to import symbols from the leaf module internally. Signed-off-by: Leandro Lucarella --- src/frequenz/sdk/timeseries/grid.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/frequenz/sdk/timeseries/grid.py b/src/frequenz/sdk/timeseries/grid.py index 5f634bd64..7f86ed9a0 100644 --- a/src/frequenz/sdk/timeseries/grid.py +++ b/src/frequenz/sdk/timeseries/grid.py @@ -17,7 +17,7 @@ from frequenz.client.microgrid._component import ComponentCategory from ..microgrid import connection_manager -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 From fefafc5de82517ede394be30085dead7a79d2375 Mon Sep 17 00:00:00 2001 From: Leandro Lucarella Date: Thu, 7 Mar 2024 14:57:05 +0100 Subject: [PATCH 06/11] Don't pass blindly what comes from the API to `Fuse` Since now we have an internal `Fuse` type that uses quantities to avoid bugs, we need to make sure to properly convert between the client's `Fuse` type and the internal one. There is also actually a bug in the old code, as the conversion to `Current` never happened. This was never caught by `mypy` because we were using the `**args` syntax to create the object, so there is no type checking of the arguments, so a bad `Fuse` was being created, with a `float` `max_current` instead of using `Current`. We now make sure the conversion is done appropriately. Signed-off-by: Leandro Lucarella --- src/frequenz/sdk/timeseries/grid.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/frequenz/sdk/timeseries/grid.py b/src/frequenz/sdk/timeseries/grid.py index 7f86ed9a0..e3f99e5c3 100644 --- a/src/frequenz/sdk/timeseries/grid.py +++ b/src/frequenz/sdk/timeseries/grid.py @@ -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") From 2faa6f6ede072b8598dad511a8b4bfaae612c837 Mon Sep 17 00:00:00 2001 From: Leandro Lucarella Date: Thu, 7 Mar 2024 14:59:52 +0100 Subject: [PATCH 07/11] Remove some clutter from the test Signed-off-by: Leandro Lucarella --- tests/microgrid/test_grid.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/microgrid/test_grid.py b/tests/microgrid/test_grid.py index 38be5b3ca..c25006fd3 100644 --- a/tests/microgrid/test_grid.py +++ b/tests/microgrid/test_grid.py @@ -69,10 +69,7 @@ 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: From a8d8d9337f990e2989f32f63608e29fdf765cf41 Mon Sep 17 00:00:00 2001 From: Leandro Lucarella Date: Thu, 7 Mar 2024 15:01:07 +0100 Subject: [PATCH 08/11] Don't mock the internals of the microgrid client Since we are now using an external microgrid client, it is better to mock the location more shallowly, avoiding any timezone lookups completely. Signed-off-by: Leandro Lucarella --- tests/microgrid/test_microgrid_api.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/microgrid/test_microgrid_api.py b/tests/microgrid/test_microgrid_api.py index 1c4af15b4..8491b595a 100644 --- a/tests/microgrid/test_microgrid_api.py +++ b/tests/microgrid/test_microgrid_api.py @@ -4,6 +4,7 @@ """Tests of MicrogridApi.""" import asyncio +import zoneinfo from asyncio.tasks import ALL_COMPLETED from unittest import mock from unittest.mock import AsyncMock, MagicMock @@ -94,13 +95,13 @@ def metadata(self) -> 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 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") From 3d9849a1afc4ccf6d87267e319f7b90022e33d95 Mon Sep 17 00:00:00 2001 From: Leandro Lucarella Date: Thu, 7 Mar 2024 15:01:36 +0100 Subject: [PATCH 09/11] Remove internal microgrid API client Signed-off-by: Leandro Lucarella --- src/frequenz/sdk/microgrid/__init__.py | 5 +- src/frequenz/sdk/microgrid/client/__init__.py | 20 - src/frequenz/sdk/microgrid/client/_client.py | 657 ------------------ .../sdk/microgrid/client/_connection.py | 25 - src/frequenz/sdk/microgrid/client/_retry.py | 169 ----- .../sdk/microgrid/component/__init__.py | 40 -- .../sdk/microgrid/component/_component.py | 249 ------- .../microgrid/component/_component_data.py | 487 ------------- .../microgrid/component/_component_states.py | 104 --- src/frequenz/sdk/microgrid/metadata.py | 50 -- tests/microgrid/test_client.py | 546 --------------- tests/microgrid/test_component.py | 95 --- tests/microgrid/test_component_data.py | 90 --- tests/microgrid/test_connection.py | 28 - tests/microgrid/test_retry.py | 209 ------ tests/microgrid/test_timeout.py | 119 ---- 16 files changed, 1 insertion(+), 2892 deletions(-) delete mode 100644 src/frequenz/sdk/microgrid/client/__init__.py delete mode 100644 src/frequenz/sdk/microgrid/client/_client.py delete mode 100644 src/frequenz/sdk/microgrid/client/_connection.py delete mode 100644 src/frequenz/sdk/microgrid/client/_retry.py delete mode 100644 src/frequenz/sdk/microgrid/component/__init__.py delete mode 100644 src/frequenz/sdk/microgrid/component/_component.py delete mode 100644 src/frequenz/sdk/microgrid/component/_component_data.py delete mode 100644 src/frequenz/sdk/microgrid/component/_component_states.py delete mode 100644 src/frequenz/sdk/microgrid/metadata.py delete mode 100644 tests/microgrid/test_client.py delete mode 100644 tests/microgrid/test_component.py delete mode 100644 tests/microgrid/test_component_data.py delete mode 100644 tests/microgrid/test_connection.py delete mode 100644 tests/microgrid/test_retry.py delete mode 100644 tests/microgrid/test_timeout.py 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/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/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/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_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() From 58a25cf4c591589fefcee0f8f15ace63998bf120 Mon Sep 17 00:00:00 2001 From: Leandro Lucarella Date: Thu, 7 Mar 2024 15:03:49 +0100 Subject: [PATCH 10/11] Remove old and unused `_api_client` module Signed-off-by: Leandro Lucarella --- src/frequenz/sdk/_api_client/__init__.py | 8 -- src/frequenz/sdk/_api_client/api_client.py | 50 --------- tests/api_client/__init__.py | 4 - tests/api_client/test_api_client.py | 120 --------------------- 4 files changed, 182 deletions(-) delete mode 100644 src/frequenz/sdk/_api_client/__init__.py delete mode 100644 src/frequenz/sdk/_api_client/api_client.py delete mode 100644 tests/api_client/__init__.py delete mode 100644 tests/api_client/test_api_client.py 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/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" From 32adac9c9dfc57e72b786f2c715e11cf74b10ba3 Mon Sep 17 00:00:00 2001 From: Leandro Lucarella Date: Thu, 7 Mar 2024 15:10:10 +0100 Subject: [PATCH 11/11] Update release notes Signed-off-by: Leandro Lucarella --- RELEASE_NOTES.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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`.