diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index b4e9d02b9..77990748b 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -39,6 +39,8 @@ LoggingConfig(root_logger=RootLoggerConfig(level="ERROR")) ``` +* The SDK now depends on the `frequenz-client-microgrid` v0.7.x series. The main change is now all component and microgrid IDs need to be passed using the wrapper classes `ComponentId`/`MicrogridId` instead of `int`. + ## New Features diff --git a/benchmarks/power_distribution/power_distributor.py b/benchmarks/power_distribution/power_distributor.py index 6dccaf6fc..447fac850 100644 --- a/benchmarks/power_distribution/power_distributor.py +++ b/benchmarks/power_distribution/power_distributor.py @@ -12,7 +12,7 @@ from typing import Any from frequenz.channels import Broadcast -from frequenz.client.microgrid import Component, ComponentCategory +from frequenz.client.microgrid import Component, ComponentCategory, ComponentId from frequenz.quantities import Power from frequenz.sdk import microgrid @@ -37,7 +37,7 @@ # send requests, and those no longer go directly to the power distributing actor, but # instead through the power managing actor. So the below function needs to be updated # to use the PowerDistributingActor directly. -async def send_requests(batteries: set[int], request_num: int) -> list[Result]: +async def send_requests(batteries: set[ComponentId], request_num: int) -> list[Result]: """Send requests to the PowerDistributingActor and wait for the response. Args: @@ -98,7 +98,7 @@ def parse_result(result: list[list[Result]]) -> dict[str, float]: async def run_test( # pylint: disable=too-many-locals num_requests: int, - batteries: set[int], + batteries: set[ComponentId], ) -> dict[str, Any]: """Run test. diff --git a/mkdocs.yml b/mkdocs.yml index ad7dba795..d7a711a50 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -118,7 +118,7 @@ plugins: # See https://mkdocstrings.github.io/python/usage/#import for details - https://docs.python.org/3/objects.inv - https://frequenz-floss.github.io/frequenz-channels-python/v1.1/objects.inv - - https://frequenz-floss.github.io/frequenz-client-microgrid-python/v0.5/objects.inv + - https://frequenz-floss.github.io/frequenz-client-microgrid-python/v0.7/objects.inv - https://frequenz-floss.github.io/frequenz-quantities-python/v1/objects.inv - https://lovasoa.github.io/marshmallow_dataclass/html/objects.inv - https://marshmallow.readthedocs.io/en/stable/objects.inv diff --git a/pyproject.toml b/pyproject.toml index b4b90ce66..27036233e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,7 @@ dependencies = [ # Make sure to update the mkdocs.yml file when # changing the version # (plugins.mkdocstrings.handlers.python.import) - "frequenz-client-microgrid >= 0.6.0, < 0.7.0", + "frequenz-client-microgrid >= 0.7.0, < 0.8.0", "frequenz-channels >= 1.6.0, < 2.0.0", "frequenz-quantities[marshmallow] >= 1.0.0, < 2.0.0", "networkx >= 2.8, < 4", diff --git a/src/frequenz/sdk/microgrid/_data_pipeline.py b/src/frequenz/sdk/microgrid/_data_pipeline.py index 9f1a2ef51..1eccb88f8 100644 --- a/src/frequenz/sdk/microgrid/_data_pipeline.py +++ b/src/frequenz/sdk/microgrid/_data_pipeline.py @@ -17,7 +17,7 @@ from datetime import timedelta from frequenz.channels import Broadcast, Sender -from frequenz.client.microgrid import ComponentCategory, InverterType +from frequenz.client.microgrid import ComponentCategory, ComponentId, InverterType from frequenz.sdk.microgrid._power_managing._base_classes import Algorithm, DefaultPower @@ -130,12 +130,14 @@ def __init__( self._producer: Producer | None = None self._grid: Grid | None = None self._ev_charger_pool_reference_stores: dict[ - frozenset[int], EVChargerPoolReferenceStore + frozenset[ComponentId], EVChargerPoolReferenceStore ] = {} self._battery_pool_reference_stores: dict[ - frozenset[int], BatteryPoolReferenceStore + frozenset[ComponentId], BatteryPoolReferenceStore + ] = {} + self._pv_pool_reference_stores: dict[ + frozenset[ComponentId], PVPoolReferenceStore ] = {} - self._pv_pool_reference_stores: dict[frozenset[int], PVPoolReferenceStore] = {} self._frequency_instance: GridFrequency | None = None self._voltage_instance: VoltageStreamer | None = None @@ -216,7 +218,7 @@ def new_ev_charger_pool( self, *, priority: int, - component_ids: abc.Set[int] | None = None, + component_ids: abc.Set[ComponentId] | None = None, name: str | None = None, ) -> EVChargerPool: """Return the corresponding EVChargerPool instance for the given ids. @@ -243,7 +245,7 @@ def new_ev_charger_pool( self._ev_power_wrapper.start() # We use frozenset to make a hashable key from the input set. - ref_store_key: frozenset[int] = frozenset() + ref_store_key: frozenset[ComponentId] = frozenset() if component_ids is not None: ref_store_key = frozenset(component_ids) @@ -292,7 +294,7 @@ def new_pv_pool( self, *, priority: int, - component_ids: abc.Set[int] | None = None, + component_ids: abc.Set[ComponentId] | None = None, name: str | None = None, ) -> PVPool: """Return a new `PVPool` instance for the given ids. @@ -317,7 +319,7 @@ def new_pv_pool( self._pv_power_wrapper.start() # We use frozenset to make a hashable key from the input set. - ref_store_key: frozenset[int] = frozenset() + ref_store_key: frozenset[ComponentId] = frozenset() if component_ids is not None: ref_store_key = frozenset(component_ids) @@ -365,7 +367,7 @@ def new_battery_pool( self, *, priority: int, - component_ids: abc.Set[int] | None = None, + component_ids: abc.Set[ComponentId] | None = None, name: str | None = None, ) -> BatteryPool: """Return a new `BatteryPool` instance for the given ids. @@ -392,7 +394,7 @@ def new_battery_pool( self._battery_power_wrapper.start() # We use frozenset to make a hashable key from the input set. - ref_store_key: frozenset[int] = frozenset() + ref_store_key: frozenset[ComponentId] = frozenset() if component_ids is not None: ref_store_key = frozenset(component_ids) @@ -551,7 +553,7 @@ def producer() -> Producer: def new_ev_charger_pool( *, priority: int, - component_ids: abc.Set[int] | None = None, + component_ids: abc.Set[ComponentId] | None = None, name: str | None = None, ) -> EVChargerPool: """Return a new `EVChargerPool` instance for the given parameters. @@ -590,7 +592,7 @@ def new_ev_charger_pool( def new_battery_pool( *, priority: int, - component_ids: abc.Set[int] | None = None, + component_ids: abc.Set[ComponentId] | None = None, name: str | None = None, ) -> BatteryPool: """Return a new `BatteryPool` instance for the given parameters. @@ -629,7 +631,7 @@ def new_battery_pool( def new_pv_pool( *, priority: int, - component_ids: abc.Set[int] | None = None, + component_ids: abc.Set[ComponentId] | None = None, name: str | None = None, ) -> PVPool: """Return a new `PVPool` instance for the given parameters. diff --git a/src/frequenz/sdk/microgrid/_data_sourcing/_component_metric_request.py b/src/frequenz/sdk/microgrid/_data_sourcing/_component_metric_request.py index fcf8e7f5d..ff9045dfc 100644 --- a/src/frequenz/sdk/microgrid/_data_sourcing/_component_metric_request.py +++ b/src/frequenz/sdk/microgrid/_data_sourcing/_component_metric_request.py @@ -6,7 +6,7 @@ from dataclasses import dataclass from datetime import datetime -from frequenz.client.microgrid import ComponentMetricId +from frequenz.client.microgrid import ComponentId, ComponentMetricId __all__ = ["ComponentMetricRequest", "ComponentMetricId"] @@ -36,7 +36,7 @@ class ComponentMetricRequest: namespace: str """A client-defined identifier influencing the channel name.""" - component_id: int + component_id: ComponentId """The ID of the requested component.""" metric_id: ComponentMetricId diff --git a/src/frequenz/sdk/microgrid/_data_sourcing/microgrid_api_source.py b/src/frequenz/sdk/microgrid/_data_sourcing/microgrid_api_source.py index 9ceabd456..b5bfa6c6a 100644 --- a/src/frequenz/sdk/microgrid/_data_sourcing/microgrid_api_source.py +++ b/src/frequenz/sdk/microgrid/_data_sourcing/microgrid_api_source.py @@ -12,6 +12,7 @@ from frequenz.client.microgrid import ( BatteryData, ComponentCategory, + ComponentId, ComponentMetricId, EVChargerData, InverterData, @@ -148,20 +149,22 @@ def __init__( registry: A channel registry. To be replaced by a singleton instance. """ - self._comp_categories_cache: dict[int, ComponentCategory] = {} + self._comp_categories_cache: dict[ComponentId, ComponentCategory] = {} - self.comp_data_receivers: dict[int, Receiver[Any]] = {} + self.comp_data_receivers: dict[ComponentId, Receiver[Any]] = {} """The dictionary of component IDs to data receivers.""" - self.comp_data_tasks: dict[int, asyncio.Task[None]] = {} + self.comp_data_tasks: dict[ComponentId, asyncio.Task[None]] = {} """The dictionary of component IDs to asyncio tasks.""" self._registry = registry self._req_streaming_metrics: dict[ - int, dict[ComponentMetricId, list[ComponentMetricRequest]] + ComponentId, dict[ComponentMetricId, list[ComponentMetricRequest]] ] = {} - async def _get_component_category(self, comp_id: int) -> ComponentCategory | None: + async def _get_component_category( + self, comp_id: ComponentId + ) -> ComponentCategory | None: """Get the component category of the given component. Args: @@ -185,7 +188,7 @@ async def _get_component_category(self, comp_id: int) -> ComponentCategory | Non async def _check_battery_request( self, - comp_id: int, + comp_id: ComponentId, requests: dict[ComponentMetricId, list[ComponentMetricRequest]], ) -> None: """Check if the requests are valid Battery metrics. @@ -210,7 +213,7 @@ async def _check_battery_request( async def _check_ev_charger_request( self, - comp_id: int, + comp_id: ComponentId, requests: dict[ComponentMetricId, list[ComponentMetricRequest]], ) -> None: """Check if the requests are valid EV Charger metrics. @@ -235,7 +238,7 @@ async def _check_ev_charger_request( async def _check_inverter_request( self, - comp_id: int, + comp_id: ComponentId, requests: dict[ComponentMetricId, list[ComponentMetricRequest]], ) -> None: """Check if the requests are valid Inverter metrics. @@ -260,7 +263,7 @@ async def _check_inverter_request( async def _check_meter_request( self, - comp_id: int, + comp_id: ComponentId, requests: dict[ComponentMetricId, list[ComponentMetricRequest]], ) -> None: """Check if the requests are valid Meter metrics. @@ -285,7 +288,7 @@ async def _check_meter_request( async def _check_requested_component_and_metrics( self, - comp_id: int, + comp_id: ComponentId, category: ComponentCategory, requests: dict[ComponentMetricId, list[ComponentMetricRequest]], ) -> None: @@ -376,7 +379,7 @@ def _get_metric_senders( async def _handle_data_stream( self, - comp_id: int, + comp_id: ComponentId, category: ComponentCategory, ) -> None: """Stream component data and send the requested metrics out. @@ -448,7 +451,7 @@ async def clean_tasks( async def _update_streams( self, - comp_id: int, + comp_id: ComponentId, category: ComponentCategory, ) -> None: """Update the requested metric streams for the given component. diff --git a/src/frequenz/sdk/microgrid/_power_distributing/_component_managers/_battery_manager.py b/src/frequenz/sdk/microgrid/_power_distributing/_component_managers/_battery_manager.py index 7846bc709..444cec360 100644 --- a/src/frequenz/sdk/microgrid/_power_distributing/_component_managers/_battery_manager.py +++ b/src/frequenz/sdk/microgrid/_power_distributing/_component_managers/_battery_manager.py @@ -15,6 +15,7 @@ ApiClientError, BatteryData, ComponentCategory, + ComponentId, InverterData, OperationOutOfRange, ) @@ -38,8 +39,9 @@ def _get_all_from_map( - source: dict[int, frozenset[int]], keys: collections.abc.Set[int] -) -> set[int]: + source: dict[ComponentId, frozenset[ComponentId]], + keys: collections.abc.Set[ComponentId], +) -> set[ComponentId]: """Get all values for the given keys from the given map. Args: @@ -53,12 +55,12 @@ def _get_all_from_map( def _get_battery_inverter_mappings( - battery_ids: collections.abc.Set[int], + battery_ids: collections.abc.Set[ComponentId], *, # force keyword arguments inv_bats: bool = True, bat_bats: bool = True, inv_invs: bool = True, -) -> dict[str, dict[int, frozenset[int]]]: +) -> dict[str, dict[ComponentId, frozenset[ComponentId]]]: """Create maps between battery and adjacent inverters. Args: @@ -74,14 +76,14 @@ def _get_battery_inverter_mappings( * "bat_bats": battery to batteries map * "inv_invs": inverter to inverters map """ - bat_invs_map: dict[int, set[int]] = {} - inv_bats_map: dict[int, set[int]] | None = {} if inv_bats else None - bat_bats_map: dict[int, set[int]] | None = {} if bat_bats else None - inv_invs_map: dict[int, set[int]] | None = {} if inv_invs else None + bat_invs_map: dict[ComponentId, set[ComponentId]] = {} + inv_bats_map: dict[ComponentId, set[ComponentId]] | None = {} if inv_bats else None + bat_bats_map: dict[ComponentId, set[ComponentId]] | None = {} if bat_bats else None + inv_invs_map: dict[ComponentId, set[ComponentId]] | None = {} if inv_invs else None component_graph = connection_manager.get().component_graph for battery_id in battery_ids: - inverters: set[int] = set( + inverters: set[ComponentId] = set( component.component_id for component in component_graph.predecessors(battery_id) if component.category == ComponentCategory.INVERTER @@ -107,10 +109,10 @@ def _get_battery_inverter_mappings( if inv_invs_map is not None: inv_invs_map.setdefault(inverter, set()).update(bat_invs_map) - mapping: dict[str, dict[int, frozenset[int]]] = {} + mapping: dict[str, dict[ComponentId, frozenset[ComponentId]]] = {} # Convert sets to frozensets to make them hashable. - def _add(key: str, value: dict[int, set[int]] | None) -> None: + def _add(key: str, value: dict[ComponentId, set[ComponentId]] | None) -> None: if value is not None: mapping[key] = {k: frozenset(v) for k, v in value.items()} @@ -157,8 +159,8 @@ def __init__( self._bat_bats_map = maps["bat_bats"] self._inv_invs_map = maps["inv_invs"] - self._battery_caches: dict[int, LatestValueCache[BatteryData]] = {} - self._inverter_caches: dict[int, LatestValueCache[InverterData]] = {} + self._battery_caches: dict[ComponentId, LatestValueCache[BatteryData]] = {} + self._inverter_caches: dict[ComponentId, LatestValueCache[InverterData]] = {} self._component_pool_status_tracker = ComponentPoolStatusTracker( component_ids=set(self._battery_ids), @@ -182,7 +184,7 @@ def __init__( """The distribution algorithm used to distribute power between batteries.""" @override - def component_ids(self) -> collections.abc.Set[int]: + def component_ids(self) -> collections.abc.Set[ComponentId]: """Return the set of component ids.""" return self._battery_ids @@ -264,7 +266,7 @@ async def _distribute_power( Result from the microgrid API. """ distributed_power_value = request.power - distribution.remaining_power - battery_distribution: dict[int, Power] = {} + battery_distribution: dict[ComponentId, Power] = {} for inverter_id, dist in distribution.distribution.items(): for battery_id in self._inv_bats_map[inverter_id]: battery_distribution[battery_id] = ( @@ -449,7 +451,7 @@ def _check_request( return None def _get_battery_inverter_data( - self, battery_ids: frozenset[int], inverter_ids: frozenset[int] + self, battery_ids: frozenset[ComponentId], inverter_ids: frozenset[ComponentId] ) -> InvBatPair | None: """Get battery and inverter data if they are correct. @@ -525,7 +527,7 @@ def nan_metric_in_list(data: list[DataType], metrics: list[str]) -> bool: return InvBatPair(AggregatedBatteryData(battery_data), inverter_data) def _get_components_data( - self, batteries: collections.abc.Set[int] + self, batteries: collections.abc.Set[ComponentId] ) -> list[InvBatPair] | str: """Get data for the given batteries and adjacent inverters. @@ -536,7 +538,7 @@ def _get_components_data( Pairs of battery and adjacent inverter data or an error message if there was an error while getting the data. """ - inverter_ids: collections.abc.Set[int] + inverter_ids: collections.abc.Set[ComponentId] pairs_data: list[InvBatPair] = [] working_batteries = self._component_pool_status_tracker.get_working_components( @@ -567,7 +569,7 @@ def _get_components_data( ) # set of set of batteries one for each working_battery - battery_sets: frozenset[frozenset[int]] = frozenset( + battery_sets: frozenset[frozenset[ComponentId]] = frozenset( self._bat_bats_map[working_battery] for working_battery in working_batteries ) @@ -586,7 +588,7 @@ def _get_components_data( pairs_data.append(data) return pairs_data - def _str_ids(self, ids: collections.abc.Set[int]) -> str: + def _str_ids(self, ids: collections.abc.Set[ComponentId]) -> str: return ", ".join(str(cid) for cid in sorted(ids)) def _get_power_distribution( @@ -606,7 +608,7 @@ def _get_power_distribution( ) unavailable_bat_ids = request.component_ids - available_bat_ids - unavailable_inv_ids: set[int] = set() + unavailable_inv_ids: set[ComponentId] = set() for inverter_ids in [ self._bat_invs_map[battery_id_set] for battery_id_set in unavailable_bat_ids @@ -623,7 +625,7 @@ async def _set_distributed_power( self, distribution: DistributionResult, timeout: timedelta, - ) -> tuple[Power, set[int]]: + ) -> tuple[Power, set[ComponentId]]: """Send distributed power to the inverters. Args: @@ -655,10 +657,10 @@ async def _set_distributed_power( def _parse_result( self, - tasks: dict[int, asyncio.Task[None]], - distribution: dict[int, Power], + tasks: dict[ComponentId, asyncio.Task[None]], + distribution: dict[ComponentId, Power], request_timeout: timedelta, - ) -> tuple[Power, set[int]]: + ) -> tuple[Power, set[ComponentId]]: """Parse the results of `set_power` requests. Check if any task has failed and determine the reason for failure. @@ -676,7 +678,7 @@ def _parse_result( the set of batteries that failed. """ failed_power: Power = Power.zero() - failed_batteries: set[int] = set() + failed_batteries: set[ComponentId] = set() for inverter_id, aws in tasks.items(): battery_ids = self._inv_bats_map[inverter_id] diff --git a/src/frequenz/sdk/microgrid/_power_distributing/_component_managers/_component_manager.py b/src/frequenz/sdk/microgrid/_power_distributing/_component_managers/_component_manager.py index f9eba111e..c561970f3 100644 --- a/src/frequenz/sdk/microgrid/_power_distributing/_component_managers/_component_manager.py +++ b/src/frequenz/sdk/microgrid/_power_distributing/_component_managers/_component_manager.py @@ -7,6 +7,7 @@ import collections.abc from frequenz.channels import Sender +from frequenz.client.microgrid import ComponentId from .._component_status import ComponentPoolStatus from ..request import Request @@ -31,7 +32,7 @@ def __init__( """ @abc.abstractmethod - def component_ids(self) -> collections.abc.Set[int]: + def component_ids(self) -> collections.abc.Set[ComponentId]: """Return the set of component ids.""" @abc.abstractmethod diff --git a/src/frequenz/sdk/microgrid/_power_distributing/_component_managers/_ev_charger_manager/_config.py b/src/frequenz/sdk/microgrid/_power_distributing/_component_managers/_ev_charger_manager/_config.py index f57a55c2d..9c18aa91c 100644 --- a/src/frequenz/sdk/microgrid/_power_distributing/_component_managers/_ev_charger_manager/_config.py +++ b/src/frequenz/sdk/microgrid/_power_distributing/_component_managers/_ev_charger_manager/_config.py @@ -7,6 +7,7 @@ from dataclasses import dataclass, field from datetime import timedelta +from frequenz.client.microgrid import ComponentId from frequenz.quantities import Current @@ -14,7 +15,7 @@ class EVDistributionConfig: """Configuration for the power distributor's EV charger manager.""" - component_ids: abc.Set[int] + component_ids: abc.Set[ComponentId] """The component ids of the EV chargers.""" min_current: Current = field(default_factory=lambda: Current.from_amperes(6.0)) diff --git a/src/frequenz/sdk/microgrid/_power_distributing/_component_managers/_ev_charger_manager/_ev_charger_manager.py b/src/frequenz/sdk/microgrid/_power_distributing/_component_managers/_ev_charger_manager/_ev_charger_manager.py index 05e276ebf..83c6981b3 100644 --- a/src/frequenz/sdk/microgrid/_power_distributing/_component_managers/_ev_charger_manager/_ev_charger_manager.py +++ b/src/frequenz/sdk/microgrid/_power_distributing/_component_managers/_ev_charger_manager/_ev_charger_manager.py @@ -19,6 +19,7 @@ from frequenz.client.microgrid import ( ApiClientError, ComponentCategory, + ComponentId, EVChargerData, MicrogridApiClient, ) @@ -82,7 +83,7 @@ def __init__( self._latest_request: Request = Request(Power.zero(), set()) @override - def component_ids(self) -> collections.abc.Set[int]: + def component_ids(self) -> collections.abc.Set[ComponentId]: """Return the set of ev charger ids.""" return self._ev_charger_ids @@ -109,7 +110,7 @@ async def stop(self) -> None: await self._voltage_cache.stop() await self._component_pool_status_tracker.stop() - def _get_ev_charger_ids(self) -> collections.abc.Set[int]: + def _get_ev_charger_ids(self) -> collections.abc.Set[ComponentId]: """Return the IDs of all EV chargers present in the component graph.""" return { evc.component_id @@ -118,7 +119,7 @@ def _get_ev_charger_ids(self) -> collections.abc.Set[int]: ) } - def _allocate_new_ev(self, component_id: int) -> dict[int, Power]: + def _allocate_new_ev(self, component_id: ComponentId) -> dict[ComponentId, Power]: """Allocate power to a newly connected EV charger. Args: @@ -147,7 +148,7 @@ def _allocate_new_ev(self, component_id: int) -> dict[int, Power]: return {} - def _act_on_new_data(self, ev_data: EVChargerData) -> dict[int, Power]: + def _act_on_new_data(self, ev_data: EVChargerData) -> dict[ComponentId, Power]: """Act on new data from an EV charger. Args: @@ -226,7 +227,7 @@ async def _run(self) -> None: # pylint: disable=too-many-locals *[await api.ev_charger_data(evc_id) for evc_id in self._ev_charger_ids] ) target_power_rx = self._target_power_channel.new_receiver() - latest_target_powers: dict[int, Power] = {} + latest_target_powers: dict[ComponentId, Power] = {} async for selected in select(ev_charger_data_rx, target_power_rx): target_power_changes = {} now = datetime.now(tz=timezone.utc) @@ -293,7 +294,7 @@ async def _run(self) -> None: # pylint: disable=too-many-locals async def _set_api_power( self, api: MicrogridApiClient, - target_power_changes: dict[int, Power], + target_power_changes: dict[ComponentId, Power], api_request_timeout: timedelta, ) -> Result: """Send the EV charger power changes to the microgrid API. @@ -308,7 +309,7 @@ async def _set_api_power( Power distribution result, corresponding to the result of the API request. """ - tasks: dict[int, asyncio.Task[None]] = {} + tasks: dict[ComponentId, asyncio.Task[None]] = {} for component_id, power in target_power_changes.items(): tasks[component_id] = asyncio.create_task( api.set_power(component_id, power.as_watts()) @@ -322,8 +323,8 @@ async def _set_api_power( task.cancel() await asyncio.gather(*pending, return_exceptions=True) - failed_components: set[int] = set() - succeeded_components: set[int] = set() + failed_components: set[ComponentId] = set() + succeeded_components: set[ComponentId] = set() failed_power = Power.zero() for component_id, task in tasks.items(): try: @@ -365,7 +366,9 @@ async def _set_api_power( request=self._latest_request, ) - def _deallocate_unused_power(self, to_deallocate: Power) -> dict[int, Power]: + def _deallocate_unused_power( + self, to_deallocate: Power + ) -> dict[ComponentId, Power]: """Reduce the power allocated to the EV chargers to meet the target power. This prioritizes reducing power to EV chargers that aren't consuming the @@ -409,7 +412,7 @@ def _deallocate_unused_power(self, to_deallocate: Power) -> dict[int, Power]: def _throttle_ev_chargers( # pylint: disable=too-many-locals self, throttle_by: Power, - ) -> dict[int, Power]: + ) -> dict[ComponentId, Power]: """Reduce EV charging power to meet the target power. This targets EV chargers that are currently consuming the most. diff --git a/src/frequenz/sdk/microgrid/_power_distributing/_component_managers/_ev_charger_manager/_states.py b/src/frequenz/sdk/microgrid/_power_distributing/_component_managers/_ev_charger_manager/_states.py index 44b62fe65..1da97d00b 100644 --- a/src/frequenz/sdk/microgrid/_power_distributing/_component_managers/_ev_charger_manager/_states.py +++ b/src/frequenz/sdk/microgrid/_power_distributing/_component_managers/_ev_charger_manager/_states.py @@ -8,7 +8,7 @@ from datetime import datetime from typing import Iterable -from frequenz.client.microgrid import EVChargerData +from frequenz.client.microgrid import ComponentId, EVChargerData from frequenz.quantities import Power @@ -16,7 +16,7 @@ class EvcState: """A class for tracking state of an ev charger.""" - component_id: int + component_id: ComponentId """The component id of the ev charger.""" last_data: EVChargerData @@ -71,7 +71,7 @@ def update_state( class EvcStates: """Tracks states of all ev chargers.""" - _states: dict[int, EvcState] + _states: dict[ComponentId, EvcState] def __init__(self) -> None: """Initialize this instance.""" @@ -91,7 +91,7 @@ def get_total_allocated_power(self) -> Power: total_allocated += evc.last_allocation return total_allocated - def get(self, component_id: int) -> EvcState: + def get(self, component_id: ComponentId) -> EvcState: """Return a reference to the EvcState object with the given component_id. Args: @@ -118,7 +118,7 @@ def values(self) -> Iterable[EvcState]: """ return self._states.values() - def __contains__(self, component_id: int) -> bool: + def __contains__(self, component_id: ComponentId) -> bool: """Check if the given component_id has an associated EvcState object. Args: diff --git a/src/frequenz/sdk/microgrid/_power_distributing/_component_managers/_pv_inverter_manager/_pv_inverter_manager.py b/src/frequenz/sdk/microgrid/_power_distributing/_component_managers/_pv_inverter_manager/_pv_inverter_manager.py index 53ddc8495..f9afc1f39 100644 --- a/src/frequenz/sdk/microgrid/_power_distributing/_component_managers/_pv_inverter_manager/_pv_inverter_manager.py +++ b/src/frequenz/sdk/microgrid/_power_distributing/_component_managers/_pv_inverter_manager/_pv_inverter_manager.py @@ -12,6 +12,7 @@ from frequenz.client.microgrid import ( ApiClientError, ComponentCategory, + ComponentId, InverterData, InverterType, ) @@ -63,14 +64,16 @@ def __init__( if self._pv_inverter_ids else None ) - self._component_data_caches: dict[int, LatestValueCache[InverterData]] = {} + self._component_data_caches: dict[ + ComponentId, LatestValueCache[InverterData] + ] = {} self._target_power = Power.zero() self._target_power_channel = Broadcast[Request](name="target_power") self._target_power_tx = self._target_power_channel.new_sender() self._task: asyncio.Task[None] | None = None @override - def component_ids(self) -> collections.abc.Set[int]: + def component_ids(self) -> collections.abc.Set[ComponentId]: """Return the set of PV inverter ids.""" return self._pv_inverter_ids @@ -106,7 +109,7 @@ async def distribute_power(self, request: Request) -> None: component_ids are provided in the request. """ remaining_power = request.power - allocations: dict[int, Power] = {} + allocations: dict[ComponentId, Power] = {} if not self._component_pool_status_tracker: if not request.component_ids: await self._results_sender.send( @@ -122,7 +125,7 @@ async def distribute_power(self, request: Request) -> None: "Cannot distribute power to PV inverters without any inverters" ) - working_components: list[int] = [] + working_components: list[ComponentId] = [] for inv_id in self._component_pool_status_tracker.get_working_components( request.component_ids ): @@ -182,10 +185,13 @@ async def distribute_power(self, request: Request) -> None: await self._set_api_power(request, allocations, remaining_power) async def _set_api_power( # pylint: disable=too-many-locals - self, request: Request, allocations: dict[int, Power], remaining_power: Power + self, + request: Request, + allocations: dict[ComponentId, Power], + remaining_power: Power, ) -> None: api_client = connection_manager.get().api_client - tasks: dict[int, asyncio.Task[None]] = {} + tasks: dict[ComponentId, asyncio.Task[None]] = {} for component_id, power in allocations.items(): tasks[component_id] = asyncio.create_task( api_client.set_power(component_id, power.as_watts()) @@ -201,8 +207,8 @@ async def _set_api_power( # pylint: disable=too-many-locals task.cancel() await asyncio.gather(*pending, return_exceptions=True) - failed_components: set[int] = set() - succeeded_components: set[int] = set() + failed_components: set[ComponentId] = set() + succeeded_components: set[ComponentId] = set() failed_power = Power.zero() for component_id, task in tasks.items(): try: @@ -250,7 +256,7 @@ async def _set_api_power( # pylint: disable=too-many-locals ) ) - def _get_pv_inverter_ids(self) -> collections.abc.Set[int]: + def _get_pv_inverter_ids(self) -> collections.abc.Set[ComponentId]: """Return the IDs of all PV inverters present in the component graph.""" return { inv.component_id diff --git a/src/frequenz/sdk/microgrid/_power_distributing/_component_pool_status_tracker.py b/src/frequenz/sdk/microgrid/_power_distributing/_component_pool_status_tracker.py index c0aed7e2d..f52301a08 100644 --- a/src/frequenz/sdk/microgrid/_power_distributing/_component_pool_status_tracker.py +++ b/src/frequenz/sdk/microgrid/_power_distributing/_component_pool_status_tracker.py @@ -11,6 +11,7 @@ from datetime import timedelta from frequenz.channels import Broadcast, Merger, Receiver, Sender, merge +from frequenz.client.microgrid import ComponentId from ..._internal._asyncio import cancel_and_await from ._component_status import ( @@ -34,7 +35,7 @@ class ComponentPoolStatusTracker: def __init__( # pylint: disable=too-many-arguments self, *, - component_ids: abc.Set[int], + component_ids: abc.Set[ComponentId], component_status_sender: Sender[ComponentPoolStatus], max_data_age: timedelta, max_blocking_duration: timedelta, @@ -136,7 +137,9 @@ async def _update_status(self) -> None: await self._component_status_sender.send(self._current_status) async def update_status( - self, succeeded_components: set[int], failed_components: set[int] + self, + succeeded_components: set[ComponentId], + failed_components: set[ComponentId], ) -> None: """Notify which components succeeded or failed in the request. @@ -153,7 +156,9 @@ async def update_status( SetPowerResult(succeeded=succeeded_components, failed=failed_components) ) - def get_working_components(self, components: abc.Set[int]) -> abc.Set[int]: + def get_working_components( + self, components: abc.Set[ComponentId] + ) -> abc.Set[ComponentId]: """From the given set of components, return only working ones. Args: diff --git a/src/frequenz/sdk/microgrid/_power_distributing/_component_status/_battery_status_tracker.py b/src/frequenz/sdk/microgrid/_power_distributing/_component_status/_battery_status_tracker.py index dd181827c..075f96784 100644 --- a/src/frequenz/sdk/microgrid/_power_distributing/_component_status/_battery_status_tracker.py +++ b/src/frequenz/sdk/microgrid/_power_distributing/_component_status/_battery_status_tracker.py @@ -29,6 +29,7 @@ BatteryRelayState, ComponentCategory, ComponentData, + ComponentId, ErrorLevel, InverterComponentState, InverterData, @@ -50,7 +51,7 @@ @dataclass class _ComponentStreamStatus: - component_id: int + component_id: ComponentId """Component id.""" data_recv_timer: Timer @@ -100,7 +101,7 @@ class BatteryStatusTracker(ComponentStatusTracker, BackgroundService): def __init__( # pylint: disable=too-many-arguments self, *, - component_id: int, + component_id: ComponentId, max_data_age: timedelta, max_blocking_duration: timedelta, status_sender: Sender[ComponentStatus], @@ -162,7 +163,7 @@ def start(self) -> None: ) @property - def battery_id(self) -> int: + def battery_id(self) -> ComponentId: """Get battery id. Returns: @@ -469,7 +470,7 @@ def _is_message_reliable(self, message: ComponentData) -> bool: return not is_outdated - def _find_adjacent_inverter_id(self, battery_id: int) -> int | None: + def _find_adjacent_inverter_id(self, battery_id: ComponentId) -> ComponentId | None: """Find inverter adjacent to this battery. Args: diff --git a/src/frequenz/sdk/microgrid/_power_distributing/_component_status/_component_status.py b/src/frequenz/sdk/microgrid/_power_distributing/_component_status/_component_status.py index ce34befc5..1ce761e14 100644 --- a/src/frequenz/sdk/microgrid/_power_distributing/_component_status/_component_status.py +++ b/src/frequenz/sdk/microgrid/_power_distributing/_component_status/_component_status.py @@ -11,6 +11,7 @@ from datetime import timedelta from frequenz.channels import Receiver, Sender +from frequenz.client.microgrid import ComponentId from ....actor._background_service import BackgroundService @@ -19,13 +20,15 @@ class ComponentPoolStatus: """Status of all components of a certain category in the microgrid.""" - working: set[int] + working: set[ComponentId] """Set of working component ids.""" - uncertain: set[int] + uncertain: set[ComponentId] """Set of components to be used only when there are none known to be working.""" - def get_working_components(self, components: abc.Set[int]) -> set[int]: + def get_working_components( + self, components: abc.Set[ComponentId] + ) -> set[ComponentId]: """From the given set of components return the working ones. Args: @@ -61,7 +64,7 @@ class ComponentStatusEnum(enum.Enum): class ComponentStatus: """Status of a single component.""" - component_id: int + component_id: ComponentId """Component ID.""" value: ComponentStatusEnum @@ -72,10 +75,10 @@ class ComponentStatus: class SetPowerResult: """Lists of components for which the last set power command succeeded or failed.""" - succeeded: abc.Set[int] + succeeded: abc.Set[ComponentId] """Component IDs for which the last set power command succeeded.""" - failed: abc.Set[int] + failed: abc.Set[ComponentId] """Component IDs for which the last set power command failed.""" @@ -86,7 +89,7 @@ class ComponentStatusTracker(BackgroundService, ABC): def __init__( # pylint: disable=too-many-arguments,super-init-not-called self, *, - component_id: int, + component_id: ComponentId, max_data_age: timedelta, max_blocking_duration: timedelta, status_sender: Sender[ComponentStatus], diff --git a/src/frequenz/sdk/microgrid/_power_distributing/_component_status/_ev_charger_status_tracker.py b/src/frequenz/sdk/microgrid/_power_distributing/_component_status/_ev_charger_status_tracker.py index 48fe2478b..389f5c632 100644 --- a/src/frequenz/sdk/microgrid/_power_distributing/_component_status/_ev_charger_status_tracker.py +++ b/src/frequenz/sdk/microgrid/_power_distributing/_component_status/_ev_charger_status_tracker.py @@ -11,6 +11,7 @@ from frequenz.channels import Receiver, Sender, select, selected_from from frequenz.channels.timer import SkipMissedAndDrift, Timer from frequenz.client.microgrid import ( + ComponentId, EVChargerCableState, EVChargerComponentState, EVChargerData, @@ -47,7 +48,7 @@ class EVChargerStatusTracker(ComponentStatusTracker, BackgroundService): def __init__( # pylint: disable=too-many-arguments self, *, - component_id: int, + component_id: ComponentId, max_data_age: timedelta, max_blocking_duration: timedelta, status_sender: Sender[ComponentStatus], diff --git a/src/frequenz/sdk/microgrid/_power_distributing/_component_status/_pv_inverter_status_tracker.py b/src/frequenz/sdk/microgrid/_power_distributing/_component_status/_pv_inverter_status_tracker.py index 39e38ba3c..120e0f3b4 100644 --- a/src/frequenz/sdk/microgrid/_power_distributing/_component_status/_pv_inverter_status_tracker.py +++ b/src/frequenz/sdk/microgrid/_power_distributing/_component_status/_pv_inverter_status_tracker.py @@ -9,7 +9,7 @@ from frequenz.channels import Receiver, Sender, select, selected_from from frequenz.channels.timer import SkipMissedAndDrift, Timer -from frequenz.client.microgrid import InverterComponentState, InverterData +from frequenz.client.microgrid import ComponentId, InverterComponentState, InverterData from typing_extensions import override from ...._internal._asyncio import run_forever @@ -44,7 +44,7 @@ class PVInverterStatusTracker(ComponentStatusTracker, BackgroundService): def __init__( # pylint: disable=too-many-arguments self, *, - component_id: int, + component_id: ComponentId, max_data_age: timedelta, max_blocking_duration: timedelta, status_sender: Sender[ComponentStatus], diff --git a/src/frequenz/sdk/microgrid/_power_distributing/_distribution_algorithm/_battery_distribution_algorithm.py b/src/frequenz/sdk/microgrid/_power_distributing/_distribution_algorithm/_battery_distribution_algorithm.py index ddc8df589..f6b373e3e 100644 --- a/src/frequenz/sdk/microgrid/_power_distributing/_distribution_algorithm/_battery_distribution_algorithm.py +++ b/src/frequenz/sdk/microgrid/_power_distributing/_distribution_algorithm/_battery_distribution_algorithm.py @@ -8,7 +8,7 @@ from dataclasses import dataclass from typing import NamedTuple, Sequence -from frequenz.client.microgrid import BatteryData, InverterData +from frequenz.client.microgrid import BatteryData, ComponentId, InverterData from frequenz.quantities import Power from ...._internal._math import is_close_to_zero @@ -21,7 +21,7 @@ class AggregatedBatteryData: """Aggregated battery data.""" - component_id: int + component_id: ComponentId """The component ID of the first battery. This is only used to identify the pair of battery and inverter. @@ -156,10 +156,10 @@ class InvBatPair(NamedTuple): class AvailabilityRatio: """Availability ratio for a battery-inverter pair.""" - battery_id: int + battery_id: ComponentId """The battery ID.""" - inverter_ids: list[int] + inverter_ids: list[ComponentId] """The inverter IDs.""" ratio: float @@ -180,7 +180,7 @@ class _Power: """The power to be set for the inverter.""" -_InverterSet = frozenset[int] +_InverterSet = frozenset[ComponentId] """A set of inverter IDs.""" @@ -199,7 +199,7 @@ class _Allocation: class DistributionResult: """Distribution result.""" - distribution: dict[int, Power] + distribution: dict[ComponentId, Power] """The power to be set for each inverter. The key is inverter ID, and the value is the power that should be set for @@ -398,8 +398,8 @@ def _total_capacity(self, components: list[InvBatPair]) -> float: def _compute_battery_availability_ratio( self, components: list[InvBatPair], - available_soc: dict[int, float], - excl_bounds: dict[int, Power], + available_soc: dict[ComponentId, float], + excl_bounds: dict[ComponentId, Power], ) -> tuple[list[AvailabilityRatio], float]: r"""Compute battery ratio and the total sum of all of them. @@ -465,9 +465,9 @@ def _distribute_power( # pylint: disable=too-many-arguments *, components: list[InvBatPair], power: Power, - available_soc: dict[int, float], - incl_bounds: dict[int, Power], - excl_bounds: dict[int, Power], + available_soc: dict[ComponentId, float], + incl_bounds: dict[ComponentId, Power], + excl_bounds: dict[ComponentId, Power], ) -> DistributionResult: # pylint: disable=too-many-locals,too-many-branches,too-many-statements """Distribute power between given components. @@ -604,9 +604,9 @@ def _distribute_power( # pylint: disable=too-many-arguments def _distribute_multi_inverter_pairs( self, distribution: dict[_InverterSet, _Power], - excl_bounds: dict[int, Power], - incl_bounds: dict[int, Power], - ) -> dict[int, Power]: + excl_bounds: dict[ComponentId, Power], + incl_bounds: dict[ComponentId, Power], + ) -> dict[ComponentId, Power]: """Distribute power between inverters in a set for a single pair. Args: @@ -617,7 +617,7 @@ def _distribute_multi_inverter_pairs( Returns: Return the power for each inverter in given distribution. """ - new_distribution: dict[int, Power] = {} + new_distribution: dict[ComponentId, Power] = {} for inverter_ids, power in distribution.items(): if len(inverter_ids) == 1: @@ -677,7 +677,7 @@ def _greedy_distribute_remaining_power( return distribution, remaining_power def distribute_power_equally( - self, power: Power, inverters: set[int] + self, power: Power, inverters: set[ComponentId] ) -> DistributionResult: """Distribute the power equally between the inverters in the set. @@ -743,7 +743,7 @@ def _distribute_consume_power( # If SoC exceeded bound then remaining SoC should be 0. # Otherwise algorithm would try to supply power from that battery # in order to keep equal SoC level. - available_soc: dict[int, float] = {} + available_soc: dict[ComponentId, float] = {} for battery, _ in components: available_soc[battery.component_id] = max( 0.0, battery.soc_upper_bound - battery.soc @@ -778,7 +778,7 @@ def _distribute_supply_power( Returns: Distribution result. """ - available_soc: dict[int, float] = {} + available_soc: dict[ComponentId, float] = {} for battery, _ in components: available_soc[battery.component_id] = max( 0.0, battery.soc - battery.soc_lower_bound @@ -804,7 +804,7 @@ def _distribute_supply_power( def _inclusion_exclusion_bounds( self, components: list[InvBatPair], supply: bool = False - ) -> tuple[dict[int, Power], dict[int, Power]]: + ) -> tuple[dict[ComponentId, Power], dict[ComponentId, Power]]: """Calculate inclusion and exclusion bounds for given components. Inverter exclusion bounds are _not_ adjusted to battery inclusion @@ -819,8 +819,8 @@ def _inclusion_exclusion_bounds( Returns: inclusion and exclusion bounds. """ - incl_bounds: dict[int, Power] = {} - excl_bounds: dict[int, Power] = {} + incl_bounds: dict[ComponentId, Power] = {} + excl_bounds: dict[ComponentId, Power] = {} for battery, inverters in components: if supply: excl_bounds[battery.component_id] = ( diff --git a/src/frequenz/sdk/microgrid/_power_distributing/power_distributing.py b/src/frequenz/sdk/microgrid/_power_distributing/power_distributing.py index 67b3ef7b4..40bceead4 100644 --- a/src/frequenz/sdk/microgrid/_power_distributing/power_distributing.py +++ b/src/frequenz/sdk/microgrid/_power_distributing/power_distributing.py @@ -15,7 +15,12 @@ from datetime import timedelta from frequenz.channels import Receiver, Sender -from frequenz.client.microgrid import ComponentCategory, ComponentType, InverterType +from frequenz.client.microgrid import ( + ComponentCategory, + ComponentId, + ComponentType, + InverterType, +) from typing_extensions import override from ...actor._actor import Actor @@ -101,10 +106,10 @@ def __init__( # pylint: disable=too-many-arguments self._result_sender = results_sender self._api_power_request_timeout = api_power_request_timeout - self._processing_tasks: dict[frozenset[int], asyncio.Task[None]] = {} + self._processing_tasks: dict[frozenset[ComponentId], asyncio.Task[None]] = {} """Track the power request tasks currently being processed.""" - self._pending_requests: dict[frozenset[int], Request] = {} + self._pending_requests: dict[frozenset[ComponentId], Request] = {} """Track the power requests that are waiting to be processed. Only one pending power request is kept for each set of components, the @@ -173,7 +178,10 @@ async def stop(self, msg: str | None = None) -> None: await super().stop(msg) def _handle_task_completion( - self, req_id: frozenset[int], request: Request, task: asyncio.Task[None] + self, + req_id: frozenset[ComponentId], + request: Request, + task: asyncio.Task[None], ) -> None: """Handle the completion of a power request task. @@ -194,7 +202,9 @@ def _handle_task_completion( else: _logger.error("Request id not found in processing tasks: %s", req_id) - def _process_request(self, req_id: frozenset[int], request: Request) -> None: + def _process_request( + self, req_id: frozenset[ComponentId], request: Request + ) -> None: """Process a power request. Args: diff --git a/src/frequenz/sdk/microgrid/_power_distributing/request.py b/src/frequenz/sdk/microgrid/_power_distributing/request.py index e7f3081c7..de73d27fa 100644 --- a/src/frequenz/sdk/microgrid/_power_distributing/request.py +++ b/src/frequenz/sdk/microgrid/_power_distributing/request.py @@ -6,6 +6,7 @@ import dataclasses from collections import abc +from frequenz.client.microgrid import ComponentId from frequenz.quantities import Power @@ -16,7 +17,7 @@ class Request: power: Power """The requested power.""" - component_ids: abc.Set[int] + component_ids: abc.Set[ComponentId] """The component ids of the components to be used for this request.""" adjust_power: bool = True diff --git a/src/frequenz/sdk/microgrid/_power_distributing/result.py b/src/frequenz/sdk/microgrid/_power_distributing/result.py index 8effcff27..f40bc0476 100644 --- a/src/frequenz/sdk/microgrid/_power_distributing/result.py +++ b/src/frequenz/sdk/microgrid/_power_distributing/result.py @@ -7,6 +7,7 @@ import dataclasses from collections import abc +from frequenz.client.microgrid import ComponentId from frequenz.quantities import Power from .request import Request @@ -27,7 +28,7 @@ class _BaseSuccessMixin: succeeded_power: Power """The part of the requested power that was successfully set.""" - succeeded_components: abc.Set[int] + succeeded_components: abc.Set[ComponentId] """The subset of components for which power was set successfully.""" excess_power: Power @@ -55,7 +56,7 @@ class PartialFailure(_BaseSuccessMixin, _BaseResultMixin): failed_power: Power """The part of the requested power that failed to be set.""" - failed_components: abc.Set[int] + failed_components: abc.Set[ComponentId] """The subset of batteries for which the request failed.""" @@ -118,6 +119,7 @@ class OutOfBounds(_BaseResultMixin): from frequenz.sdk.actor.power_distributing.request import Request from frequenz.sdk.actor.power_distributing.result import PowerBounds from frequenz.quantities import Power + from frequenz.client.microgrid import ComponentId def handle_power_request_result(result: Result) -> None: match result: @@ -135,22 +137,22 @@ def handle_power_request_result(result: Result) -> None: request = Request( namespace="TestChannel", power=Power.from_watts(123.4), - component_ids={8, 18}, + component_ids={ComponentId(8), ComponentId(18)}, ) results: list[Result] = [ Success( request, succeeded_power=Power.from_watts(123.4), - succeeded_components={8, 18}, + succeeded_components={ComponentId(8), ComponentId(18)}, excess_power=Power.zero(), ), PartialFailure( request, succeeded_power=Power.from_watts(103.4), - succeeded_components={8}, + succeeded_components={ComponentId(8)}, excess_power=Power.zero(), - failed_components={18}, + failed_components={ComponentId(18)}, failed_power=Power.from_watts(20.0), ), OutOfBounds(request, bounds=PowerBounds(0, 0, 0, 800)), diff --git a/src/frequenz/sdk/microgrid/_power_managing/_base_classes.py b/src/frequenz/sdk/microgrid/_power_managing/_base_classes.py index 1e5756897..c4ea5bd9d 100644 --- a/src/frequenz/sdk/microgrid/_power_managing/_base_classes.py +++ b/src/frequenz/sdk/microgrid/_power_managing/_base_classes.py @@ -10,6 +10,7 @@ import enum import typing +from frequenz.client.microgrid import ComponentId from frequenz.quantities import Power from ... import timeseries @@ -26,7 +27,7 @@ class ReportRequest: source_id: str """The source ID of the actor sending the request.""" - component_ids: frozenset[int] + component_ids: frozenset[ComponentId] """The component IDs to report on.""" priority: int @@ -142,7 +143,7 @@ class Proposal: feature. """ - component_ids: frozenset[int] + component_ids: frozenset[ComponentId] """The component IDs to distribute the power to.""" priority: int @@ -225,7 +226,7 @@ class BaseAlgorithm(abc.ABC): @abc.abstractmethod def calculate_target_power( self, - component_ids: frozenset[int], + component_ids: frozenset[ComponentId], proposal: Proposal | None, system_bounds: SystemBounds, ) -> Power | None: @@ -247,7 +248,7 @@ def calculate_target_power( @abc.abstractmethod def get_status( self, - component_ids: frozenset[int], + component_ids: frozenset[ComponentId], priority: int, system_bounds: SystemBounds, ) -> _Report: diff --git a/src/frequenz/sdk/microgrid/_power_managing/_matryoshka.py b/src/frequenz/sdk/microgrid/_power_managing/_matryoshka.py index b74422a19..3c2810a5f 100644 --- a/src/frequenz/sdk/microgrid/_power_managing/_matryoshka.py +++ b/src/frequenz/sdk/microgrid/_power_managing/_matryoshka.py @@ -23,6 +23,7 @@ import typing from datetime import timedelta +from frequenz.client.microgrid import ComponentId from frequenz.quantities import Power from typing_extensions import override @@ -45,8 +46,8 @@ def __init__( """Create a new instance of the matryoshka algorithm.""" self._max_proposal_age_sec = max_proposal_age.total_seconds() self._default_power = default_power - self._component_buckets: dict[frozenset[int], set[Proposal]] = {} - self._target_power: dict[frozenset[int], Power] = {} + self._component_buckets: dict[frozenset[ComponentId], set[Proposal]] = {} + self._target_power: dict[frozenset[ComponentId], Power] = {} def _calc_target_power( self, @@ -125,7 +126,7 @@ def _calc_target_power( def _validate_component_ids( self, - component_ids: frozenset[int], + component_ids: frozenset[ComponentId], proposal: Proposal | None, system_bounds: SystemBounds, ) -> bool: @@ -149,16 +150,15 @@ def _validate_component_ids( if any(component_id in bucket for component_id in component_ids): comp_ids = ", ".join(map(str, sorted(component_ids))) raise NotImplementedError( - f"PowerManagingActor: component IDs {comp_ids} are already" - " part of another bucket. Overlapping buckets are not" - " yet supported." + f"PowerManagingActor: {comp_ids} are already part of another " + "bucket. Overlapping buckets are not yet supported." ) return True @override def calculate_target_power( self, - component_ids: frozenset[int], + component_ids: frozenset[ComponentId], proposal: Proposal | None, system_bounds: SystemBounds, ) -> Power | None: @@ -226,7 +226,7 @@ def calculate_target_power( @override def get_status( self, - component_ids: frozenset[int], + component_ids: frozenset[ComponentId], priority: int, system_bounds: SystemBounds, ) -> _Report: @@ -297,7 +297,7 @@ def drop_old_proposals(self, loop_time: float) -> None: Args: loop_time: The current loop time. """ - buckets_to_delete: list[frozenset[int]] = [] + buckets_to_delete: list[frozenset[ComponentId]] = [] for component_ids, proposals in self._component_buckets.items(): to_delete: list[Proposal] = [] for proposal in proposals: diff --git a/src/frequenz/sdk/microgrid/_power_managing/_power_managing_actor.py b/src/frequenz/sdk/microgrid/_power_managing/_power_managing_actor.py index 6072df600..a6dfb1541 100644 --- a/src/frequenz/sdk/microgrid/_power_managing/_power_managing_actor.py +++ b/src/frequenz/sdk/microgrid/_power_managing/_power_managing_actor.py @@ -13,7 +13,12 @@ from frequenz.channels import Receiver, Sender, select, selected_from from frequenz.channels.timer import SkipMissedAndDrift, Timer -from frequenz.client.microgrid import ComponentCategory, ComponentType, InverterType +from frequenz.client.microgrid import ( + ComponentCategory, + ComponentId, + ComponentType, + InverterType, +) from typing_extensions import override from ..._internal._asyncio import run_forever @@ -82,9 +87,11 @@ def __init__( # pylint: disable=too-many-arguments self._channel_registry = channel_registry self._proposals_receiver = proposals_receiver - self._system_bounds: dict[frozenset[int], SystemBounds] = {} - self._bound_tracker_tasks: dict[frozenset[int], asyncio.Task[None]] = {} - self._subscriptions: dict[frozenset[int], dict[int, Sender[_Report]]] = {} + self._system_bounds: dict[frozenset[ComponentId], SystemBounds] = {} + self._bound_tracker_tasks: dict[frozenset[ComponentId], asyncio.Task[None]] = {} + self._subscriptions: dict[ + frozenset[ComponentId], dict[int, Sender[_Report]] + ] = {} match algorithm: case Algorithm.MATRYOSHKA: @@ -102,7 +109,7 @@ def __init__( # pylint: disable=too-many-arguments super().__init__() - async def _send_reports(self, component_ids: frozenset[int]) -> None: + async def _send_reports(self, component_ids: frozenset[ComponentId]) -> None: """Send reports for a set of components, to all subscribers. Args: @@ -123,7 +130,7 @@ async def _send_reports(self, component_ids: frozenset[int]) -> None: async def _bounds_tracker( self, - component_ids: frozenset[int], + component_ids: frozenset[ComponentId], bounds_receiver: Receiver[SystemBounds], ) -> None: """Track the power bounds of a set of components and update the cache. @@ -145,7 +152,7 @@ async def _bounds_tracker( await self._send_updated_target_power(component_ids, None) await self._send_reports(component_ids) - def _add_system_bounds_tracker(self, component_ids: frozenset[int]) -> None: + def _add_system_bounds_tracker(self, component_ids: frozenset[ComponentId]) -> None: """Add a system bounds tracker for the given components. Args: @@ -196,7 +203,7 @@ def _add_system_bounds_tracker(self, component_ids: frozenset[int]) -> None: async def _send_updated_target_power( self, - component_ids: frozenset[int], + component_ids: frozenset[ComponentId], proposal: Proposal | None, ) -> None: target_power = self._algorithm.calculate_target_power( diff --git a/src/frequenz/sdk/microgrid/_power_managing/_shifting_matryoshka.py b/src/frequenz/sdk/microgrid/_power_managing/_shifting_matryoshka.py index 20706d470..3e9202685 100644 --- a/src/frequenz/sdk/microgrid/_power_managing/_shifting_matryoshka.py +++ b/src/frequenz/sdk/microgrid/_power_managing/_shifting_matryoshka.py @@ -9,6 +9,7 @@ import typing from datetime import timedelta +from frequenz.client.microgrid import ComponentId from frequenz.quantities import Power from typing_extensions import override @@ -63,12 +64,12 @@ def __init__( """Create a new instance of the matryoshka algorithm.""" self._default_power = default_power self._max_proposal_age_sec = max_proposal_age.total_seconds() - self._component_buckets: dict[frozenset[int], set[Proposal]] = {} - self._target_power: dict[frozenset[int], Power] = {} + self._component_buckets: dict[frozenset[ComponentId], set[Proposal]] = {} + self._target_power: dict[frozenset[ComponentId], Power] = {} def _calc_targets( self, - component_ids: frozenset[int], + component_ids: frozenset[ComponentId], system_bounds: SystemBounds, priority: int | None = None, ) -> tuple[Power | None, Bounds[Power]]: @@ -188,7 +189,7 @@ def _calc_targets( def _validate_component_ids( self, - component_ids: frozenset[int], + component_ids: frozenset[ComponentId], proposal: Proposal | None, system_bounds: SystemBounds, ) -> bool: @@ -210,17 +211,17 @@ def _validate_component_ids( for bucket in self._component_buckets: if any(component_id in bucket for component_id in component_ids): + comp_ids = ", ".join(map(str, sorted(component_ids))) raise NotImplementedError( - f"PowerManagingActor: component IDs {component_ids} are already" - + " part of another bucket. Overlapping buckets are not" - + " yet supported." + f"PowerManagingActor: {comp_ids} are already part of another " + + "bucket. Overlapping buckets are not yet supported." ) return True @override def calculate_target_power( self, - component_ids: frozenset[int], + component_ids: frozenset[ComponentId], proposal: Proposal | None, system_bounds: SystemBounds, ) -> Power | None: @@ -282,7 +283,7 @@ def calculate_target_power( @override def get_status( # pylint: disable=too-many-locals self, - component_ids: frozenset[int], + component_ids: frozenset[ComponentId], priority: int, system_bounds: SystemBounds, ) -> _Report: @@ -317,7 +318,7 @@ def drop_old_proposals(self, loop_time: float) -> None: Args: loop_time: The current loop time. """ - buckets_to_delete: list[frozenset[int]] = [] + buckets_to_delete: list[frozenset[ComponentId]] = [] for component_ids, proposals in self._component_buckets.items(): to_delete: list[Proposal] = [] for proposal in proposals: diff --git a/src/frequenz/sdk/microgrid/component_graph.py b/src/frequenz/sdk/microgrid/component_graph.py index 51bb8c023..af752669f 100644 --- a/src/frequenz/sdk/microgrid/component_graph.py +++ b/src/frequenz/sdk/microgrid/component_graph.py @@ -30,6 +30,7 @@ from frequenz.client.microgrid import ( Component, ComponentCategory, + ComponentId, Connection, InverterType, MicrogridApiClient, @@ -54,7 +55,7 @@ class ComponentGraph(ABC): @abstractmethod def components( self, - component_ids: set[int] | None = None, + component_ids: set[ComponentId] | None = None, component_categories: set[ComponentCategory] | None = None, ) -> set[Component]: """Fetch the components of the microgrid. @@ -71,8 +72,8 @@ def components( @abstractmethod def connections( self, - start: set[int] | None = None, - end: set[int] | None = None, + start: set[ComponentId] | None = None, + end: set[ComponentId] | None = None, ) -> set[Connection]: """Fetch the connections between microgrid components. @@ -86,7 +87,7 @@ def connections( """ @abstractmethod - def predecessors(self, component_id: int) -> set[Component]: + def predecessors(self, component_id: ComponentId) -> set[Component]: """Fetch the graph predecessors of the specified component. Args: @@ -103,7 +104,7 @@ def predecessors(self, component_id: int) -> set[Component]: """ @abstractmethod - def successors(self, component_id: int) -> set[Component]: + def successors(self, component_id: ComponentId) -> set[Component]: """Fetch the graph successors of the specified component. Args: @@ -373,7 +374,7 @@ def __init__( def components( self, - component_ids: set[int] | None = None, + component_ids: set[ComponentId] | None = None, component_categories: set[ComponentCategory] | None = None, ) -> set[Component]: """Fetch the components of the microgrid. @@ -402,8 +403,8 @@ def components( def connections( self, - start: set[int] | None = None, - end: set[int] | None = None, + start: set[ComponentId] | None = None, + end: set[ComponentId] | None = None, ) -> set[Connection]: """Fetch the connections between microgrid components. @@ -429,7 +430,7 @@ def connections( return set(self._graph.edges[i][_DATA_KEY] for i in selection_ids) - def predecessors(self, component_id: int) -> set[Component]: + def predecessors(self, component_id: ComponentId) -> set[Component]: """Fetch the graph predecessors of the specified component. Args: @@ -446,14 +447,14 @@ def predecessors(self, component_id: int) -> set[Component]: """ if component_id not in self._graph: raise KeyError( - f"Component {component_id} not in graph, cannot get predecessors!" + f"Component with {component_id} not in graph, cannot get predecessors!" ) predecessors_ids = self._graph.predecessors(component_id) return set(map(lambda idx: self._graph.nodes[idx][_DATA_KEY], predecessors_ids)) - def successors(self, component_id: int) -> set[Component]: + def successors(self, component_id: ComponentId) -> set[Component]: """Fetch the graph successors of the specified component. Args: @@ -469,7 +470,7 @@ def successors(self, component_id: int) -> set[Component]: """ if component_id not in self._graph: raise KeyError( - f"Component {component_id} not in graph, cannot get successors!" + f"Component with {component_id} not in graph, cannot get successors!" ) successors_ids = self._graph.successors(component_id) @@ -977,11 +978,13 @@ def _validate_grid_endpoint(self) -> None: if self._graph.in_degree(grid_id) > 0: grid_predecessors = list(self.predecessors(grid_id)) raise InvalidGraphError( - f"Grid endpoint {grid_id} has graph predecessors: {grid_predecessors}" + f"Grid endpoint with {grid_id} has graph predecessors: {grid_predecessors}" ) if self._graph.out_degree(grid_id) == 0: - raise InvalidGraphError(f"Grid endpoint {grid_id} has no graph successors!") + raise InvalidGraphError( + f"Grid endpoint with {grid_id} has no graph successors!" + ) def _validate_intermediary_components(self) -> None: """Check that intermediary components (e.g. meters) are configured correctly. diff --git a/src/frequenz/sdk/microgrid/connection_manager.py b/src/frequenz/sdk/microgrid/connection_manager.py index afc69ddf7..b6a8b5a0f 100644 --- a/src/frequenz/sdk/microgrid/connection_manager.py +++ b/src/frequenz/sdk/microgrid/connection_manager.py @@ -11,7 +11,12 @@ import logging from abc import ABC, abstractmethod -from frequenz.client.microgrid import Location, Metadata, MicrogridApiClient +from frequenz.client.microgrid import ( + Location, + Metadata, + MicrogridApiClient, + MicrogridId, +) from .component_graph import ComponentGraph, _MicrogridComponentGraph @@ -59,7 +64,7 @@ def component_graph(self) -> ComponentGraph: @property @abstractmethod - def microgrid_id(self) -> int | None: + def microgrid_id(self) -> MicrogridId | None: """Get the ID of the microgrid if available. Returns: @@ -115,7 +120,7 @@ def api_client(self) -> MicrogridApiClient: return self._api @property - def microgrid_id(self) -> int | None: + def microgrid_id(self) -> MicrogridId | None: """Get the ID of the microgrid if available. Returns: diff --git a/src/frequenz/sdk/timeseries/_grid_frequency.py b/src/frequenz/sdk/timeseries/_grid_frequency.py index 54ea1a96b..4630d8cf5 100644 --- a/src/frequenz/sdk/timeseries/_grid_frequency.py +++ b/src/frequenz/sdk/timeseries/_grid_frequency.py @@ -9,7 +9,12 @@ import logging from frequenz.channels import Receiver, Sender -from frequenz.client.microgrid import Component, ComponentCategory, ComponentMetricId +from frequenz.client.microgrid import ( + Component, + ComponentCategory, + ComponentId, + ComponentMetricId, +) from frequenz.quantities import Frequency, Quantity from .._internal._channels import ChannelRegistry @@ -20,7 +25,7 @@ _logger = logging.getLogger(__name__) -def create_request(component_id: int) -> ComponentMetricRequest: +def create_request(component_id: ComponentId) -> ComponentMetricRequest: """Create a request for grid frequency. Args: diff --git a/src/frequenz/sdk/timeseries/battery_pool/_battery_pool.py b/src/frequenz/sdk/timeseries/battery_pool/_battery_pool.py index 5ac2b015b..c4b6ddc0a 100644 --- a/src/frequenz/sdk/timeseries/battery_pool/_battery_pool.py +++ b/src/frequenz/sdk/timeseries/battery_pool/_battery_pool.py @@ -12,6 +12,7 @@ import uuid from collections import abc +from frequenz.client.microgrid import ComponentId from frequenz.quantities import Energy, Percentage, Power, Temperature from ... import timeseries @@ -185,7 +186,7 @@ async def propose_discharge(self, power: Power | None) -> None: ) @property - def component_ids(self) -> abc.Set[int]: + def component_ids(self) -> abc.Set[ComponentId]: """Return ids of the batteries in the pool. Returns: 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 6ffa0f247..83f643345 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,7 +11,7 @@ from typing import Any from frequenz.channels import Receiver, Sender -from frequenz.client.microgrid import ComponentCategory +from frequenz.client.microgrid import ComponentCategory, ComponentId from ..._internal._asyncio import cancel_and_await from ..._internal._channels import ChannelRegistry, ReceiverFetcher @@ -47,7 +47,7 @@ def __init__( # pylint: disable=too-many-arguments power_manager_bounds_subscription_sender: Sender[ReportRequest], power_distribution_results_fetcher: ReceiverFetcher[Result], min_update_interval: timedelta, - batteries_id: Set[int] | None = None, + batteries_id: Set[ComponentId] | None = None, ) -> None: """Create the class instance. @@ -81,13 +81,13 @@ def __init__( # pylint: disable=too-many-arguments battery pool. If None or empty, then all batteries from the microgrid will be used. """ - self._batteries: frozenset[int] + self._batteries: frozenset[ComponentId] if batteries_id: self._batteries = frozenset(batteries_id) else: self._batteries = self._get_all_batteries() - self._working_batteries: set[int] = set() + self._working_batteries: set[ComponentId] = set() self._update_battery_status_task: asyncio.Task[None] | None = None self._batteries_status_receiver: Receiver[ComponentPoolStatus] = ( @@ -133,7 +133,7 @@ async def stop(self) -> None: await asyncio.gather(*tasks_to_stop) self._batteries_status_receiver.close() - def _get_all_batteries(self) -> frozenset[int]: + def _get_all_batteries(self) -> frozenset[ComponentId]: """Get all batteries from the microgrid. Returns: 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 d52c305ba..7ff16b98e 100644 --- a/src/frequenz/sdk/timeseries/battery_pool/_component_metric_fetcher.py +++ b/src/frequenz/sdk/timeseries/battery_pool/_component_metric_fetcher.py @@ -18,6 +18,7 @@ BatteryData, ComponentCategory, ComponentData, + ComponentId, ComponentMetricId, InverterData, ) @@ -41,12 +42,12 @@ class ComponentMetricFetcher(AsyncConstructible, ABC): """Define how to subscribe for and fetch the component metrics data.""" - _component_id: int + _component_id: ComponentId _metrics: Iterable[ComponentMetricId] @classmethod async def async_new( - cls, component_id: int, metrics: Iterable[ComponentMetricId] + cls, component_id: ComponentId, metrics: Iterable[ComponentMetricId] ) -> Self: """Create an instance of this class. @@ -83,7 +84,7 @@ class LatestMetricsFetcher(ComponentMetricFetcher, Generic[T], ABC): @classmethod async def async_new( cls, - component_id: int, + component_id: ComponentId, metrics: Iterable[ComponentMetricId], ) -> Self: """Create instance of this class. @@ -180,7 +181,7 @@ class LatestBatteryMetricsFetcher(LatestMetricsFetcher[BatteryData]): @classmethod async def async_new( # noqa: DOC502 (ValueError is raised indirectly super.async_new) cls, - component_id: int, + component_id: ComponentId, metrics: Iterable[ComponentMetricId], ) -> LatestBatteryMetricsFetcher: """Create instance of this class. @@ -231,7 +232,7 @@ class LatestInverterMetricsFetcher(LatestMetricsFetcher[InverterData]): @classmethod async def async_new( # noqa: DOC502 (ValueError is raised indirectly by super.async_new) cls, - component_id: int, + component_id: ComponentId, metrics: Iterable[ComponentMetricId], ) -> LatestInverterMetricsFetcher: """Create instance of this class. diff --git a/src/frequenz/sdk/timeseries/battery_pool/_component_metrics.py b/src/frequenz/sdk/timeseries/battery_pool/_component_metrics.py index 05f9ffe3c..1ad537323 100644 --- a/src/frequenz/sdk/timeseries/battery_pool/_component_metrics.py +++ b/src/frequenz/sdk/timeseries/battery_pool/_component_metrics.py @@ -8,14 +8,14 @@ from dataclasses import dataclass from datetime import datetime -from frequenz.client.microgrid import ComponentMetricId +from frequenz.client.microgrid import ComponentId, ComponentMetricId @dataclass(frozen=True, eq=False) class ComponentMetricsData: """Store values of the component metrics.""" - component_id: int + component_id: ComponentId """The component ID the data is for.""" timestamp: datetime diff --git a/src/frequenz/sdk/timeseries/battery_pool/_methods.py b/src/frequenz/sdk/timeseries/battery_pool/_methods.py index 15af7eed1..7ffef88a4 100644 --- a/src/frequenz/sdk/timeseries/battery_pool/_methods.py +++ b/src/frequenz/sdk/timeseries/battery_pool/_methods.py @@ -11,6 +11,7 @@ from typing import Generic from frequenz.channels import Broadcast, Receiver +from frequenz.client.microgrid import ComponentId from ..._internal._asyncio import cancel_and_await, run_forever from ..._internal._constants import RECEIVER_MAX_SIZE, WAIT_FOR_COMPONENT_DATA_SEC @@ -32,7 +33,7 @@ class MetricAggregator(Generic[T], ABC): """Interface to control how the component data should be aggregated and send.""" @abstractmethod - def update_working_batteries(self, new_working_batteries: set[int]) -> None: + def update_working_batteries(self, new_working_batteries: set[ComponentId]) -> None: """Update set of the working batteries. Args: @@ -74,7 +75,7 @@ class SendOnUpdate(MetricAggregator[T]): def __init__( self, - working_batteries: set[int], + working_batteries: set[ComponentId], metric_calculator: MetricCalculator[T], min_update_interval: timedelta, ) -> None: @@ -93,7 +94,7 @@ def __init__( inv_invs=False, )["bat_invs"] - self._working_batteries: set[int] = working_batteries.intersection( + self._working_batteries: set[ComponentId] = working_batteries.intersection( metric_calculator.batteries ) self._result_channel: Broadcast[T] = Broadcast( @@ -102,7 +103,7 @@ def __init__( ) self._update_event = asyncio.Event() - self._cached_metrics: dict[int, ComponentMetricsData] = {} + self._cached_metrics: dict[ComponentId, ComponentMetricsData] = {} self._update_task = asyncio.create_task(run_forever(self._update_and_notify)) self._send_task = asyncio.create_task( @@ -112,7 +113,7 @@ def __init__( set() ) - self._fetchers: dict[int, ComponentMetricFetcher] = {} + self._fetchers: dict[ComponentId, ComponentMetricFetcher] = {} @classmethod def name(cls) -> str: @@ -136,7 +137,7 @@ def new_receiver(self, limit: int | None = RECEIVER_MAX_SIZE) -> Receiver[T]: return self._result_channel.new_receiver() return self._result_channel.new_receiver(limit=limit) - def update_working_batteries(self, new_working_batteries: set[int]) -> None: + def update_working_batteries(self, new_working_batteries: set[ComponentId]) -> None: """Update set of the working batteries. Recalculate metric if set changed. @@ -172,7 +173,7 @@ async def stop(self) -> None: fetcher.stop() async def _create_data_fetchers(self) -> None: - fetchers: dict[int, ComponentMetricFetcher] = { + fetchers: dict[ComponentId, ComponentMetricFetcher] = { cid: await LatestBatteryMetricsFetcher.async_new(cid, metrics) for cid, metrics in self._metric_calculator.battery_metrics.items() } @@ -183,7 +184,7 @@ async def _create_data_fetchers(self) -> None: } self._fetchers.update(inverter_fetchers) - def _remove_metric_fetcher(self, component_id: int) -> None: + def _remove_metric_fetcher(self, component_id: ComponentId) -> None: _logger.error( "Removing component %d from the %s formula.", component_id, @@ -213,7 +214,7 @@ async def _update_and_notify(self) -> None: for item in done: metrics = item.result() if metrics is None: - self._remove_metric_fetcher(int(item.get_name())) + self._remove_metric_fetcher(ComponentId(int(item.get_name()))) continue if self._metric_updated(metrics): self._update_event.set() diff --git a/src/frequenz/sdk/timeseries/battery_pool/_metric_calculator.py b/src/frequenz/sdk/timeseries/battery_pool/_metric_calculator.py index 00968fe53..488cffeb2 100644 --- a/src/frequenz/sdk/timeseries/battery_pool/_metric_calculator.py +++ b/src/frequenz/sdk/timeseries/battery_pool/_metric_calculator.py @@ -11,7 +11,7 @@ from datetime import datetime, timezone from typing import Generic, TypeVar -from frequenz.client.microgrid import ComponentMetricId +from frequenz.client.microgrid import ComponentId, ComponentMetricId from frequenz.quantities import Energy, Percentage, Power, Temperature from ... import timeseries @@ -46,7 +46,7 @@ class MetricCalculator(ABC, Generic[T]): * how to calculate the result, """ - def __init__(self, batteries: Set[int]) -> None: + def __init__(self, batteries: Set[ComponentId]) -> None: """Create class instance. Args: @@ -64,7 +64,7 @@ def name(cls) -> str: """ @property - def batteries(self) -> Set[int]: + def batteries(self) -> Set[ComponentId]: """Return set of batteries that should be used to calculate the metrics. Some batteries given in constructor can be discarded @@ -78,7 +78,7 @@ def batteries(self) -> Set[int]: @property @abstractmethod - def battery_metrics(self) -> Mapping[int, list[ComponentMetricId]]: + def battery_metrics(self) -> Mapping[ComponentId, list[ComponentMetricId]]: """Return what metrics are needed for each battery. Returns: @@ -87,7 +87,7 @@ def battery_metrics(self) -> Mapping[int, list[ComponentMetricId]]: @property @abstractmethod - def inverter_metrics(self) -> Mapping[int, list[ComponentMetricId]]: + def inverter_metrics(self) -> Mapping[ComponentId, list[ComponentMetricId]]: """Return what metrics are needed for each inverter. Returns: @@ -97,8 +97,8 @@ def inverter_metrics(self) -> Mapping[int, list[ComponentMetricId]]: @abstractmethod def calculate( self, - metrics_data: dict[int, ComponentMetricsData], - working_batteries: set[int], + metrics_data: dict[ComponentId, ComponentMetricsData], + working_batteries: set[ComponentId], ) -> T: """Aggregate the metrics_data and calculate high level metric. @@ -121,7 +121,7 @@ def calculate( class CapacityCalculator(MetricCalculator[Sample[Energy]]): """Define how to calculate Capacity metrics.""" - def __init__(self, batteries: Set[int]) -> None: + def __init__(self, batteries: Set[ComponentId]) -> None: """Create class instance. Args: @@ -145,7 +145,7 @@ def name(cls) -> str: return "Capacity" @property - def battery_metrics(self) -> Mapping[int, list[ComponentMetricId]]: + def battery_metrics(self) -> Mapping[ComponentId, list[ComponentMetricId]]: """Return what metrics are needed for each battery. Returns: @@ -154,7 +154,7 @@ def battery_metrics(self) -> Mapping[int, list[ComponentMetricId]]: return {bid: self._metrics for bid in self._batteries} @property - def inverter_metrics(self) -> Mapping[int, list[ComponentMetricId]]: + def inverter_metrics(self) -> Mapping[ComponentId, list[ComponentMetricId]]: """Return what metrics are needed for each inverter. Returns: @@ -164,8 +164,8 @@ def inverter_metrics(self) -> Mapping[int, list[ComponentMetricId]]: def calculate( self, - metrics_data: dict[int, ComponentMetricsData], - working_batteries: set[int], + metrics_data: dict[ComponentId, ComponentMetricsData], + working_batteries: set[ComponentId], ) -> Sample[Energy]: """Aggregate the metrics_data and calculate high level metric. @@ -213,7 +213,7 @@ def calculate( class TemperatureCalculator(MetricCalculator[Sample[Temperature]]): """Define how to calculate temperature metrics.""" - def __init__(self, batteries: Set[int]) -> None: + def __init__(self, batteries: Set[ComponentId]) -> None: """Create class instance. Args: @@ -235,7 +235,7 @@ def name(cls) -> str: return "temperature" @property - def battery_metrics(self) -> Mapping[int, list[ComponentMetricId]]: + def battery_metrics(self) -> Mapping[ComponentId, list[ComponentMetricId]]: """Return what metrics are needed for each battery. Returns: @@ -244,7 +244,7 @@ def battery_metrics(self) -> Mapping[int, list[ComponentMetricId]]: return {bid: self._metrics for bid in self._batteries} @property - def inverter_metrics(self) -> Mapping[int, list[ComponentMetricId]]: + def inverter_metrics(self) -> Mapping[ComponentId, list[ComponentMetricId]]: """Return what metrics are needed for each inverter. Returns: @@ -254,8 +254,8 @@ def inverter_metrics(self) -> Mapping[int, list[ComponentMetricId]]: def calculate( self, - metrics_data: dict[int, ComponentMetricsData], - working_batteries: set[int], + metrics_data: dict[ComponentId, ComponentMetricsData], + working_batteries: set[ComponentId], ) -> Sample[Temperature]: """Aggregate the metrics_data and calculate high level metric for temperature. @@ -300,7 +300,7 @@ def calculate( class SoCCalculator(MetricCalculator[Sample[Percentage]]): """Define how to calculate SoC metrics.""" - def __init__(self, batteries: Set[int]) -> None: + def __init__(self, batteries: Set[ComponentId]) -> None: """Create class instance. Args: @@ -325,7 +325,7 @@ def name(cls) -> str: return "SoC" @property - def battery_metrics(self) -> Mapping[int, list[ComponentMetricId]]: + def battery_metrics(self) -> Mapping[ComponentId, list[ComponentMetricId]]: """Return what metrics are needed for each battery. Returns: @@ -334,7 +334,7 @@ def battery_metrics(self) -> Mapping[int, list[ComponentMetricId]]: return {bid: self._metrics for bid in self._batteries} @property - def inverter_metrics(self) -> Mapping[int, list[ComponentMetricId]]: + def inverter_metrics(self) -> Mapping[ComponentId, list[ComponentMetricId]]: """Return what metrics are needed for each inverter. Returns: @@ -344,8 +344,8 @@ def inverter_metrics(self) -> Mapping[int, list[ComponentMetricId]]: def calculate( self, - metrics_data: dict[int, ComponentMetricsData], - working_batteries: set[int], + metrics_data: dict[ComponentId, ComponentMetricsData], + working_batteries: set[ComponentId], ) -> Sample[Percentage]: """Aggregate the metrics_data and calculate high level metric. @@ -437,15 +437,17 @@ class PowerBoundsCalculator(MetricCalculator[SystemBounds]): def __init__( self, - batteries: Set[int], + batteries: Set[ComponentId], ) -> None: """Create class instance. Args: batteries: What batteries should be used for calculation. """ - mappings: dict[str, dict[int, frozenset[int]]] = _get_battery_inverter_mappings( - batteries, inv_bats=False, bat_bats=True, inv_invs=False + mappings: dict[str, dict[ComponentId, frozenset[ComponentId]]] = ( + _get_battery_inverter_mappings( + batteries, inv_bats=False, bat_bats=True, inv_invs=False + ) ) self._bat_inv_map = mappings["bat_invs"] @@ -491,7 +493,7 @@ def name(cls) -> str: return "PowerBounds" @property - def battery_metrics(self) -> Mapping[int, list[ComponentMetricId]]: + def battery_metrics(self) -> Mapping[ComponentId, list[ComponentMetricId]]: """Return what metrics are needed for each battery. Returns: @@ -500,7 +502,7 @@ def battery_metrics(self) -> Mapping[int, list[ComponentMetricId]]: return {bid: self._battery_metrics for bid in set(self._bat_inv_map.keys())} @property - def inverter_metrics(self) -> Mapping[int, list[ComponentMetricId]]: + def inverter_metrics(self) -> Mapping[ComponentId, list[ComponentMetricId]]: """Return what metrics are needed for each inverter. Returns: @@ -515,8 +517,8 @@ def inverter_metrics(self) -> Mapping[int, list[ComponentMetricId]]: # pylint: disable=too-many-locals def calculate( self, - metrics_data: dict[int, ComponentMetricsData], - working_batteries: set[int], + metrics_data: dict[ComponentId, ComponentMetricsData], + working_batteries: set[ComponentId], ) -> SystemBounds: """Aggregate the metrics_data and calculate high level metric. @@ -544,7 +546,7 @@ def calculate( } def get_validated_bounds( - comp_id: int, comp_metric_ids: list[ComponentMetricId] + comp_id: ComponentId, comp_metric_ids: list[ComponentMetricId] ) -> PowerBounds | None: results: list[float] = [] # Make timestamp accessible @@ -570,7 +572,7 @@ def get_validated_bounds( ) def get_bounds_list( - comp_ids: frozenset[int], comp_metric_ids: list[ComponentMetricId] + comp_ids: frozenset[ComponentId], comp_metric_ids: list[ComponentMetricId] ) -> list[PowerBounds]: return list( x 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 741b69bbc..b4b9298e3 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 @@ -8,6 +8,7 @@ import uuid from collections import abc +from frequenz.client.microgrid import ComponentId from frequenz.quantities import Current, Power from ..._internal._channels import MappingReceiverFetcher, ReceiverFetcher @@ -108,7 +109,7 @@ async def propose_power( ) @property - def component_ids(self) -> abc.Set[int]: + def component_ids(self) -> abc.Set[ComponentId]: """Return component IDs of all EV Chargers managed by this EVChargerPool. Returns: diff --git a/src/frequenz/sdk/timeseries/ev_charger_pool/_ev_charger_pool_reference_store.py b/src/frequenz/sdk/timeseries/ev_charger_pool/_ev_charger_pool_reference_store.py index 9b7746916..048d95c13 100644 --- a/src/frequenz/sdk/timeseries/ev_charger_pool/_ev_charger_pool_reference_store.py +++ b/src/frequenz/sdk/timeseries/ev_charger_pool/_ev_charger_pool_reference_store.py @@ -8,7 +8,7 @@ from collections import abc from frequenz.channels import Broadcast, Receiver, Sender -from frequenz.client.microgrid import ComponentCategory +from frequenz.client.microgrid import ComponentCategory, ComponentId from ..._internal._channels import ChannelRegistry, ReceiverFetcher from ...microgrid import connection_manager @@ -42,7 +42,7 @@ def __init__( # pylint: disable=too-many-arguments power_manager_requests_sender: Sender[Proposal], power_manager_bounds_subs_sender: Sender[ReportRequest], power_distribution_results_fetcher: ReceiverFetcher[Result], - component_ids: abc.Set[int] | None = None, + component_ids: abc.Set[ComponentId] | None = None, ): """Create an instance of the class. @@ -71,7 +71,7 @@ def __init__( # pylint: disable=too-many-arguments self.power_distribution_results_fetcher = power_distribution_results_fetcher if component_ids is not None: - self.component_ids: frozenset[int] = frozenset(component_ids) + self.component_ids: frozenset[ComponentId] = frozenset(component_ids) else: graph = connection_manager.get().component_graph self.component_ids = frozenset( diff --git a/src/frequenz/sdk/timeseries/ev_charger_pool/_system_bounds_tracker.py b/src/frequenz/sdk/timeseries/ev_charger_pool/_system_bounds_tracker.py index c8dc85576..e6180cf2f 100644 --- a/src/frequenz/sdk/timeseries/ev_charger_pool/_system_bounds_tracker.py +++ b/src/frequenz/sdk/timeseries/ev_charger_pool/_system_bounds_tracker.py @@ -8,7 +8,7 @@ from collections import abc from frequenz.channels import Receiver, Sender, merge, select, selected_from -from frequenz.client.microgrid import EVChargerData +from frequenz.client.microgrid import ComponentId, EVChargerData from frequenz.quantities import Power from ..._internal._asyncio import run_forever @@ -30,7 +30,7 @@ class EVCSystemBoundsTracker(BackgroundService): def __init__( self, - component_ids: abc.Set[int], + component_ids: abc.Set[ComponentId], status_receiver: Receiver[ComponentPoolStatus], bounds_sender: Sender[SystemBounds], ): @@ -47,7 +47,7 @@ def __init__( self._component_ids = component_ids self._status_receiver = status_receiver self._bounds_sender = bounds_sender - self._latest_component_data: dict[int, EVChargerData] = {} + self._latest_component_data: dict[ComponentId, EVChargerData] = {} self._last_sent_bounds: SystemBounds | None = None self._component_pool_status = ComponentPoolStatus(set(), set()) @@ -117,7 +117,7 @@ async def _run(self) -> None: async for selected in select(status_rx, ev_data_rx): if selected_from(selected, status_rx): self._component_pool_status = selected.message - to_remove = [] + to_remove: list[ComponentId] = [] for comp_id in self._latest_component_data: if ( comp_id not in self._component_pool_status.working 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 b8cf51d74..0dd877b67 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,7 +7,7 @@ import logging from collections import abc -from frequenz.client.microgrid import ComponentCategory, ComponentMetricId +from frequenz.client.microgrid import ComponentCategory, ComponentId, ComponentMetricId from frequenz.quantities import Power from ....microgrid import connection_manager @@ -58,7 +58,7 @@ def generate( # noqa: DOC502 (FormulaGenerationError is raised indirectly by _g return builder.build() - def _get_chp_meters(self) -> abc.Set[int]: + def _get_chp_meters(self) -> abc.Set[ComponentId]: """Get the meter IDs of the CHPs from the component graph. Returns: @@ -75,7 +75,7 @@ def _get_chp_meters(self) -> abc.Set[int]: if comp.category == ComponentCategory.CHP ) - chp_meters: set[int] = set() + chp_meters: set[ComponentId] = set() for chp in chps: predecessors = component_graph.predecessors(chp.component_id) if len(predecessors) != 1: 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 4b53d360f..f692090e0 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,7 @@ import logging from collections import abc -from frequenz.client.microgrid import ComponentMetricId +from frequenz.client.microgrid import ComponentId, ComponentMetricId from frequenz.quantities import Current from .._formula_engine import FormulaEngine, FormulaEngine3Phase @@ -73,7 +73,7 @@ def generate(self) -> FormulaEngine3Phase[Current]: def _gen_phase_formula( self, - component_ids: abc.Set[int], + component_ids: abc.Set[ComponentId], metric_id: ComponentMetricId, ) -> FormulaEngine[Current]: builder = self._get_builder("ev-current", metric_id, Current.from_amperes) 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 d9305a6c6..21e5c87bd 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,7 +13,12 @@ from typing import Generic from frequenz.channels import Sender -from frequenz.client.microgrid import Component, ComponentCategory, ComponentMetricId +from frequenz.client.microgrid import ( + Component, + ComponentCategory, + ComponentId, + ComponentMetricId, +) from ...._internal._channels import ChannelRegistry from ....microgrid import connection_manager @@ -31,7 +36,7 @@ class ComponentNotFound(FormulaGenerationError): """Indicates that a component required for generating a formula is not found.""" -NON_EXISTING_COMPONENT_ID = sys.maxsize +NON_EXISTING_COMPONENT_ID = ComponentId(sys.maxsize) """The component ID for non-existent components in the components graph. The non-existing component ID is commonly used in scenarios where a formula @@ -46,7 +51,7 @@ class ComponentNotFound(FormulaGenerationError): class FormulaGeneratorConfig: """Config for formula generators.""" - component_ids: abc.Set[int] | None = None + component_ids: abc.Set[ComponentId] | None = None """The component IDs to use for generating the formula.""" allow_fallback: bool = True 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 56001a129..0f4eb9a89 100644 --- a/src/frequenz/sdk/timeseries/formula_engine/_resampled_formula_builder.py +++ b/src/frequenz/sdk/timeseries/formula_engine/_resampled_formula_builder.py @@ -8,7 +8,7 @@ from collections.abc import Callable from frequenz.channels import Receiver, Sender -from frequenz.client.microgrid import ComponentMetricId +from frequenz.client.microgrid import ComponentId, ComponentMetricId from frequenz.quantities import Quantity from ..._internal._channels import ChannelRegistry @@ -57,7 +57,7 @@ def __init__( # pylint: disable=too-many-arguments super().__init__(formula_name, create_method) def _get_resampled_receiver( - self, component_id: int, metric_id: ComponentMetricId + self, component_id: ComponentId, metric_id: ComponentMetricId ) -> Receiver[Sample[QuantityT]]: """Get a receiver with the resampled data for the given component id. @@ -92,7 +92,7 @@ async def subscribe(self) -> None: def push_component_metric( self, - component_id: int, + component_id: ComponentId, *, nones_are_zeros: bool, fallback: FallbackMetricFetcher[QuantityT] | None = None, @@ -109,7 +109,7 @@ def push_component_metric( """ receiver = self._get_resampled_receiver(component_id, self._metric_id) self.push_metric( - f"#{component_id}", + f"#{int(component_id)}", receiver, nones_are_zeros=nones_are_zeros, fallback=fallback, @@ -145,7 +145,7 @@ def from_string( for token in tokenizer: if token.type == TokenType.COMPONENT_METRIC: self.push_component_metric( - int(token.value), nones_are_zeros=nones_are_zeros + ComponentId(int(token.value)), nones_are_zeros=nones_are_zeros ) elif token.type == TokenType.OPER: self.push_oper(token.value) diff --git a/src/frequenz/sdk/timeseries/pv_pool/_pv_pool.py b/src/frequenz/sdk/timeseries/pv_pool/_pv_pool.py index 7ae91c4ed..07323d51d 100644 --- a/src/frequenz/sdk/timeseries/pv_pool/_pv_pool.py +++ b/src/frequenz/sdk/timeseries/pv_pool/_pv_pool.py @@ -7,6 +7,7 @@ import uuid from collections import abc +from frequenz.client.microgrid import ComponentId from frequenz.quantities import Power from ..._internal._channels import MappingReceiverFetcher, ReceiverFetcher @@ -98,7 +99,7 @@ async def propose_power( ) @property - def component_ids(self) -> abc.Set[int]: + def component_ids(self) -> abc.Set[ComponentId]: """Return component IDs of all PV inverters managed by this PVPool. Returns: diff --git a/src/frequenz/sdk/timeseries/pv_pool/_pv_pool_reference_store.py b/src/frequenz/sdk/timeseries/pv_pool/_pv_pool_reference_store.py index d912bd5b4..7b0ee7e47 100644 --- a/src/frequenz/sdk/timeseries/pv_pool/_pv_pool_reference_store.py +++ b/src/frequenz/sdk/timeseries/pv_pool/_pv_pool_reference_store.py @@ -9,7 +9,7 @@ from collections import abc from frequenz.channels import Broadcast, Receiver, Sender -from frequenz.client.microgrid import ComponentCategory, InverterType +from frequenz.client.microgrid import ComponentCategory, ComponentId, InverterType from ..._internal._channels import ChannelRegistry, ReceiverFetcher from ...microgrid import connection_manager @@ -43,7 +43,7 @@ def __init__( # pylint: disable=too-many-arguments power_manager_requests_sender: Sender[Proposal], power_manager_bounds_subs_sender: Sender[ReportRequest], power_distribution_results_fetcher: ReceiverFetcher[Result], - component_ids: abc.Set[int] | None = None, + component_ids: abc.Set[ComponentId] | None = None, ): """Initialize this instance. @@ -72,7 +72,7 @@ def __init__( # pylint: disable=too-many-arguments self.power_distribution_results_fetcher = power_distribution_results_fetcher if component_ids is not None: - self.component_ids: frozenset[int] = frozenset(component_ids) + self.component_ids: frozenset[ComponentId] = frozenset(component_ids) else: graph = connection_manager.get().component_graph self.component_ids = frozenset( diff --git a/src/frequenz/sdk/timeseries/pv_pool/_system_bounds_tracker.py b/src/frequenz/sdk/timeseries/pv_pool/_system_bounds_tracker.py index b996f42da..b02e88ad8 100644 --- a/src/frequenz/sdk/timeseries/pv_pool/_system_bounds_tracker.py +++ b/src/frequenz/sdk/timeseries/pv_pool/_system_bounds_tracker.py @@ -7,7 +7,7 @@ from collections import abc from frequenz.channels import Receiver, Sender, merge, select, selected_from -from frequenz.client.microgrid import InverterData +from frequenz.client.microgrid import ComponentId, InverterData from frequenz.quantities import Power from ..._internal._asyncio import run_forever @@ -29,7 +29,7 @@ class PVSystemBoundsTracker(BackgroundService): def __init__( self, - component_ids: abc.Set[int], + component_ids: abc.Set[ComponentId], status_receiver: Receiver[ComponentPoolStatus], bounds_sender: Sender[SystemBounds], ): @@ -46,7 +46,7 @@ def __init__( self._component_ids = component_ids self._status_receiver = status_receiver self._bounds_sender = bounds_sender - self._latest_component_data: dict[int, InverterData] = {} + self._latest_component_data: dict[ComponentId, InverterData] = {} self._last_sent_bounds: SystemBounds | None = None self._component_pool_status = ComponentPoolStatus(set(), set()) @@ -119,7 +119,7 @@ async def _run(self) -> None: async for selected in select(status_rx, pv_data_rx): if selected_from(selected, status_rx): self._component_pool_status = selected.message - to_remove = [] + to_remove: list[ComponentId] = [] for comp_id in self._latest_component_data: if ( comp_id not in self._component_pool_status.working diff --git a/tests/actor/_power_managing/test_matryoshka.py b/tests/actor/_power_managing/test_matryoshka.py index 3e9810332..ef1196eec 100644 --- a/tests/actor/_power_managing/test_matryoshka.py +++ b/tests/actor/_power_managing/test_matryoshka.py @@ -8,6 +8,7 @@ from datetime import datetime, timedelta, timezone import pytest +from frequenz.client.microgrid import ComponentId from frequenz.quantities import Power from frequenz.sdk import timeseries @@ -22,7 +23,7 @@ class StatefulTester: def __init__( self, - batteries: frozenset[int], + batteries: frozenset[ComponentId], system_bounds: _base_types.SystemBounds, ) -> None: """Create a new instance of the stateful tester.""" @@ -40,7 +41,7 @@ def tgt_power( # pylint: disable=too-many-arguments,too-many-positional-argumen bounds: tuple[float | None, float | None], expected: float | None, creation_time: float | None = None, - batteries: frozenset[int] | None = None, + batteries: frozenset[ComponentId] | None = None, ) -> None: """Test the target power calculation.""" self._call_count += 1 @@ -94,7 +95,7 @@ async def test_matryoshka_no_excl() -> None: # pylint: disable=too-many-stateme With just inclusion bounds, and no exclusion bounds. """ - batteries = frozenset({2, 5}) + batteries = frozenset({ComponentId(2), ComponentId(5)}) system_bounds = _base_types.SystemBounds( timestamp=datetime.now(tz=timezone.utc), @@ -202,7 +203,7 @@ async def test_matryoshka_with_excl_1() -> None: With inclusion bounds, and exclusion bounds -30.0 to 0.0. """ - batteries = frozenset({2, 5}) + batteries = frozenset({ComponentId(2), ComponentId(5)}) system_bounds = _base_types.SystemBounds( timestamp=datetime.now(tz=timezone.utc), @@ -252,7 +253,7 @@ async def test_matryoshka_with_excl_2() -> None: With inclusion bounds, and exclusion bounds 0.0 to 30.0. """ - batteries = frozenset({2, 5}) + batteries = frozenset({ComponentId(2), ComponentId(5)}) system_bounds = _base_types.SystemBounds( timestamp=datetime.now(tz=timezone.utc), @@ -311,7 +312,7 @@ async def test_matryoshka_with_excl_3() -> None: With inclusion bounds, and exclusion bounds -30.0 to 30.0. """ - batteries = frozenset({2, 5}) + batteries = frozenset({ComponentId(2), ComponentId(5)}) system_bounds = _base_types.SystemBounds( timestamp=datetime.now(tz=timezone.utc), @@ -368,8 +369,8 @@ async def test_matryoshka_drop_old_proposals() -> None: With inclusion bounds, and exclusion bounds -30.0 to 30.0. """ - batteries = frozenset({2, 5}) - overlapping_batteries = frozenset({5, 8}) + batteries = frozenset({ComponentId(2), ComponentId(5)}) + overlapping_batteries = frozenset({ComponentId(5), ComponentId(8)}) system_bounds = _base_types.SystemBounds( timestamp=datetime.now(tz=timezone.utc), @@ -424,8 +425,8 @@ async def test_matryoshka_drop_old_proposals() -> None: with pytest.raises( NotImplementedError, match=re.escape( - "PowerManagingActor: component IDs 5, 8 are already part of " - "another bucket. Overlapping buckets are not yet supported." + "PowerManagingActor: CID5, CID8 are already part of another bucket. " + "Overlapping buckets are not yet supported." ), ): tester.tgt_power( @@ -475,8 +476,8 @@ async def test_matryoshka_none_proposals() -> None: When a `None` proposal is received, is source id should be dropped from the bucket. Then if the bucket becomes empty, it should be dropped as well. """ - batteries = frozenset({2, 5}) - overlapping_batteries = frozenset({5, 8}) + batteries = frozenset({ComponentId(2), ComponentId(5)}) + overlapping_batteries = frozenset({ComponentId(5), ComponentId(8)}) system_bounds = _base_types.SystemBounds( timestamp=datetime.now(tz=timezone.utc), @@ -490,8 +491,8 @@ def ensure_overlapping_bucket_request_fails() -> None: with pytest.raises( NotImplementedError, match=re.escape( - "PowerManagingActor: component IDs 5, 8 are already part of " - "another bucket. Overlapping buckets are not yet supported." + "PowerManagingActor: CID5, CID8 are already part of another bucket. " + "Overlapping buckets are not yet supported." ), ): tester.tgt_power( diff --git a/tests/actor/_power_managing/test_shifting_matryoshka.py b/tests/actor/_power_managing/test_shifting_matryoshka.py index 658c46df0..724faac1c 100644 --- a/tests/actor/_power_managing/test_shifting_matryoshka.py +++ b/tests/actor/_power_managing/test_shifting_matryoshka.py @@ -10,6 +10,7 @@ from datetime import datetime, timedelta, timezone import pytest +from frequenz.client.microgrid import ComponentId from frequenz.quantities import Power from frequenz.sdk import timeseries @@ -26,7 +27,7 @@ class StatefulTester: def __init__( self, - batteries: frozenset[int], + batteries: frozenset[ComponentId], system_bounds: _base_types.SystemBounds, ) -> None: """Create a new instance of the stateful tester.""" @@ -44,7 +45,7 @@ def tgt_power( # pylint: disable=too-many-arguments,too-many-positional-argumen bounds: tuple[float | None, float | None], expected: float | None, creation_time: float | None = None, - batteries: frozenset[int] | None = None, + batteries: frozenset[ComponentId] | None = None, ) -> None: """Test the target power calculation.""" self._call_count += 1 @@ -98,7 +99,7 @@ async def test_matryoshka_no_excl() -> None: # pylint: disable=too-many-stateme With just inclusion bounds, and no exclusion bounds. """ - batteries = frozenset({2, 5}) + batteries = frozenset({ComponentId(2), ComponentId(5)}) system_bounds = _base_types.SystemBounds( timestamp=datetime.now(tz=timezone.utc), @@ -206,7 +207,7 @@ async def test_matryoshka_simple() -> None: With inclusion bounds, and exclusion bounds -30.0 to 0.0. """ - batteries = frozenset({2, 5}) + batteries = frozenset({ComponentId(2), ComponentId(5)}) system_bounds = _base_types.SystemBounds( timestamp=datetime.now(tz=timezone.utc), @@ -235,7 +236,7 @@ async def test_matryoshka_with_excl_1() -> None: With inclusion bounds, and exclusion bounds -30.0 to 0.0. """ - batteries = frozenset({2, 5}) + batteries = frozenset({ComponentId(2), ComponentId(5)}) system_bounds = _base_types.SystemBounds( timestamp=datetime.now(tz=timezone.utc), @@ -289,7 +290,7 @@ async def test_matryoshka_with_excl_2() -> None: With inclusion bounds, and exclusion bounds 0.0 to 30.0. """ - batteries = frozenset({2, 5}) + batteries = frozenset({ComponentId(2), ComponentId(5)}) system_bounds = _base_types.SystemBounds( timestamp=datetime.now(tz=timezone.utc), @@ -344,7 +345,7 @@ async def test_matryoshka_with_excl_3() -> None: With inclusion bounds, and exclusion bounds -30.0 to 30.0. """ - batteries = frozenset({2, 5}) + batteries = frozenset({ComponentId(2), ComponentId(5)}) system_bounds = _base_types.SystemBounds( timestamp=datetime.now(tz=timezone.utc), @@ -401,8 +402,8 @@ async def test_matryoshka_drop_old_proposals() -> None: With inclusion bounds, and exclusion bounds -30.0 to 30.0. """ - batteries = frozenset({2, 5}) - overlapping_batteries = frozenset({5, 8}) + batteries = frozenset({ComponentId(2), ComponentId(5)}) + overlapping_batteries = frozenset({ComponentId(5), ComponentId(8)}) system_bounds = _base_types.SystemBounds( timestamp=datetime.now(tz=timezone.utc), @@ -457,8 +458,8 @@ async def test_matryoshka_drop_old_proposals() -> None: with pytest.raises( NotImplementedError, match=re.escape( - "PowerManagingActor: component IDs frozenset({8, 5}) are already " - + "part of another bucket. Overlapping buckets are not yet supported." + "PowerManagingActor: CID5, CID8 are already part of another bucket. " + + " Overlapping buckets are not yet supported." ), ): tester.tgt_power( @@ -508,8 +509,8 @@ async def test_matryoshka_none_proposals() -> None: When a `None` proposal is received, is source id should be dropped from the bucket. Then if the bucket becomes empty, it should be dropped as well. """ - batteries = frozenset({2, 5}) - overlapping_batteries = frozenset({5, 8}) + batteries = frozenset({ComponentId(2), ComponentId(5)}) + overlapping_batteries = frozenset({ComponentId(5), ComponentId(8)}) system_bounds = _base_types.SystemBounds( timestamp=datetime.now(tz=timezone.utc), @@ -523,8 +524,8 @@ def ensure_overlapping_bucket_request_fails() -> None: with pytest.raises( NotImplementedError, match=re.escape( - "PowerManagingActor: component IDs frozenset({8, 5}) are already " - + "part of another bucket. Overlapping buckets are not yet supported." + "PowerManagingActor: CID5, CID8 are already part of another bucket. " + + " Overlapping buckets are not yet supported." ), ): tester.tgt_power( @@ -579,7 +580,7 @@ async def test_matryoshka_shifting_limiting() -> None: | | | | | Power | | | | | | | Setpoint | 50 kW | """ - batteries = frozenset({2, 5}) + batteries = frozenset({ComponentId(2), ComponentId(5)}) system_bounds = _base_types.SystemBounds( timestamp=datetime.now(tz=timezone.utc), diff --git a/tests/actor/test_resampling.py b/tests/actor/test_resampling.py index d98bd2aa1..b5af4ef57 100644 --- a/tests/actor/test_resampling.py +++ b/tests/actor/test_resampling.py @@ -10,7 +10,7 @@ import pytest import time_machine from frequenz.channels import Broadcast -from frequenz.client.microgrid import ComponentMetricId +from frequenz.client.microgrid import ComponentId, ComponentMetricId from frequenz.quantities import Quantity from frequenz.sdk._internal._channels import ChannelRegistry @@ -119,7 +119,7 @@ async def test_single_request( ) as resampling_actor: subs_req = ComponentMetricRequest( namespace="Resampling", - component_id=9, + component_id=ComponentId(9), metric_id=ComponentMetricId.SOC, start_time=None, ) @@ -162,7 +162,7 @@ async def test_duplicate_request( ) as resampling_actor: subs_req = ComponentMetricRequest( namespace="Resampling", - component_id=9, + component_id=ComponentId(9), metric_id=ComponentMetricId.SOC, start_time=None, ) diff --git a/tests/microgrid/power_distributing/_component_status/test_battery_pool_status.py b/tests/microgrid/power_distributing/_component_status/test_battery_pool_status.py index ec66366d6..99aa395ef 100644 --- a/tests/microgrid/power_distributing/_component_status/test_battery_pool_status.py +++ b/tests/microgrid/power_distributing/_component_status/test_battery_pool_status.py @@ -6,7 +6,7 @@ from datetime import timedelta from frequenz.channels import Broadcast -from frequenz.client.microgrid import ComponentCategory +from frequenz.client.microgrid import ComponentCategory, ComponentId from pytest_mock import MockerFixture from frequenz.sdk.microgrid._power_distributing._component_pool_status_tracker import ( @@ -56,7 +56,7 @@ async def test_batteries_status(self, mocker: MockerFixture) -> None: ) await asyncio.sleep(0.1) - expected_working: set[int] = set() + expected_working: set[ComponentId] = set() assert ( batteries_status.get_working_components(batteries) == expected_working ) @@ -73,7 +73,7 @@ async def test_batteries_status(self, mocker: MockerFixture) -> None: expected_working.add(batteries_list[0]) await mock_microgrid.mock_client.send( - inverter_data(component_id=batteries_list[0] - 1) + inverter_data(component_id=ComponentId(int(batteries_list[0]) - 1)) ) await asyncio.sleep(0.1) assert ( @@ -83,14 +83,14 @@ async def test_batteries_status(self, mocker: MockerFixture) -> None: assert msg == batteries_status._current_status await mock_microgrid.mock_client.send( - inverter_data(component_id=batteries_list[1] - 1) + inverter_data(component_id=ComponentId(int(batteries_list[1]) - 1)) ) await mock_microgrid.mock_client.send( battery_data(component_id=batteries_list[1]) ) await mock_microgrid.mock_client.send( - inverter_data(component_id=batteries_list[2] - 1) + inverter_data(component_id=ComponentId(int(batteries_list[2]) - 1)) ) await mock_microgrid.mock_client.send( battery_data(component_id=batteries_list[2]) @@ -105,15 +105,22 @@ async def test_batteries_status(self, mocker: MockerFixture) -> None: assert msg == batteries_status._current_status await batteries_status.update_status( - succeeded_components={9}, failed_components={19, 29} + succeeded_components={ComponentId(9)}, + failed_components={ComponentId(19), ComponentId(29)}, ) await asyncio.sleep(0.1) - assert batteries_status.get_working_components(batteries) == {9} + assert batteries_status.get_working_components(batteries) == { + ComponentId(9) + } await batteries_status.update_status( - succeeded_components={9, 19}, failed_components=set() + succeeded_components={ComponentId(9), ComponentId(19)}, + failed_components=set(), ) await asyncio.sleep(0.1) - assert batteries_status.get_working_components(batteries) == {9, 19} + assert batteries_status.get_working_components(batteries) == { + ComponentId(9), + ComponentId(19), + } await batteries_status.stop() diff --git a/tests/microgrid/power_distributing/_component_status/test_battery_status.py b/tests/microgrid/power_distributing/_component_status/test_battery_status.py index 9f5d5ce02..0121fe7c0 100644 --- a/tests/microgrid/power_distributing/_component_status/test_battery_status.py +++ b/tests/microgrid/power_distributing/_component_status/test_battery_status.py @@ -20,6 +20,7 @@ BatteryError, BatteryErrorCode, BatteryRelayState, + ComponentId, ErrorLevel, InverterComponentState, InverterData, @@ -42,7 +43,7 @@ def battery_data( # pylint: disable=too-many-arguments,too-many-positional-arguments - component_id: int, + component_id: ComponentId, timestamp: datetime | None = None, relay_state: BatteryRelayState = BatteryRelayState.CLOSED, component_state: BatteryComponentState = BatteryComponentState.CHARGING, @@ -80,7 +81,7 @@ def battery_data( # pylint: disable=too-many-arguments,too-many-positional-argu def inverter_data( - component_id: int, + component_id: ComponentId, timestamp: datetime | None = None, component_state: InverterComponentState = InverterComponentState.CHARGING, errors: list[InverterError] | None = None, @@ -120,8 +121,8 @@ class Message(Generic[T]): inner: T -BATTERY_ID = 9 -INVERTER_ID = 8 +BATTERY_ID = ComponentId(9) +INVERTER_ID = ComponentId(8) # pylint: disable=protected-access, unused-argument @@ -345,7 +346,7 @@ async def test_sync_blocking_feature(self, mocker: MockerFixture) -> None: # message is not correct, component should not block. tracker._handle_status_set_power_result( - SetPowerResult(succeeded={1}, failed={BATTERY_ID}) + SetPowerResult(succeeded={ComponentId(1)}, failed={BATTERY_ID}) ) assert tracker._get_new_status_if_changed() is None @@ -361,7 +362,7 @@ async def test_sync_blocking_feature(self, mocker: MockerFixture) -> None: for timeout in expected_blocking_timeout: # message is not correct, component should not block. tracker._handle_status_set_power_result( - SetPowerResult(succeeded={1}, failed={BATTERY_ID}) + SetPowerResult(succeeded={ComponentId(1)}, failed={BATTERY_ID}) ) assert ( @@ -372,7 +373,7 @@ async def test_sync_blocking_feature(self, mocker: MockerFixture) -> None: # Battery should be still blocked, nothing should happen time.shift(timeout - 1) tracker._handle_status_set_power_result( - SetPowerResult(succeeded={1}, failed={BATTERY_ID}) + SetPowerResult(succeeded={ComponentId(1)}, failed={BATTERY_ID}) ) assert tracker._get_new_status_if_changed() is None @@ -395,7 +396,7 @@ async def test_sync_blocking_feature(self, mocker: MockerFixture) -> None: # should block for 30 sec tracker._handle_status_set_power_result( - SetPowerResult(succeeded={1}, failed={BATTERY_ID}) + SetPowerResult(succeeded={ComponentId(1)}, failed={BATTERY_ID}) ) assert ( @@ -424,7 +425,7 @@ async def test_sync_blocking_feature(self, mocker: MockerFixture) -> None: # should block for 30 sec tracker._handle_status_set_power_result( - SetPowerResult(succeeded={1}, failed={BATTERY_ID}) + SetPowerResult(succeeded={ComponentId(1)}, failed={BATTERY_ID}) ) assert ( tracker._get_new_status_if_changed() @@ -434,7 +435,7 @@ async def test_sync_blocking_feature(self, mocker: MockerFixture) -> None: # If battery succeed, then it should unblock. tracker._handle_status_set_power_result( - SetPowerResult(succeeded={BATTERY_ID}, failed={19}) + SetPowerResult(succeeded={BATTERY_ID}, failed={ComponentId(19)}) ) assert ( tracker._get_new_status_if_changed() is ComponentStatusEnum.WORKING @@ -478,7 +479,7 @@ async def test_sync_blocking_interrupted_with_with_max_data( assert tracker._get_new_status_if_changed() is ComponentStatusEnum.WORKING tracker._handle_status_set_power_result( - SetPowerResult(succeeded={1}, failed={BATTERY_ID}) + SetPowerResult(succeeded={ComponentId(1)}, failed={BATTERY_ID}) ) assert tracker._get_new_status_if_changed() is ComponentStatusEnum.UNCERTAIN @@ -486,7 +487,7 @@ async def test_sync_blocking_interrupted_with_with_max_data( for timeout in expected_blocking_timeout: # message is not correct, component should not block. tracker._handle_status_set_power_result( - SetPowerResult(succeeded={1}, failed={BATTERY_ID}) + SetPowerResult(succeeded={ComponentId(1)}, failed={BATTERY_ID}) ) assert tracker._get_new_status_if_changed() is None time_machine.move_to(start + timedelta(seconds=timeout)) @@ -528,7 +529,7 @@ async def test_sync_blocking_interrupted_with_invalid_message( assert tracker._get_new_status_if_changed() is ComponentStatusEnum.WORKING tracker._handle_status_set_power_result( - SetPowerResult(succeeded={1}, failed={BATTERY_ID}) + SetPowerResult(succeeded={ComponentId(1)}, failed={BATTERY_ID}) ) assert tracker._get_new_status_if_changed() is ComponentStatusEnum.UNCERTAIN @@ -543,7 +544,7 @@ async def test_sync_blocking_interrupted_with_invalid_message( ) tracker._handle_status_set_power_result( - SetPowerResult(succeeded={1}, failed={BATTERY_ID}) + SetPowerResult(succeeded={ComponentId(1)}, failed={BATTERY_ID}) ) assert tracker._get_new_status_if_changed() is None diff --git a/tests/microgrid/power_distributing/_component_status/test_ev_charger_status.py b/tests/microgrid/power_distributing/_component_status/test_ev_charger_status.py index a1f35be39..6f7e586c5 100644 --- a/tests/microgrid/power_distributing/_component_status/test_ev_charger_status.py +++ b/tests/microgrid/power_distributing/_component_status/test_ev_charger_status.py @@ -7,7 +7,11 @@ from datetime import datetime, timedelta, timezone from frequenz.channels import Broadcast -from frequenz.client.microgrid import EVChargerCableState, EVChargerComponentState +from frequenz.client.microgrid import ( + ComponentId, + EVChargerCableState, + EVChargerComponentState, +) from pytest_mock import MockerFixture from frequenz.sdk._internal._asyncio import cancel_and_await @@ -22,7 +26,7 @@ from ....utils.component_data_wrapper import EvChargerDataWrapper from ....utils.receive_timeout import Timeout, receive_timeout -_EV_CHARGER_ID = 6 +_EV_CHARGER_ID = ComponentId(6) class TestEVChargerStatusTracker: diff --git a/tests/microgrid/power_distributing/_component_status/test_pv_inverter_status.py b/tests/microgrid/power_distributing/_component_status/test_pv_inverter_status.py index f11c9a5f1..c77a4af0a 100644 --- a/tests/microgrid/power_distributing/_component_status/test_pv_inverter_status.py +++ b/tests/microgrid/power_distributing/_component_status/test_pv_inverter_status.py @@ -8,7 +8,7 @@ from datetime import datetime, timedelta, timezone from frequenz.channels import Broadcast -from frequenz.client.microgrid import InverterComponentState +from frequenz.client.microgrid import ComponentId, InverterComponentState from pytest_mock import MockerFixture from frequenz.sdk._internal._asyncio import cancel_and_await @@ -23,7 +23,7 @@ from ....utils.component_data_wrapper import InverterDataWrapper from ....utils.receive_timeout import Timeout, receive_timeout -_PV_INVERTER_ID = 8 +_PV_INVERTER_ID = ComponentId(8) class TestPVInverterStatusTracker: diff --git a/tests/microgrid/power_distributing/test_battery_distribution_algorithm.py b/tests/microgrid/power_distributing/test_battery_distribution_algorithm.py index d2acd63a4..6cad62c9f 100644 --- a/tests/microgrid/power_distributing/test_battery_distribution_algorithm.py +++ b/tests/microgrid/power_distributing/test_battery_distribution_algorithm.py @@ -8,7 +8,7 @@ from dataclasses import dataclass from datetime import datetime, timezone -from frequenz.client.microgrid import BatteryData, InverterData +from frequenz.client.microgrid import BatteryData, ComponentId, InverterData from frequenz.quantities import Power from pytest import approx, raises @@ -40,7 +40,7 @@ class Metric: def battery_msg( # pylint: disable=too-many-arguments - component_id: int, + component_id: ComponentId, capacity: Metric, soc: Metric, power: PowerBounds, @@ -73,7 +73,7 @@ def battery_msg( # pylint: disable=too-many-arguments def inverter_msg( - component_id: int, + component_id: ComponentId, power: PowerBounds, timestamp: datetime = datetime.now(timezone.utc), ) -> InverterData: @@ -116,8 +116,10 @@ def create_components( """ components: list[InvBatPair] = [] for i in range(0, num): - battery = battery_msg(2 * i, capacity[i], soc[i], power_bounds[2 * i]) - inverter = inverter_msg(2 * i + 1, power_bounds[2 * i + 1]) + battery = battery_msg( + ComponentId(2 * i), capacity[i], soc[i], power_bounds[2 * i] + ) + inverter = inverter_msg(ComponentId(2 * i + 1), power_bounds[2 * i + 1]) components.append(InvBatPair(AggregatedBatteryData([battery]), [inverter])) return components @@ -141,12 +143,13 @@ def create_components_with_capacity( if share_inverter: shared_inverter = InverterDataWrapper( - component_id=start_id + 1, timestamp=datetime.now(tz=timezone.utc) + component_id=ComponentId(start_id + 1), + timestamp=datetime.now(tz=timezone.utc), ) for i in range(start_id, num): battery_data = BatteryDataWrapper( - component_id=2 * i, + component_id=ComponentId(2 * i), timestamp=datetime.now(tz=timezone.utc), capacity=capacity[i], ) @@ -155,7 +158,8 @@ def create_components_with_capacity( inverter_data = shared_inverter else: inverter_data = InverterDataWrapper( - component_id=2 * i + 1, timestamp=datetime.now(tz=timezone.utc) + component_id=ComponentId(2 * i + 1), + timestamp=datetime.now(tz=timezone.utc), ) components.append( @@ -185,12 +189,15 @@ def test_distribute_power_one_battery(self) -> None: capacity: list[float] = [98000] components = self.create_components_with_capacity(1, capacity) - available_soc: dict[int, float] = {0: 40} - incl_bounds: dict[int, Power] = { - 0: Power.from_watts(500), - 1: Power.from_watts(500), + available_soc: dict[ComponentId, float] = {ComponentId(0): 40} + incl_bounds: dict[ComponentId, Power] = { + ComponentId(0): Power.from_watts(500), + ComponentId(1): Power.from_watts(500), + } + excl_bounds: dict[ComponentId, Power] = { + ComponentId(0): Power.zero(), + ComponentId(1): Power.zero(), } - excl_bounds: dict[int, Power] = {0: Power.zero(), 1: Power.zero()} algorithm = BatteryDistributionAlgorithm(distributor_exponent=1) result = algorithm._distribute_power( # pylint: disable=protected-access @@ -202,7 +209,7 @@ def test_distribute_power_one_battery(self) -> None: ) for key, value in {1: 500}.items(): - assert result.distribution[key].as_watts() == approx(value) + assert result.distribution[ComponentId(key)].as_watts() == approx(value) assert result.remaining_power.as_watts() == approx(150.0) @@ -215,18 +222,21 @@ def test_distribute_power_two_batteries_1(self) -> None: capacity: list[float] = [98000, 98000] components = self.create_components_with_capacity(2, capacity) - available_soc: dict[int, float] = {0: 40, 2: 20} - incl_bounds: dict[int, Power] = { - 0: Power.from_watts(500), - 2: Power.from_watts(500), - 1: Power.from_watts(500), - 3: Power.from_watts(500), + available_soc: dict[ComponentId, float] = { + ComponentId(0): 40, + ComponentId(2): 20, + } + incl_bounds: dict[ComponentId, Power] = { + ComponentId(0): Power.from_watts(500), + ComponentId(2): Power.from_watts(500), + ComponentId(1): Power.from_watts(500), + ComponentId(3): Power.from_watts(500), } - excl_bounds: dict[int, Power] = { - 0: Power.zero(), - 2: Power.zero(), - 1: Power.zero(), - 3: Power.zero(), + excl_bounds: dict[ComponentId, Power] = { + ComponentId(0): Power.zero(), + ComponentId(2): Power.zero(), + ComponentId(1): Power.zero(), + ComponentId(3): Power.zero(), } algorithm = BatteryDistributionAlgorithm(distributor_exponent=1) @@ -239,7 +249,7 @@ def test_distribute_power_two_batteries_1(self) -> None: ) for key, value in {1: 400, 3: 200}.items(): - assert result.distribution[key].as_watts() == approx(value) + assert result.distribution[ComponentId(key)].as_watts() == approx(value) assert result.remaining_power.as_watts() == approx(0.0) @@ -252,18 +262,21 @@ def test_distribute_power_two_batteries_2(self) -> None: capacity: list[float] = [49000, 98000] components = self.create_components_with_capacity(2, capacity) - available_soc: dict[int, float] = {0: 20, 2: 20} - incl_bounds: dict[int, Power] = { - 0: Power.from_watts(500), - 2: Power.from_watts(500), - 1: Power.from_watts(500), - 3: Power.from_watts(500), + available_soc: dict[ComponentId, float] = { + ComponentId(0): 20, + ComponentId(2): 20, } - excl_bounds: dict[int, Power] = { - 0: Power.zero(), - 2: Power.zero(), - 1: Power.zero(), - 3: Power.zero(), + incl_bounds: dict[ComponentId, Power] = { + ComponentId(0): Power.from_watts(500), + ComponentId(2): Power.from_watts(500), + ComponentId(1): Power.from_watts(500), + ComponentId(3): Power.from_watts(500), + } + excl_bounds: dict[ComponentId, Power] = { + ComponentId(0): Power.zero(), + ComponentId(2): Power.zero(), + ComponentId(1): Power.zero(), + ComponentId(3): Power.zero(), } algorithm = BatteryDistributionAlgorithm(distributor_exponent=1) @@ -276,7 +289,7 @@ def test_distribute_power_two_batteries_2(self) -> None: ) for key, value in {1: 200, 3: 400}.items(): - assert result.distribution[key].as_watts() == approx(value) + assert result.distribution[ComponentId(key)].as_watts() == approx(value) assert result.remaining_power.as_watts() == approx(0.0) @@ -291,16 +304,19 @@ def test_distribute_power_two_batteries_one_inverter(self) -> None: 2, capacity, share_inverter=True ) - available_soc: dict[int, float] = {0: 20, 2: 30} - incl_bounds: dict[int, Power] = { - 0: Power.from_watts(500), - 2: Power.from_watts(500), - 1: Power.from_watts(500), + available_soc: dict[ComponentId, float] = { + ComponentId(0): 20, + ComponentId(2): 30, + } + incl_bounds: dict[ComponentId, Power] = { + ComponentId(0): Power.from_watts(500), + ComponentId(2): Power.from_watts(500), + ComponentId(1): Power.from_watts(500), } - excl_bounds: dict[int, Power] = { - 0: Power.zero(), - 2: Power.zero(), - 1: Power.zero(), + excl_bounds: dict[ComponentId, Power] = { + ComponentId(0): Power.zero(), + ComponentId(2): Power.zero(), + ComponentId(1): Power.zero(), } algorithm = BatteryDistributionAlgorithm(distributor_exponent=1) @@ -313,7 +329,7 @@ def test_distribute_power_two_batteries_one_inverter(self) -> None: ) for key, value in {1: 500}.items(): - assert result.distribution[key].as_watts() == approx(value) + assert result.distribution[ComponentId(key)].as_watts() == approx(value) assert result.remaining_power.as_watts() == approx(100.0) @@ -327,18 +343,21 @@ def test_distribute_power_two_batteries_bounds(self) -> None: capacity: list[float] = [49000, 98000] components = self.create_components_with_capacity(2, capacity) - available_soc: dict[int, float] = {0: 40, 2: 20} - incl_bounds: dict[int, Power] = { - 0: Power.from_watts(250), - 2: Power.from_watts(330), - 1: Power.from_watts(250), - 3: Power.from_watts(330), + available_soc: dict[ComponentId, float] = { + ComponentId(0): 40, + ComponentId(2): 20, + } + incl_bounds: dict[ComponentId, Power] = { + ComponentId(0): Power.from_watts(250), + ComponentId(2): Power.from_watts(330), + ComponentId(1): Power.from_watts(250), + ComponentId(3): Power.from_watts(330), } - excl_bounds: dict[int, Power] = { - 0: Power.zero(), - 2: Power.zero(), - 1: Power.zero(), - 3: Power.zero(), + excl_bounds: dict[ComponentId, Power] = { + ComponentId(0): Power.zero(), + ComponentId(2): Power.zero(), + ComponentId(1): Power.zero(), + ComponentId(3): Power.zero(), } algorithm = BatteryDistributionAlgorithm(distributor_exponent=1) @@ -351,7 +370,7 @@ def test_distribute_power_two_batteries_bounds(self) -> None: ) for key, value in {1: 250, 3: 330}.items(): - assert result.distribution[key].as_watts() == approx(value) + assert result.distribution[ComponentId(key)].as_watts() == approx(value) assert result.remaining_power.as_watts() == approx(20.0) @@ -360,22 +379,26 @@ def test_distribute_power_three_batteries(self) -> None: capacity: list[float] = [49000, 98000, 49000] components = self.create_components_with_capacity(3, capacity) - available_soc: dict[int, float] = {0: 40, 2: 20, 4: 20} - incl_bounds: dict[int, Power] = { - 0: Power.from_watts(1000), - 2: Power.from_watts(1000), - 4: Power.from_watts(1000), - 1: Power.from_watts(1000), - 3: Power.from_watts(3400), - 5: Power.from_watts(3550), + available_soc: dict[ComponentId, float] = { + ComponentId(0): 40, + ComponentId(2): 20, + ComponentId(4): 20, } - excl_bounds: dict[int, Power] = { - 0: Power.zero(), - 2: Power.zero(), - 4: Power.zero(), - 1: Power.zero(), - 3: Power.zero(), - 5: Power.zero(), + incl_bounds: dict[ComponentId, Power] = { + ComponentId(0): Power.from_watts(1000), + ComponentId(2): Power.from_watts(1000), + ComponentId(4): Power.from_watts(1000), + ComponentId(1): Power.from_watts(1000), + ComponentId(3): Power.from_watts(3400), + ComponentId(5): Power.from_watts(3550), + } + excl_bounds: dict[ComponentId, Power] = { + ComponentId(0): Power.zero(), + ComponentId(2): Power.zero(), + ComponentId(4): Power.zero(), + ComponentId(1): Power.zero(), + ComponentId(3): Power.zero(), + ComponentId(5): Power.zero(), } algorithm = BatteryDistributionAlgorithm(distributor_exponent=1) @@ -388,7 +411,7 @@ def test_distribute_power_three_batteries(self) -> None: ) for key, value in {1: 400, 3: 400, 5: 200}.items(): - assert result.distribution[key].as_watts() == approx(value) + assert result.distribution[ComponentId(key)].as_watts() == approx(value) assert result.remaining_power.as_watts() == approx(0.0) @@ -397,22 +420,26 @@ def test_distribute_power_three_batteries_2(self) -> None: capacity: list[float] = [98000, 49000, 49000] components = self.create_components_with_capacity(3, capacity) - available_soc: dict[int, float] = {0: 80, 2: 10, 4: 20} - incl_bounds: dict[int, Power] = { - 0: Power.from_watts(1000), - 2: Power.from_watts(1000), - 4: Power.from_watts(1000), - 1: Power.from_watts(400), - 3: Power.from_watts(3400), - 5: Power.from_watts(300), + available_soc: dict[ComponentId, float] = { + ComponentId(0): 80, + ComponentId(2): 10, + ComponentId(4): 20, + } + incl_bounds: dict[ComponentId, Power] = { + ComponentId(0): Power.from_watts(1000), + ComponentId(2): Power.from_watts(1000), + ComponentId(4): Power.from_watts(1000), + ComponentId(1): Power.from_watts(400), + ComponentId(3): Power.from_watts(3400), + ComponentId(5): Power.from_watts(300), } - excl_bounds: dict[int, Power] = { - 0: Power.zero(), - 2: Power.zero(), - 4: Power.zero(), - 1: Power.zero(), - 3: Power.zero(), - 5: Power.zero(), + excl_bounds: dict[ComponentId, Power] = { + ComponentId(0): Power.zero(), + ComponentId(2): Power.zero(), + ComponentId(4): Power.zero(), + ComponentId(1): Power.zero(), + ComponentId(3): Power.zero(), + ComponentId(5): Power.zero(), } algorithm = BatteryDistributionAlgorithm(distributor_exponent=1) @@ -425,7 +452,7 @@ def test_distribute_power_three_batteries_2(self) -> None: ) for key, value in {1: 400, 3: 300, 5: 300}.items(): - assert result.distribution[key].as_watts() == approx(value) + assert result.distribution[ComponentId(key)].as_watts() == approx(value) assert result.remaining_power.as_watts() == approx(0.0) @@ -434,22 +461,26 @@ def test_distribute_power_three_batteries_3(self) -> None: capacity: list[float] = [0, 49000, 0] components = self.create_components_with_capacity(3, capacity) - available_soc: dict[int, float] = {0: 80, 2: 10, 4: 20} - incl_bounds: dict[int, Power] = { - 0: Power.from_watts(1000), - 2: Power.from_watts(1000), - 4: Power.from_watts(1000), - 1: Power.from_watts(500), - 3: Power.from_watts(300), - 5: Power.from_watts(300), + available_soc: dict[ComponentId, float] = { + ComponentId(0): 80, + ComponentId(2): 10, + ComponentId(4): 20, + } + incl_bounds: dict[ComponentId, Power] = { + ComponentId(0): Power.from_watts(1000), + ComponentId(2): Power.from_watts(1000), + ComponentId(4): Power.from_watts(1000), + ComponentId(1): Power.from_watts(500), + ComponentId(3): Power.from_watts(300), + ComponentId(5): Power.from_watts(300), } - excl_bounds: dict[int, Power] = { - 0: Power.zero(), - 2: Power.zero(), - 4: Power.zero(), - 1: Power.zero(), - 3: Power.zero(), - 5: Power.zero(), + excl_bounds: dict[ComponentId, Power] = { + ComponentId(0): Power.zero(), + ComponentId(2): Power.zero(), + ComponentId(4): Power.zero(), + ComponentId(1): Power.zero(), + ComponentId(3): Power.zero(), + ComponentId(5): Power.zero(), } algorithm = BatteryDistributionAlgorithm(distributor_exponent=1) @@ -462,7 +493,7 @@ def test_distribute_power_three_batteries_3(self) -> None: ) for key, value in {1: 0, 3: 300, 5: 0}.items(): - assert result.distribution[key].as_watts() == approx(value) + assert result.distribution[ComponentId(key)].as_watts() == approx(value) assert result.remaining_power.as_watts() == approx(700.0) @@ -504,7 +535,7 @@ def test_supply_three_batteries_1(self) -> None: result = algorithm.distribute_power(Power.from_watts(-1200), components) for key, value in {1: -200, 3: -400, 5: -600}.items(): - assert result.distribution[key].as_watts() == approx(value) + assert result.distribution[ComponentId(key)].as_watts() == approx(value) assert result.remaining_power.as_watts() == approx(0.0) @@ -543,7 +574,7 @@ def test_supply_three_batteries_2(self) -> None: result = algorithm.distribute_power(Power.from_watts(-1400), components) for key, value in {1: -400, 3: -400, 5: -600}.items(): - assert result.distribution[key].as_watts() == approx(value) + assert result.distribution[ComponentId(key)].as_watts() == approx(value) assert result.remaining_power.as_watts() == approx(0.0) @@ -582,7 +613,7 @@ def test_supply_three_batteries_3(self) -> None: result = algorithm.distribute_power(Power.from_watts(-1400), components) for key, value in {1: -500, 3: -100, 5: -800}.items(): - assert result.distribution[key].as_watts() == approx(value) + assert result.distribution[ComponentId(key)].as_watts() == approx(value) assert result.remaining_power.as_watts() == approx(0.0) @@ -621,7 +652,7 @@ def test_supply_three_batteries_4(self) -> None: result = algorithm.distribute_power(Power.from_watts(-1700), components) for key, value in {1: -600, 3: -100, 5: -800}.items(): - assert result.distribution[key].as_watts() == approx(value) + assert result.distribution[ComponentId(key)].as_watts() == approx(value) assert result.remaining_power.as_watts() == approx(-200.0) @@ -660,7 +691,7 @@ def test_supply_three_batteries_5(self) -> None: result = algorithm.distribute_power(Power.from_watts(-1700), components) for key, value in {1: 0, 3: -100, 5: 0}.items(): - assert result.distribution[key].as_watts() == approx(value) + assert result.distribution[ComponentId(key)].as_watts() == approx(value) assert result.remaining_power.as_watts() == approx(-1600.0) @@ -693,7 +724,7 @@ def test_supply_two_batteries_1(self) -> None: result = algorithm.distribute_power(Power.from_watts(-600), components) for key, value in {1: -500, 3: -100}.items(): - assert result.distribution[key].as_watts() == approx(value) + assert result.distribution[ComponentId(key)].as_watts() == approx(value) assert result.remaining_power.as_watts() == approx(0.0) @@ -725,7 +756,7 @@ def test_supply_two_batteries_2(self) -> None: result = algorithm.distribute_power(Power.from_watts(-600), components) for key, value in {1: -346.1538, 3: -253.8461}.items(): - assert result.distribution[key].as_watts() == approx(value) + assert result.distribution[ComponentId(key)].as_watts() == approx(value) assert result.remaining_power.as_watts() == approx(0.0) @@ -765,7 +796,7 @@ def test_consumption_three_batteries_1(self) -> None: result = algorithm.distribute_power(Power.from_watts(1200), components) for key, value in {1: 200, 3: 400, 5: 600}.items(): - assert result.distribution[key].as_watts() == approx(value) + assert result.distribution[ComponentId(key)].as_watts() == approx(value) assert result.remaining_power.as_watts() == approx(0.0) @@ -804,7 +835,7 @@ def test_consumption_three_batteries_2(self) -> None: result = algorithm.distribute_power(Power.from_watts(1400), components) for key, value in {1: 400, 3: 400, 5: 600}.items(): - assert result.distribution[key].as_watts() == approx(value) + assert result.distribution[ComponentId(key)].as_watts() == approx(value) assert result.remaining_power.as_watts() == approx(0.0) @@ -843,7 +874,7 @@ def test_consumption_three_batteries_3(self) -> None: result = algorithm.distribute_power(Power.from_watts(1400), components) for key, value in {1: 500, 3: 100, 5: 800}.items(): - assert result.distribution[key].as_watts() == approx(value) + assert result.distribution[ComponentId(key)].as_watts() == approx(value) assert result.remaining_power.as_watts() == approx(0.0) @@ -882,7 +913,7 @@ def test_consumption_three_batteries_4(self) -> None: result = algorithm.distribute_power(Power.from_watts(1700), components) for key, value in {1: 600, 3: 100, 5: 800}.items(): - assert result.distribution[key].as_watts() == approx(value) + assert result.distribution[ComponentId(key)].as_watts() == approx(value) assert result.remaining_power.as_watts() == approx(200.0) @@ -921,7 +952,7 @@ def test_consumption_three_batteries_5(self) -> None: result = algorithm.distribute_power(Power.from_watts(1700), components) for key, value in {1: 0, 3: 100, 5: 0}.items(): - assert result.distribution[key].as_watts() == approx(value) + assert result.distribution[ComponentId(key)].as_watts() == approx(value) assert result.remaining_power.as_watts() == approx(1600.0) @@ -960,7 +991,7 @@ def test_consumption_three_batteries_6(self) -> None: result = algorithm.distribute_power(Power.from_watts(1700), components) for key, value in {1: 0, 3: 100, 5: 800}.items(): - assert result.distribution[key].as_watts() == approx(value) + assert result.distribution[ComponentId(key)].as_watts() == approx(value) assert result.remaining_power.as_watts() == approx(800.0) @@ -999,7 +1030,7 @@ def test_consumption_three_batteries_7(self) -> None: result = algorithm.distribute_power(Power.from_watts(500), components) for key, value in {1: 498.3388, 3: 1.661129, 5: 0}.items(): - assert result.distribution[key].as_watts() == approx(value) + assert result.distribution[ComponentId(key)].as_watts() == approx(value) assert result.remaining_power.as_watts() == approx(0.0) @@ -1031,7 +1062,7 @@ def test_consumption_two_batteries_1(self) -> None: result = algorithm.distribute_power(Power.from_watts(600), components) for key, value in {1: 100, 3: 500}.items(): - assert result.distribution[key].as_watts() == approx(value) + assert result.distribution[ComponentId(key)].as_watts() == approx(value) assert result.remaining_power.as_watts() == approx(0.0) @@ -1063,7 +1094,7 @@ def test_consumption_two_batteries_distribution_exponent(self) -> None: result = algorithm.distribute_power(Power.from_watts(8000), components) for key, value in {1: 2000.0, 3: 6000.0}.items(): - assert result.distribution[key].as_watts() == approx(value) + assert result.distribution[ComponentId(key)].as_watts() == approx(value) assert result.remaining_power.as_watts() == approx(0.0) @@ -1071,7 +1102,7 @@ def test_consumption_two_batteries_distribution_exponent(self) -> None: result2 = algorithm2.distribute_power(Power.from_watts(8000), components) for key, value in {1: 800.0, 3: 7200.0}.items(): - assert result2.distribution[key].as_watts() == approx(value) + assert result2.distribution[ComponentId(key)].as_watts() == approx(value) assert result2.remaining_power.as_watts() == approx(0.0) @@ -1079,7 +1110,7 @@ def test_consumption_two_batteries_distribution_exponent(self) -> None: result3 = algorithm3.distribute_power(Power.from_watts(8000), components) for key, value in {1: 285.7142, 3: 7714.2857}.items(): - assert result3.distribution[key].as_watts() == approx(value) + assert result3.distribution[ComponentId(key)].as_watts() == approx(value) assert result3.remaining_power.as_watts() == approx(0.0) @@ -1111,7 +1142,7 @@ def test_consumption_two_batteries_distribution_exponent_1(self) -> None: result = algorithm.distribute_power(Power.from_watts(900), components) for key, value in {1: 300.0, 3: 600.0}.items(): - assert result.distribution[key].as_watts() == approx(value) + assert result.distribution[ComponentId(key)].as_watts() == approx(value) assert result.remaining_power.as_watts() == approx(0.0) @@ -1119,7 +1150,7 @@ def test_consumption_two_batteries_distribution_exponent_1(self) -> None: result = algorithm.distribute_power(Power.from_watts(8000), components) for key, value in {1: 2666.6666, 3: 5333.3333}.items(): - assert result.distribution[key].as_watts() == approx(value) + assert result.distribution[ComponentId(key)].as_watts() == approx(value) assert result.remaining_power.as_watts() == approx(0.0) @@ -1127,7 +1158,7 @@ def test_consumption_two_batteries_distribution_exponent_1(self) -> None: result2 = algorithm2.distribute_power(Power.from_watts(900), components) for key, value in {1: 180, 3: 720}.items(): - assert result2.distribution[key].as_watts() == approx(value) + assert result2.distribution[ComponentId(key)].as_watts() == approx(value) assert result2.remaining_power.as_watts() == approx(0.0) @@ -1135,7 +1166,7 @@ def test_consumption_two_batteries_distribution_exponent_1(self) -> None: result2 = algorithm2.distribute_power(Power.from_watts(8000), components) for key, value in {1: 1600, 3: 6400}.items(): - assert result2.distribution[key].as_watts() == approx(value) + assert result2.distribution[ComponentId(key)].as_watts() == approx(value) assert result2.remaining_power.as_watts() == approx(0.0) @@ -1143,7 +1174,7 @@ def test_consumption_two_batteries_distribution_exponent_1(self) -> None: result2 = algorithm2.distribute_power(Power.from_watts(900), components) for key, value in {1: 100, 3: 800}.items(): - assert result2.distribution[key].as_watts() == approx(value) + assert result2.distribution[ComponentId(key)].as_watts() == approx(value) assert result2.remaining_power.as_watts() == approx(0.0) @@ -1151,7 +1182,7 @@ def test_consumption_two_batteries_distribution_exponent_1(self) -> None: result3 = algorithm3.distribute_power(Power.from_watts(8000), components) for key, value in {1: 888.8888, 3: 7111.1111}.items(): - assert result3.distribution[key].as_watts() == approx(value) + assert result3.distribution[ComponentId(key)].as_watts() == approx(value) assert result3.remaining_power.as_watts() == approx(0.0) @@ -1183,7 +1214,7 @@ def test_supply_two_batteries_distribution_exponent(self) -> None: result = algorithm.distribute_power(Power.from_watts(-8000), components) for key, value in {1: -2000.0, 3: -6000.0}.items(): - assert result.distribution[key].as_watts() == approx(value) + assert result.distribution[ComponentId(key)].as_watts() == approx(value) assert result.remaining_power.as_watts() == approx(0.0) @@ -1191,7 +1222,7 @@ def test_supply_two_batteries_distribution_exponent(self) -> None: result2 = algorithm2.distribute_power(Power.from_watts(-8000), components) for key, value in {1: -800, 3: -7200}.items(): - assert result2.distribution[key].as_watts() == approx(value) + assert result2.distribution[ComponentId(key)].as_watts() == approx(value) assert result2.remaining_power.as_watts() == approx(0.0) @@ -1199,7 +1230,7 @@ def test_supply_two_batteries_distribution_exponent(self) -> None: result3 = algorithm3.distribute_power(Power.from_watts(-8000), components) for key, value in {1: -285.7142, 3: -7714.2857}.items(): - assert result3.distribution[key].as_watts() == approx(value) + assert result3.distribution[ComponentId(key)].as_watts() == approx(value) assert result3.remaining_power.as_watts() == approx(0.0) @@ -1231,7 +1262,7 @@ def test_supply_two_batteries_distribution_exponent_1(self) -> None: result = algorithm.distribute_power(Power.from_watts(-8000), components) for key, value in {1: -2666.6666, 3: -5333.3333}.items(): - assert result.distribution[key].as_watts() == approx(value) + assert result.distribution[ComponentId(key)].as_watts() == approx(value) assert result.remaining_power.as_watts() == approx(0.0) @@ -1239,7 +1270,7 @@ def test_supply_two_batteries_distribution_exponent_1(self) -> None: result2 = algorithm2.distribute_power(Power.from_watts(-8000), components) for key, value in {1: -1600, 3: -6400}.items(): - assert result2.distribution[key].as_watts() == approx(value) + assert result2.distribution[ComponentId(key)].as_watts() == approx(value) assert result2.remaining_power.as_watts() == approx(0.0) @@ -1247,7 +1278,7 @@ def test_supply_two_batteries_distribution_exponent_1(self) -> None: result3 = algorithm3.distribute_power(Power.from_watts(-8000), components) for key, value in {1: -888.8888, 3: -7111.1111}.items(): - assert result3.distribution[key].as_watts() == approx(value) + assert result3.distribution[ComponentId(key)].as_watts() == approx(value) assert result3.remaining_power.as_watts() == approx(0.0) @@ -1286,7 +1317,7 @@ def test_supply_three_batteries_distribution_exponent_2(self) -> None: result = algorithm.distribute_power(Power.from_watts(-8000), components) for key, value in {1: -1777.7777, 3: -2666.6666, 5: -3555.5555}.items(): - assert result.distribution[key].as_watts() == approx(value) + assert result.distribution[ComponentId(key)].as_watts() == approx(value) assert result.remaining_power.as_watts() == approx(0.0) @@ -1294,7 +1325,7 @@ def test_supply_three_batteries_distribution_exponent_2(self) -> None: result2 = algorithm2.distribute_power(Power.from_watts(-8000), components) for key, value in {1: -1103.4482, 3: -2482.7586, 5: -4413.7931}.items(): - assert result2.distribution[key].as_watts() == approx(value) + assert result2.distribution[ComponentId(key)].as_watts() == approx(value) assert result2.remaining_power.as_watts() == approx(0.0) @@ -1302,7 +1333,7 @@ def test_supply_three_batteries_distribution_exponent_2(self) -> None: result3 = algorithm3.distribute_power(Power.from_watts(-8000), components) for key, value in {1: -646.4646, 3: -2181.8181, 5: -5171.7171}.items(): - assert result3.distribution[key].as_watts() == approx(value) + assert result3.distribution[ComponentId(key)].as_watts() == approx(value) assert result3.remaining_power.as_watts() == approx(0.0) @@ -1341,7 +1372,7 @@ def test_supply_three_batteries_distribution_exponent_3(self) -> None: result = algorithm.distribute_power(Power.from_watts(-1300), components) for key, value in {1: -600, 3: -400, 5: -300}.items(): - assert result.distribution[key].as_watts() == approx(value) + assert result.distribution[ComponentId(key)].as_watts() == approx(value) assert result.remaining_power.as_watts() == approx(0.0) @@ -1349,11 +1380,11 @@ def test_supply_three_batteries_distribution_exponent_3(self) -> None: result = algorithm.distribute_power(Power.from_watts(-1200), components) for key, value in {1: -400, 3: -400, 5: -400}.items(): - assert result.distribution[key].as_watts() == approx(value) + assert result.distribution[ComponentId(key)].as_watts() == approx(value) assert result.remaining_power.as_watts() == approx(0.0) - def test_supply_two_batteries_distribution_exponent_less_then_1(self) -> None: + def test_supply_two_batteries_distribution_exponent_less_than_1(self) -> None: """Distribute power.""" capacity: list[Metric] = [Metric(98000), Metric(98000)] soc: list[Metric] = [ @@ -1381,7 +1412,7 @@ def test_supply_two_batteries_distribution_exponent_less_then_1(self) -> None: result = algorithm.distribute_power(Power.from_watts(1000), components) for key, value in {1: 600, 3: 400}.items(): - assert result.distribution[key].as_watts() == approx(value) + assert result.distribution[ComponentId(key)].as_watts() == approx(value) assert result.remaining_power.as_watts() == approx(0.0) @@ -1389,7 +1420,7 @@ def test_supply_two_batteries_distribution_exponent_less_then_1(self) -> None: result = algorithm.distribute_power(Power.from_watts(1000), components) for key, value in {1: 500, 3: 500}.items(): - assert result.distribution[key].as_watts() == approx(value) + assert result.distribution[ComponentId(key)].as_watts() == approx(value) assert result.remaining_power.as_watts() == approx(0.0) @@ -1417,7 +1448,9 @@ def assert_any_result( ) -> None: """Assert the result is as expected, disregarding which power goes to which component.""" assert result.remaining_power == expected_remaining_power - assert {*result.distribution.keys()} == expected_distribution_ids + assert {*result.distribution.keys()} == { + *map(ComponentId, expected_distribution_ids) + } assert list(sorted(result.distribution.values())) == list( sorted(expected_distribution_powers) ) @@ -1503,7 +1536,11 @@ def test_scenario_1(self) -> None: self.assert_result( algorithm.distribute_power(Power.zero(), components), DistributionResult( - {1: Power.zero(), 3: Power.zero(), 5: Power.zero()}, + { + ComponentId(1): Power.zero(), + ComponentId(3): Power.zero(), + ComponentId(5): Power.zero(), + }, remaining_power=Power.zero(), ), ) @@ -1512,9 +1549,9 @@ def test_scenario_1(self) -> None: algorithm.distribute_power(Power.from_watts(-300), components), DistributionResult( { - 1: Power.from_watts(-100), - 3: Power.from_watts(-100), - 5: Power.from_watts(-100), + ComponentId(1): Power.from_watts(-100), + ComponentId(3): Power.from_watts(-100), + ComponentId(5): Power.from_watts(-100), }, remaining_power=Power.zero(), ), @@ -1523,9 +1560,9 @@ def test_scenario_1(self) -> None: algorithm.distribute_power(Power.from_watts(300), components), DistributionResult( { - 1: Power.from_watts(100), - 3: Power.from_watts(100), - 5: Power.from_watts(100), + ComponentId(1): Power.from_watts(100), + ComponentId(3): Power.from_watts(100), + ComponentId(5): Power.from_watts(100), }, remaining_power=Power.zero(), ), @@ -1534,9 +1571,9 @@ def test_scenario_1(self) -> None: algorithm.distribute_power(Power.from_watts(-600), components), DistributionResult( { - 1: Power.from_watts(-100), - 3: Power.from_watts(-200), - 5: Power.from_watts(-300), + ComponentId(1): Power.from_watts(-100), + ComponentId(3): Power.from_watts(-200), + ComponentId(5): Power.from_watts(-300), }, remaining_power=Power.zero(), ), @@ -1545,9 +1582,9 @@ def test_scenario_1(self) -> None: algorithm.distribute_power(Power.from_watts(900), components), DistributionResult( { - 1: Power.from_watts(450), - 3: Power.from_watts(300), - 5: Power.from_watts(150), + ComponentId(1): Power.from_watts(450), + ComponentId(3): Power.from_watts(300), + ComponentId(5): Power.from_watts(150), }, remaining_power=Power.zero(), ), @@ -1556,9 +1593,9 @@ def test_scenario_1(self) -> None: algorithm.distribute_power(Power.from_watts(-900), components), DistributionResult( { - 1: Power.from_watts(-150), - 3: Power.from_watts(-300), - 5: Power.from_watts(-450), + ComponentId(1): Power.from_watts(-150), + ComponentId(3): Power.from_watts(-300), + ComponentId(5): Power.from_watts(-450), }, remaining_power=Power.zero(), ), @@ -1567,9 +1604,9 @@ def test_scenario_1(self) -> None: algorithm.distribute_power(Power.from_watts(2200), components), DistributionResult( { - 1: Power.from_watts(1000), - 3: Power.from_watts(833.33), - 5: Power.from_watts(366.66), + ComponentId(1): Power.from_watts(1000), + ComponentId(3): Power.from_watts(833.33), + ComponentId(5): Power.from_watts(366.66), }, remaining_power=Power.zero(), ), @@ -1578,9 +1615,9 @@ def test_scenario_1(self) -> None: algorithm.distribute_power(Power.from_watts(-2200), components), DistributionResult( { - 1: Power.from_watts(-366.66), - 3: Power.from_watts(-833.33), - 5: Power.from_watts(-1000), + ComponentId(1): Power.from_watts(-366.66), + ComponentId(3): Power.from_watts(-833.33), + ComponentId(5): Power.from_watts(-1000), }, remaining_power=Power.zero(), ), @@ -1589,9 +1626,9 @@ def test_scenario_1(self) -> None: algorithm.distribute_power(Power.from_watts(2800), components), DistributionResult( { - 1: Power.from_watts(1000), - 3: Power.from_watts(1000), - 5: Power.from_watts(800), + ComponentId(1): Power.from_watts(1000), + ComponentId(3): Power.from_watts(1000), + ComponentId(5): Power.from_watts(800), }, remaining_power=Power.zero(), ), @@ -1600,9 +1637,9 @@ def test_scenario_1(self) -> None: algorithm.distribute_power(Power.from_watts(-2800), components), DistributionResult( { - 1: Power.from_watts(-800), - 3: Power.from_watts(-1000), - 5: Power.from_watts(-1000), + ComponentId(1): Power.from_watts(-800), + ComponentId(3): Power.from_watts(-1000), + ComponentId(5): Power.from_watts(-1000), }, remaining_power=Power.zero(), ), @@ -1611,9 +1648,9 @@ def test_scenario_1(self) -> None: algorithm.distribute_power(Power.from_watts(3800), components), DistributionResult( { - 1: Power.from_watts(1000), - 3: Power.from_watts(1000), - 5: Power.from_watts(1000), + ComponentId(1): Power.from_watts(1000), + ComponentId(3): Power.from_watts(1000), + ComponentId(5): Power.from_watts(1000), }, remaining_power=Power.from_watts(800.0), ), @@ -1622,9 +1659,9 @@ def test_scenario_1(self) -> None: algorithm.distribute_power(Power.from_watts(-3200), components), DistributionResult( { - 1: Power.from_watts(-1000), - 3: Power.from_watts(-1000), - 5: Power.from_watts(-1000), + ComponentId(1): Power.from_watts(-1000), + ComponentId(3): Power.from_watts(-1000), + ComponentId(5): Power.from_watts(-1000), }, remaining_power=Power.from_watts(-200.0), ), @@ -1712,9 +1749,9 @@ def test_scenario_2(self) -> None: algorithm.distribute_power(Power.from_watts(-300), components), DistributionResult( { - 1: Power.from_watts(-100), - 3: Power.from_watts(-100), - 5: Power.from_watts(-100), + ComponentId(1): Power.from_watts(-100), + ComponentId(3): Power.from_watts(-100), + ComponentId(5): Power.from_watts(-100), }, remaining_power=Power.zero(), ), @@ -1723,9 +1760,9 @@ def test_scenario_2(self) -> None: algorithm.distribute_power(Power.from_watts(300), components), DistributionResult( { - 1: Power.from_watts(100), - 3: Power.from_watts(100), - 5: Power.from_watts(100), + ComponentId(1): Power.from_watts(100), + ComponentId(3): Power.from_watts(100), + ComponentId(5): Power.from_watts(100), }, remaining_power=Power.zero(), ), @@ -1734,9 +1771,9 @@ def test_scenario_2(self) -> None: algorithm.distribute_power(Power.from_watts(-530), components), DistributionResult( { - 1: Power.from_watts(-151.42), - 3: Power.from_watts(-151.42), - 5: Power.from_watts(-227.14), + ComponentId(1): Power.from_watts(-151.42), + ComponentId(3): Power.from_watts(-151.42), + ComponentId(5): Power.from_watts(-227.14), }, remaining_power=Power.zero(), ), @@ -1745,9 +1782,9 @@ def test_scenario_2(self) -> None: algorithm.distribute_power(Power.from_watts(530), components), DistributionResult( { - 1: Power.from_watts(212), - 3: Power.from_watts(212), - 5: Power.from_watts(106), + ComponentId(1): Power.from_watts(212), + ComponentId(3): Power.from_watts(212), + ComponentId(5): Power.from_watts(106), }, remaining_power=Power.zero(), ), @@ -1756,9 +1793,9 @@ def test_scenario_2(self) -> None: algorithm.distribute_power(Power.from_watts(2000), components), DistributionResult( { - 1: Power.from_watts(800), - 3: Power.from_watts(800), - 5: Power.from_watts(400), + ComponentId(1): Power.from_watts(800), + ComponentId(3): Power.from_watts(800), + ComponentId(5): Power.from_watts(400), }, remaining_power=Power.zero(), ), @@ -1767,9 +1804,9 @@ def test_scenario_2(self) -> None: algorithm.distribute_power(Power.from_watts(-2000), components), DistributionResult( { - 1: Power.from_watts(-571.42), - 3: Power.from_watts(-571.42), - 5: Power.from_watts(-857.14), + ComponentId(1): Power.from_watts(-571.42), + ComponentId(3): Power.from_watts(-571.42), + ComponentId(5): Power.from_watts(-857.14), }, remaining_power=Power.zero(), ), @@ -1778,9 +1815,9 @@ def test_scenario_2(self) -> None: algorithm.distribute_power(Power.from_watts(2500), components), DistributionResult( { - 1: Power.from_watts(1000), - 3: Power.from_watts(1000), - 5: Power.from_watts(500), + ComponentId(1): Power.from_watts(1000), + ComponentId(3): Power.from_watts(1000), + ComponentId(5): Power.from_watts(500), }, remaining_power=Power.zero(), ), @@ -1789,9 +1826,9 @@ def test_scenario_2(self) -> None: algorithm.distribute_power(Power.from_watts(-2500), components), DistributionResult( { - 1: Power.from_watts(-785.71), - 3: Power.from_watts(-714.28), - 5: Power.from_watts(-1000.0), + ComponentId(1): Power.from_watts(-785.71), + ComponentId(3): Power.from_watts(-714.28), + ComponentId(5): Power.from_watts(-1000.0), }, remaining_power=Power.zero(), ), @@ -1800,9 +1837,9 @@ def test_scenario_2(self) -> None: algorithm.distribute_power(Power.from_watts(3000), components), DistributionResult( { - 1: Power.from_watts(1000), - 3: Power.from_watts(1000), - 5: Power.from_watts(1000), + ComponentId(1): Power.from_watts(1000), + ComponentId(3): Power.from_watts(1000), + ComponentId(5): Power.from_watts(1000), }, remaining_power=Power.zero(), ), @@ -1811,9 +1848,9 @@ def test_scenario_2(self) -> None: algorithm.distribute_power(Power.from_watts(-3000), components), DistributionResult( { - 1: Power.from_watts(-1000), - 3: Power.from_watts(-1000), - 5: Power.from_watts(-1000), + ComponentId(1): Power.from_watts(-1000), + ComponentId(3): Power.from_watts(-1000), + ComponentId(5): Power.from_watts(-1000), }, remaining_power=Power.zero(), ), @@ -1822,9 +1859,9 @@ def test_scenario_2(self) -> None: algorithm.distribute_power(Power.from_watts(3500), components), DistributionResult( { - 1: Power.from_watts(1000), - 3: Power.from_watts(1000), - 5: Power.from_watts(1000), + ComponentId(1): Power.from_watts(1000), + ComponentId(3): Power.from_watts(1000), + ComponentId(5): Power.from_watts(1000), }, remaining_power=Power.from_watts(500.0), ), @@ -1833,9 +1870,9 @@ def test_scenario_2(self) -> None: algorithm.distribute_power(Power.from_watts(-3500), components), DistributionResult( { - 1: Power.from_watts(-1000), - 3: Power.from_watts(-1000), - 5: Power.from_watts(-1000), + ComponentId(1): Power.from_watts(-1000), + ComponentId(3): Power.from_watts(-1000), + ComponentId(5): Power.from_watts(-1000), }, remaining_power=Power.from_watts(-500.0), ), @@ -1921,9 +1958,9 @@ def test_scenario_3(self) -> None: algorithm.distribute_power(Power.from_watts(-320), components), DistributionResult( { - 1: Power.from_watts(-88), - 3: Power.from_watts(-108.57), - 5: Power.from_watts(-123.43), + ComponentId(1): Power.from_watts(-88), + ComponentId(3): Power.from_watts(-108.57), + ComponentId(5): Power.from_watts(-123.43), }, remaining_power=Power.zero(), ), @@ -1932,9 +1969,9 @@ def test_scenario_3(self) -> None: algorithm.distribute_power(Power.from_watts(320), components), DistributionResult( { - 1: Power.from_watts(128), - 3: Power.from_watts(128), - 5: Power.from_watts(64), + ComponentId(1): Power.from_watts(128), + ComponentId(3): Power.from_watts(128), + ComponentId(5): Power.from_watts(64), }, remaining_power=Power.zero(), ), @@ -1943,9 +1980,9 @@ def test_scenario_3(self) -> None: algorithm.distribute_power(Power.from_watts(-1800), components), DistributionResult( { - 1: Power.from_watts(-514.28), - 3: Power.from_watts(-514.28), - 5: Power.from_watts(-771.42), + ComponentId(1): Power.from_watts(-514.28), + ComponentId(3): Power.from_watts(-514.28), + ComponentId(5): Power.from_watts(-771.42), }, remaining_power=Power.zero(), ), @@ -1954,9 +1991,9 @@ def test_scenario_3(self) -> None: algorithm.distribute_power(Power.from_watts(1800), components), DistributionResult( { - 1: Power.from_watts(720), - 3: Power.from_watts(720), - 5: Power.from_watts(360), + ComponentId(1): Power.from_watts(720), + ComponentId(3): Power.from_watts(720), + ComponentId(5): Power.from_watts(360), }, remaining_power=Power.zero(), ), @@ -1965,9 +2002,9 @@ def test_scenario_3(self) -> None: algorithm.distribute_power(Power.from_watts(-2800), components), DistributionResult( { - 1: Power.from_watts(-800), - 3: Power.from_watts(-1000), - 5: Power.from_watts(-1000), + ComponentId(1): Power.from_watts(-800), + ComponentId(3): Power.from_watts(-1000), + ComponentId(5): Power.from_watts(-1000), }, remaining_power=Power.zero(), ), @@ -1976,9 +2013,9 @@ def test_scenario_3(self) -> None: algorithm.distribute_power(Power.from_watts(2800), components), DistributionResult( { - 1: Power.from_watts(1000), - 3: Power.from_watts(1000), - 5: Power.from_watts(800), + ComponentId(1): Power.from_watts(1000), + ComponentId(3): Power.from_watts(1000), + ComponentId(5): Power.from_watts(800), }, remaining_power=Power.zero(), ), @@ -1987,9 +2024,9 @@ def test_scenario_3(self) -> None: algorithm.distribute_power(Power.from_watts(-3500), components), DistributionResult( { - 1: Power.from_watts(-1000), - 3: Power.from_watts(-1000), - 5: Power.from_watts(-1000), + ComponentId(1): Power.from_watts(-1000), + ComponentId(3): Power.from_watts(-1000), + ComponentId(5): Power.from_watts(-1000), }, remaining_power=Power.from_watts(-500.0), ), @@ -1998,9 +2035,9 @@ def test_scenario_3(self) -> None: algorithm.distribute_power(Power.from_watts(3500), components), DistributionResult( { - 1: Power.from_watts(1000), - 3: Power.from_watts(1000), - 5: Power.from_watts(1000), + ComponentId(1): Power.from_watts(1000), + ComponentId(3): Power.from_watts(1000), + ComponentId(5): Power.from_watts(1000), }, remaining_power=Power.from_watts(500.0), ), @@ -2034,7 +2071,7 @@ def test_scenario_4(self) -> None: AggregatedBatteryData( [ battery_msg( - component_id=1, + component_id=ComponentId(1), capacity=Metric(10000), soc=Metric(50, Bound(10, 90)), power=PowerBounds( @@ -2048,7 +2085,7 @@ def test_scenario_4(self) -> None: ), [ inverter_msg( - 2, + ComponentId(2), PowerBounds( Power.from_watts(-1000), Power.from_watts(-100), @@ -2057,7 +2094,7 @@ def test_scenario_4(self) -> None: ), ), inverter_msg( - 3, + ComponentId(3), PowerBounds( Power.from_watts(-1000), Power.from_watts(-100), @@ -2132,7 +2169,7 @@ def test_scenario_5(self) -> None: AggregatedBatteryData( [ battery_msg( - component_id=1, + component_id=ComponentId(1), capacity=Metric(10000), soc=Metric(20, Bound(10, 90)), power=PowerBounds( @@ -2146,7 +2183,7 @@ def test_scenario_5(self) -> None: ), [ inverter_msg( - 10, + ComponentId(10), PowerBounds( Power.from_watts(-1000), Power.from_watts(-100), @@ -2155,7 +2192,7 @@ def test_scenario_5(self) -> None: ), ), inverter_msg( - 11, + ComponentId(11), PowerBounds( Power.from_watts(-1000), Power.from_watts(-100), @@ -2169,7 +2206,7 @@ def test_scenario_5(self) -> None: AggregatedBatteryData( [ battery_msg( - component_id=2, + component_id=ComponentId(2), capacity=Metric(10000), soc=Metric(60, Bound(10, 90)), power=PowerBounds( @@ -2183,7 +2220,7 @@ def test_scenario_5(self) -> None: ), [ inverter_msg( - 20, + ComponentId(20), PowerBounds( Power.from_watts(-1000), Power.from_watts(-100), @@ -2192,7 +2229,7 @@ def test_scenario_5(self) -> None: ), ), inverter_msg( - 21, + ComponentId(21), PowerBounds( Power.from_watts(-1000), Power.from_watts(-100), diff --git a/tests/microgrid/power_distributing/test_power_distributing.py b/tests/microgrid/power_distributing/test_power_distributing.py index 80976d2b6..e36d3f969 100644 --- a/tests/microgrid/power_distributing/test_power_distributing.py +++ b/tests/microgrid/power_distributing/test_power_distributing.py @@ -12,7 +12,7 @@ from unittest.mock import MagicMock from frequenz.channels import Broadcast -from frequenz.client.microgrid import ComponentCategory +from frequenz.client.microgrid import ComponentCategory, ComponentId from frequenz.quantities import Power from pytest_mock import MockerFixture @@ -64,7 +64,7 @@ async def _patch_battery_pool_status( self, mocks: _Mocks, mocker: MockerFixture, - battery_ids: abc.Set[int] | None = None, + battery_ids: abc.Set[ComponentId] | None = None, ) -> None: """Patch the battery pool status. @@ -115,14 +115,14 @@ async def test_constructor_with_grid_meter(self, mocker: MockerFixture) -> None: ) as distributor: assert isinstance(distributor._component_manager, BatteryManager) assert distributor._component_manager._bat_invs_map == { - 9: {8}, - 19: {18}, - 29: {28}, + ComponentId(9): frozenset({ComponentId(8)}), + ComponentId(19): frozenset({ComponentId(18)}), + ComponentId(29): frozenset({ComponentId(28)}), } assert distributor._component_manager._inv_bats_map == { - 8: {9}, - 18: {19}, - 28: {29}, + ComponentId(8): frozenset({ComponentId(9)}), + ComponentId(18): frozenset({ComponentId(19)}), + ComponentId(28): frozenset({ComponentId(29)}), } async def test_constructor_without_grid_meter(self, mocker: MockerFixture) -> None: @@ -147,22 +147,22 @@ async def test_constructor_without_grid_meter(self, mocker: MockerFixture) -> No ) as distributor: assert isinstance(distributor._component_manager, BatteryManager) assert distributor._component_manager._bat_invs_map == { - 9: {8}, - 19: {18}, - 29: {28}, + ComponentId(9): frozenset({ComponentId(8)}), + ComponentId(19): frozenset({ComponentId(18)}), + ComponentId(29): frozenset({ComponentId(28)}), } assert distributor._component_manager._inv_bats_map == { - 8: {9}, - 18: {19}, - 28: {29}, + ComponentId(8): frozenset({ComponentId(9)}), + ComponentId(18): frozenset({ComponentId(19)}), + ComponentId(28): frozenset({ComponentId(29)}), } async def init_component_data( self, mocks: _Mocks, *, - skip_batteries: abc.Set[int] | None = None, - skip_inverters: abc.Set[int] | None = None, + skip_batteries: abc.Set[ComponentId] | None = None, + skip_inverters: abc.Set[ComponentId] | None = None, ) -> None: """Send initial component data, for power distributor to start.""" for battery_id in set(mocks.microgrid.battery_ids) - (skip_batteries or set()): @@ -205,7 +205,7 @@ async def test_power_distributor_one_user(self, mocker: MockerFixture) -> None: request = Request( power=Power.from_kilowatts(1.2), - component_ids={9, 19}, + component_ids={ComponentId(9), ComponentId(19)}, ) await self._patch_battery_pool_status(mocks, mocker, request.component_ids) @@ -240,12 +240,16 @@ async def test_power_distributor_exclusion_bounds( ) -> None: """Test if power distributing actor rejects non-zero requests in exclusion bounds.""" async with _mocks(mocker, ComponentCategory.BATTERY) as mocks: - await self._patch_battery_pool_status(mocks, mocker, {9, 19}) - await self.init_component_data(mocks, skip_batteries={9, 19}) + await self._patch_battery_pool_status( + mocks, mocker, {ComponentId(9), ComponentId(19)} + ) + await self.init_component_data( + mocks, skip_batteries={ComponentId(9), ComponentId(19)} + ) mocks.streamer.start_streaming( battery_msg( - 9, + ComponentId(9), soc=Metric(60, Bound(20, 80)), capacity=Metric(98000), power=PowerBounds( @@ -260,7 +264,7 @@ async def test_power_distributor_exclusion_bounds( mocks.streamer.start_streaming( battery_msg( - 19, + ComponentId(19), soc=Metric(60, Bound(20, 80)), capacity=Metric(98000), power=PowerBounds( @@ -292,7 +296,7 @@ async def test_power_distributor_exclusion_bounds( # zero power requests should pass through despite the exclusion bounds. request = Request( power=Power.zero(), - component_ids={9, 19}, + component_ids={ComponentId(9), ComponentId(19)}, ) await requests_channel.new_sender().send(request) @@ -310,7 +314,7 @@ async def test_power_distributor_exclusion_bounds( # rejected. request = Request( power=Power.from_watts(300.0), - component_ids={9, 19}, + component_ids={ComponentId(9), ComponentId(19)}, ) await requests_channel.new_sender().send(request) @@ -472,7 +476,7 @@ async def test_two_batteries_one_broken_one_inverters( assert result.request == request assert ( result.msg - == "No data for at least one of the given batteries: 9, 19" + == "No data for at least one of the given batteries: CID9, CID19" ) async def test_battery_two_inverters(self, mocker: MockerFixture) -> None: @@ -826,17 +830,18 @@ async def test_connected_but_not_requested_batteries( assert result.request == request assert ( result.msg - == "Inverter(s) (48) are connected to battery(ies) (19) that were not requested" + == "Inverter(s) (CID48) are connected to battery(ies) (CID19) that" + " were not requested" ) async def test_battery_soc_nan(self, mocker: MockerFixture) -> None: """Test if battery with SoC==NaN is not used.""" async with _mocks(mocker, ComponentCategory.BATTERY, grid_meter=False) as mocks: - await self.init_component_data(mocks, skip_batteries={9}) + await self.init_component_data(mocks, skip_batteries={ComponentId(9)}) mocks.streamer.start_streaming( battery_msg( - 9, + ComponentId(9), soc=Metric(math.nan, Bound(20, 80)), capacity=Metric(98000), power=PowerBounds( @@ -854,7 +859,7 @@ async def test_battery_soc_nan(self, mocker: MockerFixture) -> None: request = Request( power=Power.from_kilowatts(1.2), - component_ids={9, 19}, + component_ids={ComponentId(9), ComponentId(19)}, ) await self._patch_battery_pool_status(mocks, mocker, request.component_ids) @@ -878,7 +883,7 @@ async def test_battery_soc_nan(self, mocker: MockerFixture) -> None: result = await result_rx.receive() assert isinstance(result, Success) - assert result.succeeded_components == {19} + assert result.succeeded_components == {ComponentId(19)} assert result.succeeded_power.isclose(Power.from_watts(500.0)) assert result.excess_power.isclose(Power.from_watts(700.0)) assert result.request == request @@ -886,11 +891,11 @@ async def test_battery_soc_nan(self, mocker: MockerFixture) -> None: async def test_battery_capacity_nan(self, mocker: MockerFixture) -> None: """Test battery with capacity set to NaN is not used.""" async with _mocks(mocker, ComponentCategory.BATTERY, grid_meter=False) as mocks: - await self.init_component_data(mocks, skip_batteries={9}) + await self.init_component_data(mocks, skip_batteries={ComponentId(9)}) mocks.streamer.start_streaming( battery_msg( - 9, + ComponentId(9), soc=Metric(40, Bound(20, 80)), capacity=Metric(math.nan), power=PowerBounds( @@ -908,7 +913,7 @@ async def test_battery_capacity_nan(self, mocker: MockerFixture) -> None: request = Request( power=Power.from_kilowatts(1.2), - component_ids={9, 19}, + component_ids={ComponentId(9), ComponentId(19)}, ) await self._patch_battery_pool_status(mocks, mocker, request.component_ids) @@ -933,7 +938,7 @@ async def test_battery_capacity_nan(self, mocker: MockerFixture) -> None: result = await result_rx.receive() assert isinstance(result, Success) - assert result.succeeded_components == {19} + assert result.succeeded_components == {ComponentId(19)} assert result.succeeded_power.isclose(Power.from_watts(500.0)) assert result.excess_power.isclose(Power.from_watts(700.0)) assert result.request == request @@ -942,12 +947,14 @@ async def test_battery_power_bounds_nan(self, mocker: MockerFixture) -> None: """Test battery with power bounds set to NaN is not used.""" async with _mocks(mocker, ComponentCategory.BATTERY, grid_meter=False) as mocks: await self.init_component_data( - mocks, skip_batteries={9}, skip_inverters={8, 18} + mocks, + skip_batteries={ComponentId(9)}, + skip_inverters={ComponentId(8), ComponentId(18)}, ) mocks.streamer.start_streaming( inverter_msg( - 18, + ComponentId(18), power=PowerBounds( Power.from_watts(-1000), Power.zero(), @@ -961,7 +968,7 @@ async def test_battery_power_bounds_nan(self, mocker: MockerFixture) -> None: # Battery 9 should not work because both battery and inverter sends NaN mocks.streamer.start_streaming( inverter_msg( - 8, + ComponentId(8), power=PowerBounds( Power.from_watts(-1000), Power.zero(), @@ -974,7 +981,7 @@ async def test_battery_power_bounds_nan(self, mocker: MockerFixture) -> None: mocks.streamer.start_streaming( battery_msg( - 9, + ComponentId(9), soc=Metric(40, Bound(20, 80)), capacity=Metric(float(98000)), power=PowerBounds( @@ -992,7 +999,7 @@ async def test_battery_power_bounds_nan(self, mocker: MockerFixture) -> None: request = Request( power=Power.from_kilowatts(1.2), - component_ids={9, 19}, + component_ids={ComponentId(9), ComponentId(19)}, ) await self._patch_battery_pool_status(mocks, mocker, request.component_ids) @@ -1017,7 +1024,7 @@ async def test_battery_power_bounds_nan(self, mocker: MockerFixture) -> None: result = await result_rx.receive() assert isinstance(result, Success) - assert result.succeeded_components == {19} + assert result.succeeded_components == {ComponentId(19)} assert result.succeeded_power.isclose(Power.from_kilowatts(1.0)) assert result.excess_power.isclose(Power.from_watts(200.0)) assert result.request == request @@ -1033,7 +1040,7 @@ async def test_power_distributor_invalid_battery_id( results_channel = Broadcast[Result](name="power_distributor results") request = Request( power=Power.from_kilowatts(1.2), - component_ids={9, 100}, + component_ids={ComponentId(9), ComponentId(100)}, ) await self._patch_battery_pool_status(mocks, mocker, request.component_ids) @@ -1057,7 +1064,10 @@ async def test_power_distributor_invalid_battery_id( assert isinstance(result, Error) assert result.request == request - assert result.msg == "No battery 100, available batteries: 9, 19, 29" + assert ( + result.msg + == "No battery CID100, available batteries: CID9, CID19, CID29" + ) async def test_power_distributor_one_user_adjust_power_consume( self, mocker: MockerFixture @@ -1071,7 +1081,7 @@ async def test_power_distributor_one_user_adjust_power_consume( request = Request( power=Power.from_kilowatts(1.2), - component_ids={9, 19}, + component_ids={ComponentId(9), ComponentId(19)}, adjust_power=False, ) @@ -1113,7 +1123,7 @@ async def test_power_distributor_one_user_adjust_power_supply( request = Request( power=-Power.from_kilowatts(1.2), - component_ids={9, 19}, + component_ids={ComponentId(9), ComponentId(19)}, adjust_power=False, ) @@ -1155,7 +1165,7 @@ async def test_power_distributor_one_user_adjust_power_success( request = Request( power=Power.from_kilowatts(1.0), - component_ids={9, 19}, + component_ids={ComponentId(9), ComponentId(19)}, adjust_power=False, ) @@ -1190,9 +1200,11 @@ async def test_not_all_batteries_are_working(self, mocker: MockerFixture) -> Non async with _mocks(mocker, ComponentCategory.BATTERY, grid_meter=False) as mocks: await self.init_component_data(mocks) - batteries = {9, 19} + batteries = {ComponentId(9), ComponentId(19)} - await self._patch_battery_pool_status(mocks, mocker, batteries - {9}) + await self._patch_battery_pool_status( + mocks, mocker, batteries - {ComponentId(9)} + ) requests_channel = Broadcast[Request](name="power_distributor requests") results_channel = Broadcast[Result](name="power_distributor results") @@ -1222,7 +1234,7 @@ async def test_not_all_batteries_are_working(self, mocker: MockerFixture) -> Non result = await result_rx.receive() assert isinstance(result, Success) - assert result.succeeded_components == {19} + assert result.succeeded_components == {ComponentId(19)} assert result.excess_power.isclose(Power.from_watts(700.0)) assert result.succeeded_power.isclose(Power.from_watts(500.0)) assert result.request == request @@ -1232,8 +1244,8 @@ async def test_partial_failure_result(self, mocker: MockerFixture) -> None: async with _mocks(mocker, ComponentCategory.BATTERY, grid_meter=False) as mocks: await self.init_component_data(mocks) - batteries = {9, 19, 29} - failed_batteries = {9} + batteries = {ComponentId(9), ComponentId(19), ComponentId(29)} + failed_batteries = {ComponentId(9)} failed_power = Power.from_watts(500.0) await self._patch_battery_pool_status(mocks, mocker, batteries) diff --git a/tests/microgrid/test_data_sourcing.py b/tests/microgrid/test_data_sourcing.py index 1ee207ce1..01ba35f9e 100644 --- a/tests/microgrid/test_data_sourcing.py +++ b/tests/microgrid/test_data_sourcing.py @@ -19,6 +19,7 @@ Component, ComponentCategory, ComponentData, + ComponentId, ComponentMetricId, EVChargerCableState, EVChargerComponentState, @@ -46,16 +47,24 @@ def mock_connection_manager(mocker: pytest_mock.MockFixture) -> mock.Mock: mock_client.components = mock.AsyncMock( name="components()", return_value=[ - Component(component_id=4, category=ComponentCategory.METER), - Component(component_id=6, category=ComponentCategory.INVERTER), - Component(component_id=9, category=ComponentCategory.BATTERY), - Component(component_id=12, category=ComponentCategory.EV_CHARGER), + Component(component_id=ComponentId(4), category=ComponentCategory.METER), + Component(component_id=ComponentId(6), category=ComponentCategory.INVERTER), + Component(component_id=ComponentId(9), category=ComponentCategory.BATTERY), + Component( + component_id=ComponentId(12), category=ComponentCategory.EV_CHARGER + ), ], ) - mock_client.meter_data = _new_meter_data_mock(4, starting_value=100.0) - mock_client.inverter_data = _new_inverter_data_mock(6, starting_value=0.0) - mock_client.battery_data = _new_battery_data_mock(9, starting_value=9.0) - mock_client.ev_charger_data = _new_ev_charger_data_mock(12, starting_value=-13.0) + mock_client.meter_data = _new_meter_data_mock(ComponentId(4), starting_value=100.0) + mock_client.inverter_data = _new_inverter_data_mock( + ComponentId(6), starting_value=0.0 + ) + mock_client.battery_data = _new_battery_data_mock( + ComponentId(9), starting_value=9.0 + ) + mock_client.ev_charger_data = _new_ev_charger_data_mock( + ComponentId(12), starting_value=-13.0 + ) mock_conn_manager = mock.MagicMock(name="connection_manager") mocker.patch( "frequenz.sdk.microgrid._data_sourcing" @@ -77,7 +86,7 @@ async def test_data_sourcing_actor( # pylint: disable=too-many-locals async with DataSourcingActor(req_chan.new_receiver(), registry): active_power_request_4 = ComponentMetricRequest( - "test-namespace", 4, ComponentMetricId.ACTIVE_POWER, None + "test-namespace", ComponentId(4), ComponentMetricId.ACTIVE_POWER, None ) active_power_recv_4 = registry.get_or_create( Sample[Quantity], active_power_request_4.get_channel_name() @@ -85,7 +94,7 @@ async def test_data_sourcing_actor( # pylint: disable=too-many-locals await req_sender.send(active_power_request_4) reactive_power_request_4 = ComponentMetricRequest( - "test-namespace", 4, ComponentMetricId.REACTIVE_POWER, None + "test-namespace", ComponentId(4), ComponentMetricId.REACTIVE_POWER, None ) reactive_power_recv_4 = registry.get_or_create( Sample[Quantity], reactive_power_request_4.get_channel_name() @@ -93,7 +102,7 @@ async def test_data_sourcing_actor( # pylint: disable=too-many-locals await req_sender.send(reactive_power_request_4) active_power_request_6 = ComponentMetricRequest( - "test-namespace", 6, ComponentMetricId.ACTIVE_POWER, None + "test-namespace", ComponentId(6), ComponentMetricId.ACTIVE_POWER, None ) active_power_recv_6 = registry.get_or_create( Sample[Quantity], active_power_request_6.get_channel_name() @@ -101,7 +110,7 @@ async def test_data_sourcing_actor( # pylint: disable=too-many-locals await req_sender.send(active_power_request_6) soc_request_9 = ComponentMetricRequest( - "test-namespace", 9, ComponentMetricId.SOC, None + "test-namespace", ComponentId(9), ComponentMetricId.SOC, None ) soc_recv_9 = registry.get_or_create( Sample[Quantity], soc_request_9.get_channel_name() @@ -109,7 +118,7 @@ async def test_data_sourcing_actor( # pylint: disable=too-many-locals await req_sender.send(soc_request_9) soc2_request_9 = ComponentMetricRequest( - "test-namespace", 9, ComponentMetricId.SOC, None + "test-namespace", ComponentId(9), ComponentMetricId.SOC, None ) soc2_recv_9 = registry.get_or_create( Sample[Quantity], soc2_request_9.get_channel_name() @@ -117,7 +126,7 @@ async def test_data_sourcing_actor( # pylint: disable=too-many-locals await req_sender.send(soc2_request_9) active_power_request_12 = ComponentMetricRequest( - "test-namespace", 12, ComponentMetricId.ACTIVE_POWER, None + "test-namespace", ComponentId(12), ComponentMetricId.ACTIVE_POWER, None ) active_power_recv_12 = registry.get_or_create( Sample[Quantity], active_power_request_12.get_channel_name() @@ -150,7 +159,9 @@ async def test_data_sourcing_actor( # pylint: disable=too-many-locals assert -13.0 + i == sample.value.base_value -def _new_meter_data(component_id: int, timestamp: datetime, value: float) -> MeterData: +def _new_meter_data( + component_id: ComponentId, timestamp: datetime, value: float +) -> MeterData: return MeterData( component_id=component_id, timestamp=timestamp, @@ -165,7 +176,7 @@ def _new_meter_data(component_id: int, timestamp: datetime, value: float) -> Met def _new_inverter_data( - component_id: int, timestamp: datetime, value: float + component_id: ComponentId, timestamp: datetime, value: float ) -> InverterData: return InverterData( component_id=component_id, @@ -187,7 +198,7 @@ def _new_inverter_data( def _new_battery_data( - component_id: int, timestamp: datetime, value: float + component_id: ComponentId, timestamp: datetime, value: float ) -> BatteryData: return BatteryData( component_id=component_id, @@ -208,7 +219,7 @@ def _new_battery_data( def _new_ev_charger_data( - component_id: int, timestamp: datetime, value: float + component_id: ComponentId, timestamp: datetime, value: float ) -> EVChargerData: return EVChargerData( component_id=component_id, @@ -231,8 +242,8 @@ def _new_ev_charger_data( def _new_streamer_mock( name: str, - constructor: Callable[[int, datetime, float], T], - component_id: int, + constructor: Callable[[ComponentId, datetime, float], T], + component_id: ComponentId, starting_value: float, ) -> mock.AsyncMock: """Get a mock streamer.""" @@ -247,7 +258,9 @@ async def generate_data(starting_value: float) -> AsyncIterator[T]: return mock.AsyncMock(name=name, return_value=generate_data(starting_value)) -def _new_meter_data_mock(component_id: int, starting_value: float) -> mock.AsyncMock: +def _new_meter_data_mock( + component_id: ComponentId, starting_value: float +) -> mock.AsyncMock: """Get a mock streamer for meter data.""" return _new_streamer_mock( f"meter_data_mock(id={component_id}, starting_value={starting_value})", @@ -257,7 +270,9 @@ def _new_meter_data_mock(component_id: int, starting_value: float) -> mock.Async ) -def _new_inverter_data_mock(component_id: int, starting_value: float) -> mock.AsyncMock: +def _new_inverter_data_mock( + component_id: ComponentId, starting_value: float +) -> mock.AsyncMock: """Get a mock streamer for inverter data.""" return _new_streamer_mock( f"inverter_data_mock(id={component_id}, starting_value={starting_value})", @@ -267,7 +282,9 @@ def _new_inverter_data_mock(component_id: int, starting_value: float) -> mock.As ) -def _new_battery_data_mock(component_id: int, starting_value: float) -> mock.AsyncMock: +def _new_battery_data_mock( + component_id: ComponentId, starting_value: float +) -> mock.AsyncMock: """Get a mock streamer for battery data.""" return _new_streamer_mock( f"battery_data_mock(id={component_id}, starting_value={starting_value})", @@ -278,7 +295,7 @@ def _new_battery_data_mock(component_id: int, starting_value: float) -> mock.Asy def _new_ev_charger_data_mock( - component_id: int, starting_value: float + component_id: ComponentId, starting_value: float ) -> mock.AsyncMock: """Get a mock streamer for EV charger data.""" return _new_streamer_mock( diff --git a/tests/microgrid/test_datapipeline.py b/tests/microgrid/test_datapipeline.py index 76a815192..b0ee32e2f 100644 --- a/tests/microgrid/test_datapipeline.py +++ b/tests/microgrid/test_datapipeline.py @@ -12,6 +12,7 @@ from frequenz.client.microgrid import ( Component, ComponentCategory, + ComponentId, Connection, InverterType, ) @@ -60,11 +61,14 @@ async def test_actors_started( mock_client = MockMicrogridClient( { - Component(1, ComponentCategory.GRID), - Component(4, ComponentCategory.INVERTER, InverterType.BATTERY), - Component(15, ComponentCategory.BATTERY), + Component(ComponentId(1), ComponentCategory.GRID), + Component(ComponentId(4), ComponentCategory.INVERTER, InverterType.BATTERY), + Component(ComponentId(15), ComponentCategory.BATTERY), + }, + connections={ + Connection(ComponentId(1), ComponentId(4)), + Connection(ComponentId(4), ComponentId(15)), }, - connections={Connection(1, 4), Connection(4, 15)}, ) mock_client.initialize(mocker) diff --git a/tests/microgrid/test_graph.py b/tests/microgrid/test_graph.py index fe865851c..7347e1e18 100644 --- a/tests/microgrid/test_graph.py +++ b/tests/microgrid/test_graph.py @@ -13,6 +13,7 @@ from frequenz.client.microgrid import ( Component, ComponentCategory, + ComponentId, ComponentMetadata, Connection, Fuse, @@ -48,10 +49,10 @@ def _add_connections( def _check_predecessors_and_successors(graph: gr.ComponentGraph) -> None: - expected_predecessors: dict[int, set[Component]] = {} - expected_successors: dict[int, set[Component]] = {} + expected_predecessors: dict[ComponentId, set[Component]] = {} + expected_successors: dict[ComponentId, set[Component]] = {} - components: dict[int, Component] = { + components: dict[ComponentId, Component] = { component.component_id: component for component in graph.components() } @@ -85,21 +86,21 @@ class TestComponentGraph: def sample_input_components(self) -> set[Component]: """Create a sample set of components for testing purposes.""" return { - Component(11, ComponentCategory.GRID), - Component(21, ComponentCategory.METER), - Component(41, ComponentCategory.METER), - Component(51, ComponentCategory.INVERTER), - Component(61, ComponentCategory.BATTERY), + Component(ComponentId(11), ComponentCategory.GRID), + Component(ComponentId(21), ComponentCategory.METER), + Component(ComponentId(41), ComponentCategory.METER), + Component(ComponentId(51), ComponentCategory.INVERTER), + Component(ComponentId(61), ComponentCategory.BATTERY), } @pytest.fixture() def sample_input_connections(self) -> set[Connection]: """Create a sample set of connections for testing purposes.""" return { - Connection(11, 21), - Connection(21, 41), - Connection(41, 51), - Connection(51, 61), + Connection(ComponentId(11), ComponentId(21)), + Connection(ComponentId(21), ComponentId(41)), + Connection(ComponentId(41), ComponentId(51)), + Connection(ComponentId(51), ComponentId(61)), } @pytest.fixture() @@ -124,58 +125,62 @@ def test_without_filters(self) -> None: assert graph.connections() == set() with pytest.raises( KeyError, - match="Component 1 not in graph, cannot get predecessors!", + match="Component with CID1 not in graph, cannot get predecessors!", ): - graph.predecessors(1) + graph.predecessors(ComponentId(1)) with pytest.raises( KeyError, - match="Component 1 not in graph, cannot get successors!", + match="Component with CID1 not in graph, cannot get successors!", ): - graph.successors(1) + graph.successors(ComponentId(1)) # simplest valid microgrid: a grid endpoint and a meter _graph_implementation.refresh_from( components={ - Component(1, ComponentCategory.GRID), - Component(3, ComponentCategory.METER), + Component(ComponentId(1), ComponentCategory.GRID), + Component(ComponentId(3), ComponentCategory.METER), }, - connections={Connection(1, 3)}, + connections={Connection(ComponentId(1), ComponentId(3))}, ) expected_components = { - Component(1, ComponentCategory.GRID), - Component(3, ComponentCategory.METER), + Component(ComponentId(1), ComponentCategory.GRID), + Component(ComponentId(3), ComponentCategory.METER), } assert len(graph.components()) == len(expected_components) assert graph.components() == expected_components - assert graph.connections() == {Connection(1, 3)} + assert graph.connections() == {Connection(ComponentId(1), ComponentId(3))} - assert graph.predecessors(1) == set() - assert graph.successors(1) == {Component(3, ComponentCategory.METER)} - assert graph.predecessors(3) == {Component(1, ComponentCategory.GRID)} - assert graph.successors(3) == set() + assert graph.predecessors(ComponentId(1)) == set() + assert graph.successors(ComponentId(1)) == { + Component(ComponentId(3), ComponentCategory.METER) + } + assert graph.predecessors(ComponentId(3)) == { + Component(ComponentId(1), ComponentCategory.GRID) + } + assert graph.successors(ComponentId(3)) == set() with pytest.raises( KeyError, - match="Component 2 not in graph, cannot get predecessors!", + match="Component with CID2 not in graph, cannot get predecessors!", ): - graph.predecessors(2) + graph.predecessors(ComponentId(2)) with pytest.raises( KeyError, - match="Component 2 not in graph, cannot get successors!", + match="Component with CID2 not in graph, cannot get successors!", ): - graph.successors(2) + graph.successors(ComponentId(2)) input_components = { - 101: Component(101, ComponentCategory.GRID), - 102: Component(102, ComponentCategory.METER), - 104: Component(104, ComponentCategory.METER), - 105: Component(105, ComponentCategory.INVERTER), - 106: Component(106, ComponentCategory.BATTERY), + ComponentId(101): Component(ComponentId(101), ComponentCategory.GRID), + ComponentId(102): Component(ComponentId(102), ComponentCategory.METER), + ComponentId(104): Component(ComponentId(104), ComponentCategory.METER), + ComponentId(105): Component(ComponentId(105), ComponentCategory.INVERTER), + ComponentId(106): Component(ComponentId(106), ComponentCategory.BATTERY), } input_connections = { - Connection(101, 102), - Connection(102, 104), - Connection(104, 105), - Connection(105, 106), + Connection(ComponentId(101), ComponentId(102)), + Connection(ComponentId(102), ComponentId(104)), + Connection(ComponentId(104), ComponentId(105)), + Connection(ComponentId(105), ComponentId(106)), } # more complex microgrid: grid endpoint, load, grid-side meter, @@ -193,46 +198,50 @@ def test_without_filters(self) -> None: with pytest.raises( KeyError, - match="Component 9 not in graph, cannot get predecessors!", + match="Component with CID9 not in graph, cannot get predecessors!", ): - graph.predecessors(9) + graph.predecessors(ComponentId(9)) with pytest.raises( KeyError, - match="Component 99 not in graph, cannot get successors!", + match="Component with CID99 not in graph, cannot get successors!", ): - graph.successors(99) + graph.successors(ComponentId(99)) @pytest.mark.parametrize( - "ids, expected", + "int_ids, expected", [ ({1}, set()), ({1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, set()), - ({11}, {Component(11, ComponentCategory.GRID)}), - ({21}, {Component(21, ComponentCategory.METER)}), - ({41}, {Component(41, ComponentCategory.METER)}), - ({51}, {Component(51, ComponentCategory.INVERTER)}), - ({61}, {Component(61, ComponentCategory.BATTERY)}), + ({11}, {Component(ComponentId(11), ComponentCategory.GRID)}), + ({21}, {Component(ComponentId(21), ComponentCategory.METER)}), + ({41}, {Component(ComponentId(41), ComponentCategory.METER)}), + ({51}, {Component(ComponentId(51), ComponentCategory.INVERTER)}), + ({61}, {Component(ComponentId(61), ComponentCategory.BATTERY)}), ( {11, 61}, { - Component(11, ComponentCategory.GRID), - Component(61, ComponentCategory.BATTERY), + Component(ComponentId(11), ComponentCategory.GRID), + Component(ComponentId(61), ComponentCategory.BATTERY), }, ), ( {9, 51, 41, 21, 101}, { - Component(41, ComponentCategory.METER), - Component(51, ComponentCategory.INVERTER), - Component(21, ComponentCategory.METER), + Component(ComponentId(41), ComponentCategory.METER), + Component(ComponentId(51), ComponentCategory.INVERTER), + Component(ComponentId(21), ComponentCategory.METER), }, ), ], ) def test_filter_graph_components_by_id( - self, sample_graph: gr.ComponentGraph, ids: set[int], expected: set[Component] + self, + sample_graph: gr.ComponentGraph, + int_ids: set[int], + expected: set[Component], ) -> None: """Test the graph component query with component ID filter.""" + ids = set(ComponentId(id) for id in int_ids) # with component_id filter specified, we get back only components whose ID # matches one of the specified values assert len(sample_graph.components(component_ids=ids)) == len(expected) @@ -244,23 +253,32 @@ def test_filter_graph_components_by_id( ({ComponentCategory.EV_CHARGER}, set()), ( {ComponentCategory.BATTERY, ComponentCategory.EV_CHARGER}, - {Component(61, ComponentCategory.BATTERY)}, + {Component(ComponentId(61), ComponentCategory.BATTERY)}, + ), + ( + {ComponentCategory.GRID}, + {Component(ComponentId(11), ComponentCategory.GRID)}, ), - ({ComponentCategory.GRID}, {Component(11, ComponentCategory.GRID)}), ( {ComponentCategory.METER}, { - Component(21, ComponentCategory.METER), - Component(41, ComponentCategory.METER), + Component(ComponentId(21), ComponentCategory.METER), + Component(ComponentId(41), ComponentCategory.METER), }, ), - ({ComponentCategory.INVERTER}, {Component(51, ComponentCategory.INVERTER)}), - ({ComponentCategory.BATTERY}, {Component(61, ComponentCategory.BATTERY)}), + ( + {ComponentCategory.INVERTER}, + {Component(ComponentId(51), ComponentCategory.INVERTER)}, + ), + ( + {ComponentCategory.BATTERY}, + {Component(ComponentId(61), ComponentCategory.BATTERY)}, + ), ( {ComponentCategory.GRID, ComponentCategory.BATTERY}, { - Component(11, ComponentCategory.GRID), - Component(61, ComponentCategory.BATTERY), + Component(ComponentId(11), ComponentCategory.GRID), + Component(ComponentId(61), ComponentCategory.BATTERY), }, ), ( @@ -270,9 +288,9 @@ def test_filter_graph_components_by_id( ComponentCategory.EV_CHARGER, }, { - Component(21, ComponentCategory.METER), - Component(61, ComponentCategory.BATTERY), - Component(41, ComponentCategory.METER), + Component(ComponentId(21), ComponentCategory.METER), + Component(ComponentId(61), ComponentCategory.BATTERY), + Component(ComponentId(41), ComponentCategory.METER), }, ), ], @@ -290,21 +308,25 @@ def test_filter_graph_components_by_type( assert sample_graph.components(component_categories=types) == expected @pytest.mark.parametrize( - "ids, types, expected", + "int_ids, types, expected", [ - ({11}, {ComponentCategory.GRID}, {Component(11, ComponentCategory.GRID)}), + ( + {11}, + {ComponentCategory.GRID}, + {Component(ComponentId(11), ComponentCategory.GRID)}, + ), ({31}, {ComponentCategory.GRID}, set()), ( {61}, {ComponentCategory.BATTERY}, - {Component(61, ComponentCategory.BATTERY)}, + {Component(ComponentId(61), ComponentCategory.BATTERY)}, ), ( {11, 21, 31, 61}, {ComponentCategory.METER, ComponentCategory.BATTERY}, { - Component(61, ComponentCategory.BATTERY), - Component(21, ComponentCategory.METER), + Component(ComponentId(61), ComponentCategory.BATTERY), + Component(ComponentId(21), ComponentCategory.METER), }, ), ], @@ -312,11 +334,12 @@ def test_filter_graph_components_by_type( def test_filter_graph_components_with_composite_filter( self, sample_graph: gr.ComponentGraph, - ids: set[int], + int_ids: set[int], types: set[ComponentCategory], expected: set[Component], ) -> None: """Test the graph component query with composite filter.""" + ids = set(ComponentId(id) for id in int_ids) # when both filters are applied, they are combined via AND logic, i.e. # the component must have one of the specified IDs and be of one of # the specified types @@ -340,120 +363,147 @@ def test_connection_filters(self) -> None: """Test the graph connection query with filters.""" _graph_implementation = gr._MicrogridComponentGraph( components={ - Component(1, ComponentCategory.GRID), - Component(2, ComponentCategory.METER), - Component(3, ComponentCategory.METER), - Component(4, ComponentCategory.EV_CHARGER), - Component(5, ComponentCategory.EV_CHARGER), - Component(6, ComponentCategory.EV_CHARGER), + Component(ComponentId(1), ComponentCategory.GRID), + Component(ComponentId(2), ComponentCategory.METER), + Component(ComponentId(3), ComponentCategory.METER), + Component(ComponentId(4), ComponentCategory.EV_CHARGER), + Component(ComponentId(5), ComponentCategory.EV_CHARGER), + Component(ComponentId(6), ComponentCategory.EV_CHARGER), }, connections={ - Connection(1, 2), - Connection(1, 3), - Connection(2, 4), - Connection(2, 5), - Connection(2, 6), + Connection(ComponentId(1), ComponentId(2)), + Connection(ComponentId(1), ComponentId(3)), + Connection(ComponentId(2), ComponentId(4)), + Connection(ComponentId(2), ComponentId(5)), + Connection(ComponentId(2), ComponentId(6)), }, ) graph: gr.ComponentGraph = _graph_implementation # without any filter applied, we get back all the connections in the graph assert graph.connections() == { - Connection(1, 2), - Connection(1, 3), - Connection(2, 4), - Connection(2, 5), - Connection(2, 6), + Connection(ComponentId(1), ComponentId(2)), + Connection(ComponentId(1), ComponentId(3)), + Connection(ComponentId(2), ComponentId(4)), + Connection(ComponentId(2), ComponentId(5)), + Connection(ComponentId(2), ComponentId(6)), } # with start filter applied, we get back only connections whose `start` # component matches one of the provided IDs - assert graph.connections(start={8}) == set() - assert graph.connections(start={7}) == set() - assert graph.connections(start={6}) == set() - assert graph.connections(start={5}) == set() - assert graph.connections(start={4}) == set() - assert graph.connections(start={3}) == set() - assert graph.connections(start={2}) == { - Connection(2, 4), - Connection(2, 5), - Connection(2, 6), + assert graph.connections(start={ComponentId(8)}) == set() + assert graph.connections(start={ComponentId(7)}) == set() + assert graph.connections(start={ComponentId(6)}) == set() + assert graph.connections(start={ComponentId(5)}) == set() + assert graph.connections(start={ComponentId(4)}) == set() + assert graph.connections(start={ComponentId(3)}) == set() + assert graph.connections(start={ComponentId(2)}) == { + Connection(ComponentId(2), ComponentId(4)), + Connection(ComponentId(2), ComponentId(5)), + Connection(ComponentId(2), ComponentId(6)), } - assert graph.connections(start={1}) == { - Connection(1, 2), - Connection(1, 3), + assert graph.connections(start={ComponentId(1)}) == { + Connection(ComponentId(1), ComponentId(2)), + Connection(ComponentId(1), ComponentId(3)), } - assert graph.connections(start={1, 3, 5}) == { - Connection(1, 2), - Connection(1, 3), + assert graph.connections( + start={ComponentId(1), ComponentId(3), ComponentId(5)} + ) == { + Connection(ComponentId(1), ComponentId(2)), + Connection(ComponentId(1), ComponentId(3)), } - assert graph.connections(start={1, 2, 5, 6}) == { - Connection(1, 2), - Connection(1, 3), - Connection(2, 4), - Connection(2, 5), - Connection(2, 6), + assert graph.connections( + start={ComponentId(1), ComponentId(2), ComponentId(5), ComponentId(6)} + ) == { + Connection(ComponentId(1), ComponentId(2)), + Connection(ComponentId(1), ComponentId(3)), + Connection(ComponentId(2), ComponentId(4)), + Connection(ComponentId(2), ComponentId(5)), + Connection(ComponentId(2), ComponentId(6)), } # with end filter applied, we get back only connections whose `end` # component matches one of the provided IDs - assert graph.connections(end={8}) == set() - assert graph.connections(end={6}) == {Connection(2, 6)} - assert graph.connections(end={5}) == {Connection(2, 5)} - assert graph.connections(end={4}) == {Connection(2, 4)} - assert graph.connections(end={3}) == {Connection(1, 3)} - assert graph.connections(end={2}) == {Connection(1, 2)} - assert graph.connections(end={1}) == set() - assert graph.connections(end={1, 2, 3}) == { - Connection(1, 2), - Connection(1, 3), + assert graph.connections(end={ComponentId(8)}) == set() + assert graph.connections(end={ComponentId(6)}) == { + Connection(ComponentId(2), ComponentId(6)) + } + assert graph.connections(end={ComponentId(5)}) == { + Connection(ComponentId(2), ComponentId(5)) + } + assert graph.connections(end={ComponentId(4)}) == { + Connection(ComponentId(2), ComponentId(4)) } - assert graph.connections(end={4, 5, 6}) == { - Connection(2, 4), - Connection(2, 5), - Connection(2, 6), + assert graph.connections(end={ComponentId(3)}) == { + Connection(ComponentId(1), ComponentId(3)) + } + assert graph.connections(end={ComponentId(2)}) == { + Connection(ComponentId(1), ComponentId(2)) + } + assert graph.connections(end={ComponentId(1)}) == set() + assert graph.connections( + end={ComponentId(1), ComponentId(2), ComponentId(3)} + ) == { + Connection(ComponentId(1), ComponentId(2)), + Connection(ComponentId(1), ComponentId(3)), + } + assert graph.connections( + end={ComponentId(4), ComponentId(5), ComponentId(6)} + ) == { + Connection(ComponentId(2), ComponentId(4)), + Connection(ComponentId(2), ComponentId(5)), + Connection(ComponentId(2), ComponentId(6)), } - assert graph.connections(end={2, 4, 6, 8}) == { - Connection(1, 2), - Connection(2, 4), - Connection(2, 6), + assert graph.connections( + end={ComponentId(2), ComponentId(4), ComponentId(6), ComponentId(8)} + ) == { + Connection(ComponentId(1), ComponentId(2)), + Connection(ComponentId(2), ComponentId(4)), + Connection(ComponentId(2), ComponentId(6)), } - assert graph.connections(end={1}) == set() + assert graph.connections(end={ComponentId(1)}) == set() # when both filters are applied, they are combined via AND logic, i.e. # a connection must have its `start` matching one of the provided start # values, and its `end` matching one of the provided end values - assert graph.connections(start={1}, end={2}) == {Connection(1, 2)} - assert graph.connections(start={2}, end={3}) == set() - assert graph.connections(start={1, 2}, end={3, 4}) == { - Connection(1, 3), - Connection(2, 4), + assert graph.connections(start={ComponentId(1)}, end={ComponentId(2)}) == { + Connection(ComponentId(1), ComponentId(2)) } - assert graph.connections(start={2, 3}, end={5, 6, 7}) == { - Connection(2, 5), - Connection(2, 6), + assert graph.connections(start={ComponentId(2)}, end={ComponentId(3)}) == set() + assert graph.connections( + start={ComponentId(1), ComponentId(2)}, end={ComponentId(3), ComponentId(4)} + ) == { + Connection(ComponentId(1), ComponentId(3)), + Connection(ComponentId(2), ComponentId(4)), + } + assert graph.connections( + start={ComponentId(2), ComponentId(3)}, + end={ComponentId(5), ComponentId(6), ComponentId(7)}, + ) == { + Connection(ComponentId(2), ComponentId(5)), + Connection(ComponentId(2), ComponentId(6)), } def test_dfs_search_two_grid_meters(self) -> None: """Test DFS searching PV components in a graph with two grid meters.""" - grid = Component(1, ComponentCategory.GRID) + grid = Component(ComponentId(1), ComponentCategory.GRID) pv_inverters = { - Component(4, ComponentCategory.INVERTER, InverterType.SOLAR), - Component(5, ComponentCategory.INVERTER, InverterType.SOLAR), + Component(ComponentId(4), ComponentCategory.INVERTER, InverterType.SOLAR), + Component(ComponentId(5), ComponentCategory.INVERTER, InverterType.SOLAR), } graph = gr._MicrogridComponentGraph( components={ grid, - Component(2, ComponentCategory.METER), - Component(3, ComponentCategory.METER), + Component(ComponentId(2), ComponentCategory.METER), + Component(ComponentId(3), ComponentCategory.METER), }.union(pv_inverters), connections={ - Connection(1, 2), - Connection(1, 3), - Connection(2, 4), - Connection(2, 5), + Connection(ComponentId(1), ComponentId(2)), + Connection(ComponentId(1), ComponentId(3)), + Connection(ComponentId(2), ComponentId(4)), + Connection(ComponentId(2), ComponentId(5)), }, ) @@ -462,25 +512,29 @@ def test_dfs_search_two_grid_meters(self) -> None: def test_dfs_search_grid_meter(self) -> None: """Test DFS searching PV components in a graph with a single grid meter.""" - grid = Component(1, ComponentCategory.GRID) + grid = Component(ComponentId(1), ComponentCategory.GRID) pv_meters = { - Component(3, ComponentCategory.METER), - Component(4, ComponentCategory.METER), + Component(ComponentId(3), ComponentCategory.METER), + Component(ComponentId(4), ComponentCategory.METER), } graph = gr._MicrogridComponentGraph( components={ grid, - Component(2, ComponentCategory.METER), - Component(5, ComponentCategory.INVERTER, InverterType.SOLAR), - Component(6, ComponentCategory.INVERTER, InverterType.SOLAR), + Component(ComponentId(2), ComponentCategory.METER), + Component( + ComponentId(5), ComponentCategory.INVERTER, InverterType.SOLAR + ), + Component( + ComponentId(6), ComponentCategory.INVERTER, InverterType.SOLAR + ), }.union(pv_meters), connections={ - Connection(1, 2), - Connection(2, 3), - Connection(2, 4), - Connection(3, 5), - Connection(4, 6), + Connection(ComponentId(1), ComponentId(2)), + Connection(ComponentId(2), ComponentId(3)), + Connection(ComponentId(2), ComponentId(4)), + Connection(ComponentId(3), ComponentId(5)), + Connection(ComponentId(4), ComponentId(6)), }, ) @@ -489,21 +543,21 @@ def test_dfs_search_grid_meter(self) -> None: def test_dfs_search_grid_meter_no_pv_meter(self) -> None: """Test DFS searching PV components in a graph with a single grid meter.""" - grid = Component(1, ComponentCategory.GRID) + grid = Component(ComponentId(1), ComponentCategory.GRID) pv_inverters = { - Component(3, ComponentCategory.INVERTER, InverterType.SOLAR), - Component(4, ComponentCategory.INVERTER, InverterType.SOLAR), + Component(ComponentId(3), ComponentCategory.INVERTER, InverterType.SOLAR), + Component(ComponentId(4), ComponentCategory.INVERTER, InverterType.SOLAR), } graph = gr._MicrogridComponentGraph( components={ grid, - Component(2, ComponentCategory.METER), + Component(ComponentId(2), ComponentCategory.METER), }.union(pv_inverters), connections={ - Connection(1, 2), - Connection(2, 3), - Connection(2, 4), + Connection(ComponentId(1), ComponentId(2)), + Connection(ComponentId(2), ComponentId(3)), + Connection(ComponentId(2), ComponentId(4)), }, ) @@ -512,25 +566,29 @@ def test_dfs_search_grid_meter_no_pv_meter(self) -> None: def test_dfs_search_no_grid_meter(self) -> None: """Test DFS searching PV components in a graph with no grid meter.""" - grid = Component(1, ComponentCategory.GRID) + grid = Component(ComponentId(1), ComponentCategory.GRID) pv_meters = { - Component(3, ComponentCategory.METER), - Component(4, ComponentCategory.METER), + Component(ComponentId(3), ComponentCategory.METER), + Component(ComponentId(4), ComponentCategory.METER), } graph = gr._MicrogridComponentGraph( components={ grid, - Component(2, ComponentCategory.METER), - Component(5, ComponentCategory.INVERTER, InverterType.SOLAR), - Component(6, ComponentCategory.INVERTER, InverterType.SOLAR), + Component(ComponentId(2), ComponentCategory.METER), + Component( + ComponentId(5), ComponentCategory.INVERTER, InverterType.SOLAR + ), + Component( + ComponentId(6), ComponentCategory.INVERTER, InverterType.SOLAR + ), }.union(pv_meters), connections={ - Connection(1, 2), - Connection(1, 3), - Connection(1, 4), - Connection(3, 5), - Connection(4, 6), + Connection(ComponentId(1), ComponentId(2)), + Connection(ComponentId(1), ComponentId(3)), + Connection(ComponentId(1), ComponentId(4)), + Connection(ComponentId(3), ComponentId(5)), + Connection(ComponentId(4), ComponentId(6)), }, ) @@ -539,29 +597,33 @@ def test_dfs_search_no_grid_meter(self) -> None: def test_dfs_search_nested_components(self) -> None: """Test DFS searching PV components in a graph with nested components.""" - grid = Component(1, ComponentCategory.GRID) + grid = Component(ComponentId(1), ComponentCategory.GRID) battery_components = { - Component(4, ComponentCategory.METER), - Component(5, ComponentCategory.METER), - Component(6, ComponentCategory.INVERTER, InverterType.BATTERY), + Component(ComponentId(4), ComponentCategory.METER), + Component(ComponentId(5), ComponentCategory.METER), + Component(ComponentId(6), ComponentCategory.INVERTER, InverterType.BATTERY), } graph = gr._MicrogridComponentGraph( components={ grid, - Component(2, ComponentCategory.METER), - Component(3, ComponentCategory.METER), - Component(7, ComponentCategory.INVERTER, InverterType.BATTERY), - Component(8, ComponentCategory.INVERTER, InverterType.BATTERY), + Component(ComponentId(2), ComponentCategory.METER), + Component(ComponentId(3), ComponentCategory.METER), + Component( + ComponentId(7), ComponentCategory.INVERTER, InverterType.BATTERY + ), + Component( + ComponentId(8), ComponentCategory.INVERTER, InverterType.BATTERY + ), }.union(battery_components), connections={ - Connection(1, 2), - Connection(2, 3), - Connection(2, 6), - Connection(3, 4), - Connection(3, 5), - Connection(4, 7), - Connection(5, 8), + Connection(ComponentId(1), ComponentId(2)), + Connection(ComponentId(2), ComponentId(3)), + Connection(ComponentId(2), ComponentId(6)), + Connection(ComponentId(3), ComponentId(4)), + Connection(ComponentId(3), ComponentId(5)), + Connection(ComponentId(4), ComponentId(7)), + Connection(ComponentId(5), ComponentId(8)), }, ) @@ -572,19 +634,23 @@ def test_find_first_descendant_component(self) -> None: """Test scenarios for finding the first descendant component.""" graph = gr._MicrogridComponentGraph( components={ - Component(1, ComponentCategory.GRID), - Component(2, ComponentCategory.METER), - Component(3, ComponentCategory.METER), - Component(4, ComponentCategory.INVERTER, InverterType.BATTERY), - Component(5, ComponentCategory.INVERTER, InverterType.SOLAR), - Component(6, ComponentCategory.EV_CHARGER), + Component(ComponentId(1), ComponentCategory.GRID), + Component(ComponentId(2), ComponentCategory.METER), + Component(ComponentId(3), ComponentCategory.METER), + Component( + ComponentId(4), ComponentCategory.INVERTER, InverterType.BATTERY + ), + Component( + ComponentId(5), ComponentCategory.INVERTER, InverterType.SOLAR + ), + Component(ComponentId(6), ComponentCategory.EV_CHARGER), }, connections={ - Connection(1, 2), - Connection(2, 3), - Connection(2, 4), - Connection(2, 5), - Connection(3, 6), + Connection(ComponentId(1), ComponentId(2)), + Connection(ComponentId(2), ComponentId(3)), + Connection(ComponentId(2), ComponentId(4)), + Connection(ComponentId(2), ComponentId(5)), + Connection(ComponentId(3), ComponentId(6)), }, ) @@ -592,14 +658,14 @@ def test_find_first_descendant_component(self) -> None: result = graph.find_first_descendant_component( descendant_categories=(ComponentCategory.METER,), ) - assert result == Component(2, ComponentCategory.METER) + assert result == Component(ComponentId(2), ComponentCategory.METER) # Find the first descendant component of the grid, # considering meter or inverter categories. result = graph.find_first_descendant_component( descendant_categories=(ComponentCategory.METER, ComponentCategory.INVERTER), ) - assert result == Component(2, ComponentCategory.METER) + assert result == Component(ComponentId(2), ComponentCategory.METER) # Verify behavior when component is not found in immediate descendant # categories for the first meter. @@ -643,11 +709,13 @@ def test___init__(self) -> None: # other with pytest.raises(gr.InvalidGraphError): gr._MicrogridComponentGraph( - components={Component(1, ComponentCategory.GRID)} + components={Component(ComponentId(1), ComponentCategory.GRID)} ) with pytest.raises(gr.InvalidGraphError): - gr._MicrogridComponentGraph(connections={Connection(1, 2)}) + gr._MicrogridComponentGraph( + connections={Connection(ComponentId(1), ComponentId(2))} + ) # if both are provided, the graph data must itself # be valid (we give just a couple of cases of each @@ -659,49 +727,60 @@ def test___init__(self) -> None: # connected to a meter grid_and_meter = gr._MicrogridComponentGraph( components={ - Component(1, ComponentCategory.GRID), - Component(2, ComponentCategory.METER), + Component(ComponentId(1), ComponentCategory.GRID), + Component(ComponentId(2), ComponentCategory.METER), }, - connections={Connection(1, 2)}, + connections={Connection(ComponentId(1), ComponentId(2))}, ) expected = { - Component(1, ComponentCategory.GRID), - Component(2, ComponentCategory.METER), + Component(ComponentId(1), ComponentCategory.GRID), + Component(ComponentId(2), ComponentCategory.METER), } assert len(grid_and_meter.components()) == len(expected) assert set(grid_and_meter.components()) == expected - assert list(grid_and_meter.connections()) == [Connection(1, 2)] + assert list(grid_and_meter.connections()) == [ + Connection(ComponentId(1), ComponentId(2)) + ] grid_and_meter.validate() # invalid graph data: unknown component category with pytest.raises(gr.InvalidGraphError): gr._MicrogridComponentGraph( components={ - Component(1, ComponentCategory.GRID), - Component(2, ComponentCategory.METER), - Component(3, 666), # type: ignore + Component(ComponentId(1), ComponentCategory.GRID), + Component(ComponentId(2), ComponentCategory.METER), + Component(ComponentId(3), 666), # type: ignore + }, + connections={ + Connection(ComponentId(1), ComponentId(2)), + Connection(ComponentId(1), ComponentId(3)), }, - connections={Connection(1, 2), Connection(1, 3)}, ) # invalid graph data: a connection between components that do not exist with pytest.raises(gr.InvalidGraphError): gr._MicrogridComponentGraph( components={ - Component(1, ComponentCategory.GRID), - Component(2, ComponentCategory.METER), + Component(ComponentId(1), ComponentCategory.GRID), + Component(ComponentId(2), ComponentCategory.METER), + }, + connections={ + Connection(ComponentId(1), ComponentId(2)), + Connection(ComponentId(1), ComponentId(3)), }, - connections={Connection(1, 2), Connection(1, 3)}, ) # invalid graph data: one of the connections is not valid with pytest.raises(gr.InvalidGraphError): gr._MicrogridComponentGraph( components={ - Component(1, ComponentCategory.GRID), - Component(2, ComponentCategory.METER), + Component(ComponentId(1), ComponentCategory.GRID), + Component(ComponentId(2), ComponentCategory.METER), + }, + connections={ + Connection(ComponentId(1), ComponentId(2)), + Connection(ComponentId(2), ComponentId(2)), }, - connections={Connection(1, 2), Connection(2, 2)}, ) def test_refresh_from(self) -> None: @@ -721,14 +800,16 @@ def test_refresh_from(self) -> None: graph.validate() with pytest.raises(gr.InvalidGraphError): - graph.refresh_from(set(), {Connection(1, 2)}) + graph.refresh_from(set(), {Connection(ComponentId(1), ComponentId(2))}) assert set(graph.components()) == set() assert list(graph.connections()) == [] with pytest.raises(gr.InvalidGraphError): graph.validate() with pytest.raises(gr.InvalidGraphError): - graph.refresh_from({Component(1, ComponentCategory.GRID)}, set()) + graph.refresh_from( + {Component(ComponentId(1), ComponentCategory.GRID)}, set() + ) assert set(graph.components()) == set() assert list(graph.connections()) == [] with pytest.raises(gr.InvalidGraphError): @@ -740,11 +821,11 @@ def test_refresh_from(self) -> None: with pytest.raises(gr.InvalidGraphError): graph.refresh_from( components={ - Component(0, ComponentCategory.GRID), - Component(1, ComponentCategory.METER), - Component(2, ComponentCategory.METER), + Component(ComponentId(0), ComponentCategory.GRID), + Component(ComponentId(1), ComponentCategory.METER), + Component(ComponentId(2), ComponentCategory.METER), }, - connections={Connection(1, 2)}, + connections={Connection(ComponentId(1), ComponentId(2))}, ) assert set(graph.components()) == set() assert list(graph.connections()) == [] @@ -755,11 +836,14 @@ def test_refresh_from(self) -> None: with pytest.raises(gr.InvalidGraphError): graph.refresh_from( components={ - Component(1, ComponentCategory.GRID), - Component(2, ComponentCategory.METER), - Component(3, ComponentCategory.METER), + Component(ComponentId(1), ComponentCategory.GRID), + Component(ComponentId(2), ComponentCategory.METER), + Component(ComponentId(3), ComponentCategory.METER), + }, + connections={ + Connection(ComponentId(1), ComponentId(1)), + Connection(ComponentId(2), ComponentId(3)), }, - connections={Connection(1, 1), Connection(2, 3)}, ) assert set(graph.components()) == set() assert list(graph.connections()) == [] @@ -769,33 +853,33 @@ def test_refresh_from(self) -> None: # valid graph with both load and battery setup graph.refresh_from( components={ - Component(1, ComponentCategory.GRID), - Component(2, ComponentCategory.METER), - Component(4, ComponentCategory.METER), - Component(5, ComponentCategory.INVERTER), - Component(6, ComponentCategory.BATTERY), + Component(ComponentId(1), ComponentCategory.GRID), + Component(ComponentId(2), ComponentCategory.METER), + Component(ComponentId(4), ComponentCategory.METER), + Component(ComponentId(5), ComponentCategory.INVERTER), + Component(ComponentId(6), ComponentCategory.BATTERY), }, connections={ - Connection(1, 2), - Connection(2, 4), - Connection(4, 5), - Connection(5, 6), + Connection(ComponentId(1), ComponentId(2)), + Connection(ComponentId(2), ComponentId(4)), + Connection(ComponentId(4), ComponentId(5)), + Connection(ComponentId(5), ComponentId(6)), }, ) expected = { - Component(1, ComponentCategory.GRID), - Component(2, ComponentCategory.METER), - Component(4, ComponentCategory.METER), - Component(5, ComponentCategory.INVERTER), - Component(6, ComponentCategory.BATTERY), + Component(ComponentId(1), ComponentCategory.GRID), + Component(ComponentId(2), ComponentCategory.METER), + Component(ComponentId(4), ComponentCategory.METER), + Component(ComponentId(5), ComponentCategory.INVERTER), + Component(ComponentId(6), ComponentCategory.BATTERY), } assert len(graph.components()) == len(expected) assert set(graph.components()) == expected assert graph.connections() == { - Connection(1, 2), - Connection(2, 4), - Connection(4, 5), - Connection(5, 6), + Connection(ComponentId(1), ComponentId(2)), + Connection(ComponentId(2), ComponentId(4)), + Connection(ComponentId(4), ComponentId(5)), + Connection(ComponentId(5), ComponentId(6)), } graph.validate() @@ -805,14 +889,14 @@ def test_refresh_from(self) -> None: with pytest.raises(gr.InvalidGraphError): graph.refresh_from( components={ - Component(7, ComponentCategory.GRID), - Component(8, ComponentCategory.METER), - Component(9, ComponentCategory.INVERTER), + Component(ComponentId(7), ComponentCategory.GRID), + Component(ComponentId(8), ComponentCategory.METER), + Component(ComponentId(9), ComponentCategory.INVERTER), }, connections={ - Connection(7, 8), - Connection(8, 9), - Connection(9, 8), + Connection(ComponentId(7), ComponentId(8)), + Connection(ComponentId(8), ComponentId(9)), + Connection(ComponentId(9), ComponentId(8)), }, ) @@ -820,10 +904,10 @@ def test_refresh_from(self) -> None: assert graph.components() == expected assert graph.connections() == { - Connection(1, 2), - Connection(2, 4), - Connection(4, 5), - Connection(5, 6), + Connection(ComponentId(1), ComponentId(2)), + Connection(ComponentId(2), ComponentId(4)), + Connection(ComponentId(4), ComponentId(5)), + Connection(ComponentId(5), ComponentId(6)), } graph.validate() @@ -838,10 +922,10 @@ def pretend_to_correct_errors(_g: gr._MicrogridComponentGraph) -> None: with pytest.raises(gr.InvalidGraphError): graph.refresh_from( components={ - Component(7, ComponentCategory.GRID), - Component(9, ComponentCategory.METER), + Component(ComponentId(7), ComponentCategory.GRID), + Component(ComponentId(9), ComponentCategory.METER), }, - connections={Connection(9, 7)}, + connections={Connection(ComponentId(9), ComponentId(7))}, correct_errors=pretend_to_correct_errors, ) @@ -851,19 +935,19 @@ def pretend_to_correct_errors(_g: gr._MicrogridComponentGraph) -> None: # contents will be overwritten graph.refresh_from( components={ - Component(10, ComponentCategory.GRID), - Component(11, ComponentCategory.METER), + Component(ComponentId(10), ComponentCategory.GRID), + Component(ComponentId(11), ComponentCategory.METER), }, - connections={Connection(10, 11)}, + connections={Connection(ComponentId(10), ComponentId(11))}, ) expected = { - Component(10, ComponentCategory.GRID), - Component(11, ComponentCategory.METER), + Component(ComponentId(10), ComponentCategory.GRID), + Component(ComponentId(11), ComponentCategory.METER), } assert len(graph.components()) == len(expected) assert set(graph.components()) == expected - assert graph.connections() == {Connection(10, 11)} + assert graph.connections() == {Connection(ComponentId(10), ComponentId(11))} graph.validate() async def test_refresh_from_api(self) -> None: @@ -888,7 +972,9 @@ async def test_refresh_from_api(self) -> None: with pytest.raises(gr.InvalidGraphError): graph.validate() - client.components.return_value = [Component(1, ComponentCategory.GRID)] + client.components.return_value = [ + Component(ComponentId(1), ComponentCategory.GRID) + ] with pytest.raises(gr.InvalidGraphError): await graph.refresh_from_api(client) assert graph.components() == set() @@ -897,7 +983,7 @@ async def test_refresh_from_api(self) -> None: graph.validate() client.components.return_value = [] - client.connections.return_value = [Connection(1, 2)] + client.connections.return_value = [Connection(ComponentId(1), ComponentId(2))] with pytest.raises(gr.InvalidGraphError): await graph.refresh_from_api(client) assert graph.components() == set() @@ -910,16 +996,16 @@ async def test_refresh_from_api(self) -> None: # valid graph with meter, and EV charger client.components.return_value = [ Component( - 101, + ComponentId(101), ComponentCategory.GRID, metadata=ComponentMetadata(fuse=Fuse(max_current=0.0)), ), - Component(111, ComponentCategory.METER), - Component(131, ComponentCategory.EV_CHARGER), + Component(ComponentId(111), ComponentCategory.METER), + Component(ComponentId(131), ComponentCategory.EV_CHARGER), ] client.connections.return_value = [ - Connection(101, 111), - Connection(111, 131), + Connection(ComponentId(101), ComponentId(111)), + Connection(ComponentId(111), ComponentId(131)), ] await graph.refresh_from_api(client) @@ -928,19 +1014,19 @@ async def test_refresh_from_api(self) -> None: # two graphs. expected = { Component( - 101, + ComponentId(101), ComponentCategory.GRID, None, ComponentMetadata(fuse=Fuse(max_current=0.0)), ), - Component(111, ComponentCategory.METER), - Component(131, ComponentCategory.EV_CHARGER), + Component(ComponentId(111), ComponentCategory.METER), + Component(ComponentId(131), ComponentCategory.EV_CHARGER), } assert len(graph.components()) == len(expected) assert graph.components() == expected assert graph.connections() == { - Connection(101, 111), - Connection(111, 131), + Connection(ComponentId(101), ComponentId(111)), + Connection(ComponentId(111), ComponentId(131)), } graph.validate() @@ -948,43 +1034,45 @@ async def test_refresh_from_api(self) -> None: # contents will be overwritten client.components.return_value = [ Component( - 707, + ComponentId(707), ComponentCategory.GRID, metadata=ComponentMetadata(fuse=Fuse(max_current=0.0)), ), - Component(717, ComponentCategory.METER), - Component(727, ComponentCategory.INVERTER, type=InverterType.NONE), - Component(737, ComponentCategory.BATTERY), - Component(747, ComponentCategory.METER), + Component(ComponentId(717), ComponentCategory.METER), + Component( + ComponentId(727), ComponentCategory.INVERTER, type=InverterType.NONE + ), + Component(ComponentId(737), ComponentCategory.BATTERY), + Component(ComponentId(747), ComponentCategory.METER), ] client.connections.return_value = [ - Connection(707, 717), - Connection(717, 727), - Connection(727, 737), - Connection(717, 747), + Connection(ComponentId(707), ComponentId(717)), + Connection(ComponentId(717), ComponentId(727)), + Connection(ComponentId(727), ComponentId(737)), + Connection(ComponentId(717), ComponentId(747)), ] await graph.refresh_from_api(client) expected = { Component( - 707, + ComponentId(707), ComponentCategory.GRID, None, ComponentMetadata(fuse=Fuse(max_current=0.0)), ), - Component(717, ComponentCategory.METER), - Component(727, ComponentCategory.INVERTER, InverterType.NONE), - Component(737, ComponentCategory.BATTERY), - Component(747, ComponentCategory.METER), + Component(ComponentId(717), ComponentCategory.METER), + Component(ComponentId(727), ComponentCategory.INVERTER, InverterType.NONE), + Component(ComponentId(737), ComponentCategory.BATTERY), + Component(ComponentId(747), ComponentCategory.METER), } assert len(graph.components()) == len(expected) assert graph.components() == expected assert graph.connections() == { - Connection(707, 717), - Connection(717, 727), - Connection(717, 747), - Connection(727, 737), + Connection(ComponentId(707), ComponentId(717)), + Connection(ComponentId(717), ComponentId(727)), + Connection(ComponentId(717), ComponentId(747)), + Connection(ComponentId(727), ComponentId(737)), } graph.validate() @@ -1017,11 +1105,15 @@ def test_validate(self) -> None: graph._graph.clear() _add_components( graph, - Component(1, ComponentCategory.GRID), - Component(2, ComponentCategory.NONE), - Component(3, ComponentCategory.METER), + Component(ComponentId(1), ComponentCategory.GRID), + Component(ComponentId(2), ComponentCategory.NONE), + Component(ComponentId(3), ComponentCategory.METER), + ) + _add_connections( + graph, + Connection(ComponentId(1), ComponentId(3)), + Connection(ComponentId(2), ComponentId(3)), ) - _add_connections(graph, Connection(1, 3), Connection(2, 3)) with pytest.raises(gr.InvalidGraphError, match="Multiple potential root nodes"): graph.validate() @@ -1029,11 +1121,15 @@ def test_validate(self) -> None: graph._graph.clear() _add_components( graph, - Component(1, ComponentCategory.GRID), - Component(2, ComponentCategory.GRID), - Component(3, ComponentCategory.METER), + Component(ComponentId(1), ComponentCategory.GRID), + Component(ComponentId(2), ComponentCategory.GRID), + Component(ComponentId(3), ComponentCategory.METER), + ) + _add_connections( + graph, + Connection(ComponentId(1), ComponentId(2)), + Connection(ComponentId(2), ComponentId(3)), ) - _add_connections(graph, Connection(1, 2), Connection(2, 3)) with pytest.raises( gr.InvalidGraphError, match="Multiple grid endpoints in component graph" ): @@ -1044,11 +1140,15 @@ def test_validate(self) -> None: graph._graph.clear() _add_components( graph, - Component(1, ComponentCategory.GRID), - Component(2, ComponentCategory.BATTERY), - Component(3, ComponentCategory.METER), + Component(ComponentId(1), ComponentCategory.GRID), + Component(ComponentId(2), ComponentCategory.BATTERY), + Component(ComponentId(3), ComponentCategory.METER), + ) + _add_connections( + graph, + Connection(ComponentId(1), ComponentId(2)), + Connection(ComponentId(2), ComponentId(3)), ) - _add_connections(graph, Connection(1, 2), Connection(2, 3)) with pytest.raises( gr.InvalidGraphError, match="Leaf components with graph successors" ): @@ -1069,7 +1169,7 @@ def test__validate_graph(self) -> None: # graph has no connections graph._graph.clear() - _add_components(graph, Component(1, ComponentCategory.GRID)) + _add_components(graph, Component(ComponentId(1), ComponentCategory.GRID)) with pytest.raises( gr.InvalidGraphError, match="No connections in component graph!" ): @@ -1079,11 +1179,16 @@ def test__validate_graph(self) -> None: graph._graph.clear() _add_components( graph, - Component(1, ComponentCategory.GRID), - Component(2, ComponentCategory.INVERTER), - Component(3, ComponentCategory.METER), + Component(ComponentId(1), ComponentCategory.GRID), + Component(ComponentId(2), ComponentCategory.INVERTER), + Component(ComponentId(3), ComponentCategory.METER), + ) + _add_connections( + graph, + Connection(ComponentId(1), ComponentId(2)), + Connection(ComponentId(2), ComponentId(3)), + Connection(ComponentId(3), ComponentId(2)), ) - _add_connections(graph, Connection(1, 2), Connection(2, 3), Connection(3, 2)) with pytest.raises( gr.InvalidGraphError, match="Component graph is not a tree!" ): @@ -1093,11 +1198,11 @@ def test__validate_graph(self) -> None: # (this violates the tree property): _add_components( graph, - Component(1, ComponentCategory.GRID), - Component(2, ComponentCategory.METER), - Component(3, ComponentCategory.NONE), + Component(ComponentId(1), ComponentCategory.GRID), + Component(ComponentId(2), ComponentCategory.METER), + Component(ComponentId(3), ComponentCategory.NONE), ) - _add_connections(graph, Connection(1, 2)) + _add_connections(graph, Connection(ComponentId(1), ComponentId(2))) with pytest.raises( gr.InvalidGraphError, match="Component graph is not a tree!" ): @@ -1118,11 +1223,16 @@ def test__validate_graph_root(self) -> None: graph._graph.clear() _add_components( graph, - Component(1, ComponentCategory.METER), - Component(2, ComponentCategory.METER), - Component(3, ComponentCategory.METER), + Component(ComponentId(1), ComponentCategory.METER), + Component(ComponentId(2), ComponentCategory.METER), + Component(ComponentId(3), ComponentCategory.METER), + ) + _add_connections( + graph, + Connection(ComponentId(1), ComponentId(2)), + Connection(ComponentId(2), ComponentId(3)), + Connection(ComponentId(3), ComponentId(1)), ) - _add_connections(graph, Connection(1, 2), Connection(2, 3), Connection(3, 1)) with pytest.raises( gr.InvalidGraphError, match="No valid root nodes of component graph!" ): @@ -1133,11 +1243,15 @@ def test__validate_graph_root(self) -> None: graph._graph.clear() _add_components( graph, - Component(1, ComponentCategory.METER), - Component(2, ComponentCategory.INVERTER), - Component(3, ComponentCategory.BATTERY), + Component(ComponentId(1), ComponentCategory.METER), + Component(ComponentId(2), ComponentCategory.INVERTER), + Component(ComponentId(3), ComponentCategory.BATTERY), + ) + _add_connections( + graph, + Connection(ComponentId(1), ComponentId(2)), + Connection(ComponentId(2), ComponentId(3)), ) - _add_connections(graph, Connection(1, 2), Connection(2, 3)) with pytest.raises( gr.InvalidGraphError, match="No valid root nodes of component graph!" ): @@ -1148,44 +1262,55 @@ def test__validate_graph_root(self) -> None: graph._graph.clear() _add_components( graph, - Component(1, ComponentCategory.NONE), - Component(2, ComponentCategory.GRID), - Component(3, ComponentCategory.METER), + Component(ComponentId(1), ComponentCategory.NONE), + Component(ComponentId(2), ComponentCategory.GRID), + Component(ComponentId(3), ComponentCategory.METER), + ) + _add_connections( + graph, + Connection(ComponentId(1), ComponentId(3)), + Connection(ComponentId(2), ComponentId(3)), ) - _add_connections(graph, Connection(1, 3), Connection(2, 3)) with pytest.raises(gr.InvalidGraphError, match="Multiple potential root nodes"): graph._validate_graph_root() graph._graph.clear() _add_components( graph, - Component(1, ComponentCategory.GRID), - Component(2, ComponentCategory.GRID), - Component(3, ComponentCategory.METER), + Component(ComponentId(1), ComponentCategory.GRID), + Component(ComponentId(2), ComponentCategory.GRID), + Component(ComponentId(3), ComponentCategory.METER), + ) + _add_connections( + graph, + Connection(ComponentId(1), ComponentId(3)), + Connection(ComponentId(2), ComponentId(3)), ) - _add_connections(graph, Connection(1, 3), Connection(2, 3)) with pytest.raises(gr.InvalidGraphError, match="Multiple potential root nodes"): graph._validate_graph_root() # there is just one potential root node but it has no successors graph._graph.clear() - _add_components(graph, Component(1, ComponentCategory.NONE)) + _add_components(graph, Component(ComponentId(1), ComponentCategory.NONE)) with pytest.raises( - gr.InvalidGraphError, match="Graph root .*id=1.* has no successors!" + gr.InvalidGraphError, + match=r"Graph root .*component_id=ComponentId\(1\).* has no successors!", ): graph._validate_graph_root() graph._graph.clear() - _add_components(graph, Component(2, ComponentCategory.GRID)) + _add_components(graph, Component(ComponentId(2), ComponentCategory.GRID)) with pytest.raises( - gr.InvalidGraphError, match="Graph root .*id=2.* has no successors!" + gr.InvalidGraphError, + match=r"Graph root .*component_id=ComponentId\(2\).* has no successors!", ): graph._validate_graph_root() graph._graph.clear() - _add_components(graph, Component(3, ComponentCategory.GRID)) + _add_components(graph, Component(ComponentId(3), ComponentCategory.GRID)) with pytest.raises( - gr.InvalidGraphError, match="Graph root .*id=3.* has no successors!" + gr.InvalidGraphError, + match=r"Graph root .*component_id=ComponentId\(3\).* has no successors!", ): graph._validate_graph_root() @@ -1193,28 +1318,28 @@ def test__validate_graph_root(self) -> None: graph._graph.clear() _add_components( graph, - Component(1, ComponentCategory.NONE), - Component(2, ComponentCategory.METER), + Component(ComponentId(1), ComponentCategory.NONE), + Component(ComponentId(2), ComponentCategory.METER), ) - _add_connections(graph, Connection(1, 2)) + _add_connections(graph, Connection(ComponentId(1), ComponentId(2))) graph._validate_graph_root() graph._graph.clear() _add_components( graph, - Component(1, ComponentCategory.GRID), - Component(2, ComponentCategory.METER), + Component(ComponentId(1), ComponentCategory.GRID), + Component(ComponentId(2), ComponentCategory.METER), ) - _add_connections(graph, Connection(1, 2)) + _add_connections(graph, Connection(ComponentId(1), ComponentId(2))) graph._validate_graph_root() graph._graph.clear() _add_components( graph, - Component(1, ComponentCategory.GRID), - Component(2, ComponentCategory.METER), + Component(ComponentId(1), ComponentCategory.GRID), + Component(ComponentId(2), ComponentCategory.METER), ) - _add_connections(graph, Connection(1, 2)) + _add_connections(graph, Connection(ComponentId(1), ComponentId(2))) graph._validate_graph_root() def test__validate_grid_endpoint(self) -> None: @@ -1229,18 +1354,22 @@ def test__validate_grid_endpoint(self) -> None: # missing grid endpoint is OK as the graph might have # another kind of root graph._graph.clear() - _add_components(graph, Component(2, ComponentCategory.METER)) + _add_components(graph, Component(ComponentId(2), ComponentCategory.METER)) graph._validate_grid_endpoint() # multiple grid endpoints graph._graph.clear() _add_components( graph, - Component(1, ComponentCategory.GRID), - Component(2, ComponentCategory.METER), - Component(3, ComponentCategory.GRID), + Component(ComponentId(1), ComponentCategory.GRID), + Component(ComponentId(2), ComponentCategory.METER), + Component(ComponentId(3), ComponentCategory.GRID), + ) + _add_connections( + graph, + Connection(ComponentId(1), ComponentId(2)), + Connection(ComponentId(3), ComponentId(2)), ) - _add_connections(graph, Connection(1, 2), Connection(3, 2)) with pytest.raises( gr.InvalidGraphError, match="Multiple grid endpoints in component graph", @@ -1251,24 +1380,24 @@ def test__validate_grid_endpoint(self) -> None: graph._graph.clear() _add_components( graph, - Component(1, ComponentCategory.GRID), - Component(99, ComponentCategory.METER), + Component(ComponentId(1), ComponentCategory.GRID), + Component(ComponentId(99), ComponentCategory.METER), ) - _add_connections(graph, Connection(99, 1)) + _add_connections(graph, Connection(ComponentId(99), ComponentId(1))) with pytest.raises( gr.InvalidGraphError, - match=r"Grid endpoint 1 has graph predecessors: \[Component" - r"\(component_id=99, category=, " + match=r"Grid endpoint with CID1 has graph predecessors: \[Component" + r"\(component_id=ComponentId\(99\), category=, " r"type=None, metadata=None\)\]", ): graph._validate_grid_endpoint() # grid endpoint has no successors graph._graph.clear() - _add_components(graph, Component(101, ComponentCategory.GRID)) + _add_components(graph, Component(ComponentId(101), ComponentCategory.GRID)) with pytest.raises( gr.InvalidGraphError, - match="Grid endpoint 101 has no graph successors!", + match="Grid endpoint with CID101 has no graph successors!", ): graph._validate_grid_endpoint() @@ -1276,10 +1405,10 @@ def test__validate_grid_endpoint(self) -> None: graph._graph.clear() _add_components( graph, - Component(1, ComponentCategory.GRID), - Component(2, ComponentCategory.METER), + Component(ComponentId(1), ComponentCategory.GRID), + Component(ComponentId(2), ComponentCategory.METER), ) - _add_connections(graph, Connection(1, 2)) + _add_connections(graph, Connection(ComponentId(1), ComponentId(2))) graph._validate_grid_endpoint() def test__validate_intermediary_components(self) -> None: @@ -1293,7 +1422,7 @@ def test__validate_intermediary_components(self) -> None: # missing predecessor for at least one intermediary node graph._graph.clear() - _add_components(graph, Component(3, ComponentCategory.INVERTER)) + _add_components(graph, Component(ComponentId(3), ComponentCategory.INVERTER)) with pytest.raises( gr.InvalidGraphError, match="Intermediary components without graph predecessors", @@ -1303,20 +1432,24 @@ def test__validate_intermediary_components(self) -> None: graph._graph.clear() _add_components( graph, - Component(1, ComponentCategory.GRID), - Component(3, ComponentCategory.INVERTER), + Component(ComponentId(1), ComponentCategory.GRID), + Component(ComponentId(3), ComponentCategory.INVERTER), ) - _add_connections(graph, Connection(1, 3)) + _add_connections(graph, Connection(ComponentId(1), ComponentId(3))) graph._validate_intermediary_components() graph._graph.clear() _add_components( graph, - Component(1, ComponentCategory.GRID), - Component(2, ComponentCategory.METER), - Component(3, ComponentCategory.INVERTER), + Component(ComponentId(1), ComponentCategory.GRID), + Component(ComponentId(2), ComponentCategory.METER), + Component(ComponentId(3), ComponentCategory.INVERTER), + ) + _add_connections( + graph, + Connection(ComponentId(1), ComponentId(2)), + Connection(ComponentId(2), ComponentId(3)), ) - _add_connections(graph, Connection(1, 2), Connection(2, 3)) graph._validate_intermediary_components() # all intermediary nodes have at least one predecessor @@ -1324,12 +1457,17 @@ def test__validate_intermediary_components(self) -> None: graph._graph.clear() _add_components( graph, - Component(1, ComponentCategory.GRID), - Component(2, ComponentCategory.METER), - Component(3, ComponentCategory.INVERTER), - Component(4, ComponentCategory.BATTERY), + Component(ComponentId(1), ComponentCategory.GRID), + Component(ComponentId(2), ComponentCategory.METER), + Component(ComponentId(3), ComponentCategory.INVERTER), + Component(ComponentId(4), ComponentCategory.BATTERY), + ) + _add_connections( + graph, + Connection(ComponentId(1), ComponentId(2)), + Connection(ComponentId(2), ComponentId(3)), + Connection(ComponentId(3), ComponentId(4)), ) - _add_connections(graph, Connection(1, 2), Connection(2, 3), Connection(3, 4)) graph._validate_intermediary_components() def test__validate_leaf_components(self) -> None: @@ -1343,14 +1481,14 @@ def test__validate_leaf_components(self) -> None: # missing predecessor for at least one leaf node graph._graph.clear() - _add_components(graph, Component(3, ComponentCategory.BATTERY)) + _add_components(graph, Component(ComponentId(3), ComponentCategory.BATTERY)) with pytest.raises( gr.InvalidGraphError, match="Leaf components without graph predecessors" ): graph._validate_leaf_components() graph._graph.clear() - _add_components(graph, Component(4, ComponentCategory.EV_CHARGER)) + _add_components(graph, Component(ComponentId(4), ComponentCategory.EV_CHARGER)) with pytest.raises( gr.InvalidGraphError, match="Leaf components without graph predecessors" ): @@ -1360,12 +1498,16 @@ def test__validate_leaf_components(self) -> None: graph._graph.clear() _add_components( graph, - Component(1, ComponentCategory.GRID), - Component(2, ComponentCategory.EV_CHARGER), - Component(3, ComponentCategory.BATTERY), + Component(ComponentId(1), ComponentCategory.GRID), + Component(ComponentId(2), ComponentCategory.EV_CHARGER), + Component(ComponentId(3), ComponentCategory.BATTERY), ) - _add_connections(graph, Connection(1, 2), Connection(2, 3)) + _add_connections( + graph, + Connection(ComponentId(1), ComponentId(2)), + Connection(ComponentId(2), ComponentId(3)), + ) with pytest.raises( gr.InvalidGraphError, match="Leaf components with graph successors" ): @@ -1374,11 +1516,15 @@ def test__validate_leaf_components(self) -> None: graph._graph.clear() _add_components( graph, - Component(1, ComponentCategory.GRID), - Component(3, ComponentCategory.BATTERY), - Component(4, ComponentCategory.EV_CHARGER), + Component(ComponentId(1), ComponentCategory.GRID), + Component(ComponentId(3), ComponentCategory.BATTERY), + Component(ComponentId(4), ComponentCategory.EV_CHARGER), + ) + _add_connections( + graph, + Connection(ComponentId(1), ComponentId(3)), + Connection(ComponentId(3), ComponentId(4)), ) - _add_connections(graph, Connection(1, 3), Connection(3, 4)) with pytest.raises( gr.InvalidGraphError, match="Leaf components with graph successors" ): @@ -1389,12 +1535,17 @@ def test__validate_leaf_components(self) -> None: graph._graph.clear() _add_components( graph, - Component(1, ComponentCategory.GRID), - Component(2, ComponentCategory.METER), - Component(3, ComponentCategory.BATTERY), - Component(4, ComponentCategory.EV_CHARGER), + Component(ComponentId(1), ComponentCategory.GRID), + Component(ComponentId(2), ComponentCategory.METER), + Component(ComponentId(3), ComponentCategory.BATTERY), + Component(ComponentId(4), ComponentCategory.EV_CHARGER), + ) + _add_connections( + graph, + Connection(ComponentId(1), ComponentId(2)), + Connection(ComponentId(1), ComponentId(3)), + Connection(ComponentId(1), ComponentId(4)), ) - _add_connections(graph, Connection(1, 2), Connection(1, 3), Connection(1, 4)) graph._validate_leaf_components() @@ -1403,10 +1554,14 @@ class TestComponentTypeIdentification: def test_no_comp_meters_pv(self) -> None: """Test the case where there are no meters in the graph.""" - grid = Component(1, ComponentCategory.GRID) - grid_meter = Component(2, ComponentCategory.METER) - pv_inv_1 = Component(3, ComponentCategory.INVERTER, InverterType.SOLAR) - pv_inv_2 = Component(4, ComponentCategory.INVERTER, InverterType.SOLAR) + grid = Component(ComponentId(1), ComponentCategory.GRID) + grid_meter = Component(ComponentId(2), ComponentCategory.METER) + pv_inv_1 = Component( + ComponentId(3), ComponentCategory.INVERTER, InverterType.SOLAR + ) + pv_inv_2 = Component( + ComponentId(4), ComponentCategory.INVERTER, InverterType.SOLAR + ) graph = gr._MicrogridComponentGraph( components={ @@ -1416,9 +1571,9 @@ def test_no_comp_meters_pv(self) -> None: pv_inv_2, }, connections={ - Connection(1, 2), - Connection(2, 3), - Connection(2, 4), + Connection(ComponentId(1), ComponentId(2)), + Connection(ComponentId(2), ComponentId(3)), + Connection(ComponentId(2), ComponentId(4)), }, ) @@ -1431,11 +1586,15 @@ def test_no_comp_meters_pv(self) -> None: def test_no_comp_meters_mixed(self) -> None: """Test the case where there are no meters in the graph.""" - grid = Component(1, ComponentCategory.GRID) - grid_meter = Component(2, ComponentCategory.METER) - pv_inv = Component(3, ComponentCategory.INVERTER, InverterType.SOLAR) - battery_inv = Component(4, ComponentCategory.INVERTER, InverterType.BATTERY) - battery = Component(5, ComponentCategory.BATTERY) + grid = Component(ComponentId(1), ComponentCategory.GRID) + grid_meter = Component(ComponentId(2), ComponentCategory.METER) + pv_inv = Component( + ComponentId(3), ComponentCategory.INVERTER, InverterType.SOLAR + ) + battery_inv = Component( + ComponentId(4), ComponentCategory.INVERTER, InverterType.BATTERY + ) + battery = Component(ComponentId(5), ComponentCategory.BATTERY) graph = gr._MicrogridComponentGraph( components={ @@ -1446,10 +1605,10 @@ def test_no_comp_meters_mixed(self) -> None: battery, }, connections={ - Connection(1, 2), - Connection(2, 3), - Connection(2, 4), - Connection(4, 5), + Connection(ComponentId(1), ComponentId(2)), + Connection(ComponentId(2), ComponentId(3)), + Connection(ComponentId(2), ComponentId(4)), + Connection(ComponentId(4), ComponentId(5)), }, ) @@ -1471,13 +1630,17 @@ def test_no_comp_meters_mixed(self) -> None: def test_with_meters(self) -> None: """Test the case where there are meters in the graph.""" - grid = Component(1, ComponentCategory.GRID) - grid_meter = Component(2, ComponentCategory.METER) - pv_meter = Component(3, ComponentCategory.METER) - pv_inv = Component(4, ComponentCategory.INVERTER, InverterType.SOLAR) - battery_meter = Component(5, ComponentCategory.METER) - battery_inv = Component(6, ComponentCategory.INVERTER, InverterType.BATTERY) - battery = Component(7, ComponentCategory.BATTERY) + grid = Component(ComponentId(1), ComponentCategory.GRID) + grid_meter = Component(ComponentId(2), ComponentCategory.METER) + pv_meter = Component(ComponentId(3), ComponentCategory.METER) + pv_inv = Component( + ComponentId(4), ComponentCategory.INVERTER, InverterType.SOLAR + ) + battery_meter = Component(ComponentId(5), ComponentCategory.METER) + battery_inv = Component( + ComponentId(6), ComponentCategory.INVERTER, InverterType.BATTERY + ) + battery = Component(ComponentId(7), ComponentCategory.BATTERY) graph = gr._MicrogridComponentGraph( components={ @@ -1490,12 +1653,12 @@ def test_with_meters(self) -> None: battery, }, connections={ - Connection(1, 2), - Connection(2, 3), - Connection(3, 4), - Connection(2, 5), - Connection(5, 6), - Connection(6, 7), + Connection(ComponentId(1), ComponentId(2)), + Connection(ComponentId(2), ComponentId(3)), + Connection(ComponentId(3), ComponentId(4)), + Connection(ComponentId(2), ComponentId(5)), + Connection(ComponentId(5), ComponentId(6)), + Connection(ComponentId(6), ComponentId(7)), }, ) @@ -1515,11 +1678,11 @@ def test_with_meters(self) -> None: def test_without_grid_meters(self) -> None: """Test the case where there are no grid meters in the graph.""" - grid = Component(1, ComponentCategory.GRID) - ev_meter = Component(2, ComponentCategory.METER) - ev_charger = Component(3, ComponentCategory.EV_CHARGER) - chp_meter = Component(4, ComponentCategory.METER) - chp = Component(5, ComponentCategory.CHP) + grid = Component(ComponentId(1), ComponentCategory.GRID) + ev_meter = Component(ComponentId(2), ComponentCategory.METER) + ev_charger = Component(ComponentId(3), ComponentCategory.EV_CHARGER) + chp_meter = Component(ComponentId(4), ComponentCategory.METER) + chp = Component(ComponentId(5), ComponentCategory.CHP) graph = gr._MicrogridComponentGraph( components={ @@ -1530,10 +1693,10 @@ def test_without_grid_meters(self) -> None: chp, }, connections={ - Connection(1, 2), - Connection(2, 3), - Connection(1, 4), - Connection(4, 5), + Connection(ComponentId(1), ComponentId(2)), + Connection(ComponentId(2), ComponentId(3)), + Connection(ComponentId(1), ComponentId(4)), + Connection(ComponentId(4), ComponentId(5)), }, ) diff --git a/tests/microgrid/test_grid.py b/tests/microgrid/test_grid.py index 3621c89fd..1f847f711 100644 --- a/tests/microgrid/test_grid.py +++ b/tests/microgrid/test_grid.py @@ -6,7 +6,7 @@ from contextlib import AsyncExitStack import frequenz.client.microgrid as client -from frequenz.client.microgrid import ComponentCategory +from frequenz.client.microgrid import ComponentCategory, ComponentId from frequenz.quantities import Current, Power, Quantity, ReactivePower from pytest_mock import MockerFixture @@ -27,11 +27,11 @@ async def test_grid_1(mocker: MockerFixture) -> None: # validate that islands with no grid connection are accepted. components = { - client.Component(1, client.ComponentCategory.NONE), - client.Component(2, client.ComponentCategory.METER), + client.Component(ComponentId(1), client.ComponentCategory.NONE), + client.Component(ComponentId(2), client.ComponentCategory.METER), } connections = { - client.Connection(1, 2), + client.Connection(ComponentId(1), ComponentId(2)), } graph = gr._MicrogridComponentGraph( # pylint: disable=protected-access @@ -52,15 +52,15 @@ async def test_grid_2(mocker: MockerFixture) -> None: """Validate that microgrids with one grid connection are accepted.""" components = { client.Component( - 1, + ComponentId(1), client.ComponentCategory.GRID, None, client.ComponentMetadata(fuse=client.Fuse(max_current=123.0)), ), - client.Component(2, client.ComponentCategory.METER), + client.Component(ComponentId(2), client.ComponentCategory.METER), } connections = { - client.Connection(1, 2), + client.Connection(ComponentId(1), ComponentId(2)), } graph = gr._MicrogridComponentGraph( # pylint: disable=protected-access @@ -79,13 +79,14 @@ async def test_grid_3(mocker: MockerFixture) -> None: """Validate that microgrids with a grid connection without a fuse are instantiated.""" components = { client.Component( - 1, client.ComponentCategory.GRID, None, client.GridMetadata(None) + ComponentId(1), + client.ComponentCategory.GRID, + None, + client.GridMetadata(None), ), - client.Component(2, client.ComponentCategory.METER), - } - connections = { - client.Connection(1, 2), + client.Component(ComponentId(2), client.ComponentCategory.METER), } + connections = {client.Connection(ComponentId(1), ComponentId(2))} graph = gr._MicrogridComponentGraph( # pylint: disable=protected-access components=components, connections=connections diff --git a/tests/microgrid/test_microgrid_api.py b/tests/microgrid/test_microgrid_api.py index 430605207..dd7ffc61f 100644 --- a/tests/microgrid/test_microgrid_api.py +++ b/tests/microgrid/test_microgrid_api.py @@ -13,9 +13,11 @@ from frequenz.client.microgrid import ( Component, ComponentCategory, + ComponentId, Connection, Location, Metadata, + MicrogridId, ) from frequenz.sdk.microgrid import connection_manager @@ -37,22 +39,22 @@ def components(self) -> list[list[Component]]: """ components = [ [ - Component(1, ComponentCategory.GRID), - Component(4, ComponentCategory.METER), - Component(5, ComponentCategory.METER), - Component(7, ComponentCategory.METER), - Component(8, ComponentCategory.INVERTER), - Component(9, ComponentCategory.BATTERY), - Component(10, ComponentCategory.METER), - Component(11, ComponentCategory.INVERTER), - Component(12, ComponentCategory.BATTERY), + Component(ComponentId(1), ComponentCategory.GRID), + Component(ComponentId(4), ComponentCategory.METER), + Component(ComponentId(5), ComponentCategory.METER), + Component(ComponentId(7), ComponentCategory.METER), + Component(ComponentId(8), ComponentCategory.INVERTER), + Component(ComponentId(9), ComponentCategory.BATTERY), + Component(ComponentId(10), ComponentCategory.METER), + Component(ComponentId(11), ComponentCategory.INVERTER), + Component(ComponentId(12), ComponentCategory.BATTERY), ], [ - Component(1, ComponentCategory.GRID), - Component(4, ComponentCategory.METER), - Component(7, ComponentCategory.METER), - Component(8, ComponentCategory.INVERTER), - Component(9, ComponentCategory.BATTERY), + Component(ComponentId(1), ComponentCategory.GRID), + Component(ComponentId(4), ComponentCategory.METER), + Component(ComponentId(7), ComponentCategory.METER), + Component(ComponentId(8), ComponentCategory.INVERTER), + Component(ComponentId(9), ComponentCategory.BATTERY), ], ] return components @@ -70,20 +72,20 @@ def connections(self) -> list[list[Connection]]: """ connections = [ [ - Connection(1, 4), - Connection(1, 5), - Connection(1, 7), - Connection(7, 8), - Connection(8, 9), - Connection(1, 10), - Connection(10, 11), - Connection(11, 12), + Connection(ComponentId(1), ComponentId(4)), + Connection(ComponentId(1), ComponentId(5)), + Connection(ComponentId(1), ComponentId(7)), + Connection(ComponentId(7), ComponentId(8)), + Connection(ComponentId(8), ComponentId(9)), + Connection(ComponentId(1), ComponentId(10)), + Connection(ComponentId(10), ComponentId(11)), + Connection(ComponentId(11), ComponentId(12)), ], [ - Connection(1, 4), - Connection(1, 7), - Connection(7, 8), - Connection(8, 9), + Connection(ComponentId(1), ComponentId(4)), + Connection(ComponentId(1), ComponentId(7)), + Connection(ComponentId(7), ComponentId(8)), + Connection(ComponentId(8), ComponentId(9)), ], ] return connections @@ -96,7 +98,7 @@ def metadata(self) -> Metadata: the microgrid metadata. """ return Metadata( - microgrid_id=8, + microgrid_id=MicrogridId(8), location=Location( latitude=52.520008, longitude=13.404954, diff --git a/tests/timeseries/_battery_pool/test_battery_pool.py b/tests/timeseries/_battery_pool/test_battery_pool.py index 5123f759e..e41db0868 100644 --- a/tests/timeseries/_battery_pool/test_battery_pool.py +++ b/tests/timeseries/_battery_pool/test_battery_pool.py @@ -20,7 +20,7 @@ import pytest import time_machine from frequenz.channels import Receiver, Sender -from frequenz.client.microgrid import ComponentCategory +from frequenz.client.microgrid import ComponentCategory, ComponentId from frequenz.quantities import Energy, Percentage, Power, Temperature from pytest_mock import MockerFixture @@ -62,7 +62,7 @@ def event_loop_policy() -> async_solipsism.EventLoopPolicy: def get_components( mock_microgrid: MockMicrogridClient, component_category: ComponentCategory -) -> set[int]: +) -> set[ComponentId]: """Get components of given category from mock microgrid. Args: @@ -209,10 +209,12 @@ async def setup_batteries_pool(mocker: MockerFixture) -> AsyncIterator[SetupArgs # tests were written). This can also change in the future if generator of mock # components changes, as it might produce different IDs, so this solution is also # not bullet-proof. - assert 8 in all_batteries - assert 11 in all_batteries + assert ComponentId(8) in all_batteries + assert ComponentId(11) in all_batteries - battery_pool = microgrid.new_battery_pool(priority=5, component_ids=set([8, 11])) + battery_pool = microgrid.new_battery_pool( + priority=5, component_ids=set([ComponentId(8), ComponentId(11)]) + ) dp = microgrid._data_pipeline._DATA_PIPELINE assert dp is not None @@ -244,7 +246,7 @@ async def setup_batteries_pool(mocker: MockerFixture) -> AsyncIterator[SetupArgs class Scenario(Generic[T]): """Single test scenario.""" - component_id: int + component_id: ComponentId """Which component should send new metrics.""" new_metrics: dict[str, Any] @@ -418,8 +420,8 @@ def compare_messages(msg: Any, expected_msg: Any) -> None: async def run_test_battery_status_channel( battery_status_sender: Sender[ComponentPoolStatus], battery_pool_metric_receiver: Receiver[T], - all_batteries: set[int], - batteries_in_pool: list[int], + all_batteries: set[ComponentId], + batteries_in_pool: list[ComponentId], waiting_time_sec: float, all_pool_result: T, only_first_battery_result: T, diff --git a/tests/timeseries/_formula_engine/utils.py b/tests/timeseries/_formula_engine/utils.py index 2a745d547..a3bae95d3 100644 --- a/tests/timeseries/_formula_engine/utils.py +++ b/tests/timeseries/_formula_engine/utils.py @@ -8,7 +8,7 @@ from math import isclose from frequenz.channels import Receiver -from frequenz.client.microgrid import ComponentMetricId +from frequenz.client.microgrid import ComponentId, ComponentMetricId from frequenz.sdk.microgrid import _data_pipeline from frequenz.sdk.timeseries._base_types import QuantityT, Sample @@ -19,7 +19,7 @@ def get_resampled_stream( namespace: str, - comp_id: int, + comp_id: ComponentId, metric_id: ComponentMetricId, create_method: Callable[[float], QuantityT], ) -> Receiver[Sample[QuantityT]]: diff --git a/tests/timeseries/_pv_pool/test_pv_pool_control_methods.py b/tests/timeseries/_pv_pool/test_pv_pool_control_methods.py index 6e16b5c1d..3bea88145 100644 --- a/tests/timeseries/_pv_pool/test_pv_pool_control_methods.py +++ b/tests/timeseries/_pv_pool/test_pv_pool_control_methods.py @@ -11,7 +11,7 @@ import async_solipsism import pytest from frequenz.channels import Receiver -from frequenz.client.microgrid import InverterComponentState +from frequenz.client.microgrid import ComponentId, InverterComponentState from frequenz.quantities import Power from pytest_mock import MockerFixture @@ -75,7 +75,9 @@ async def _init_pv_inverters(self, mocks: _Mocks) -> None: 0.05, ) - async def _fail_pv_inverters(self, fail_ids: list[int], mocks: _Mocks) -> None: + async def _fail_pv_inverters( + self, fail_ids: list[ComponentId], mocks: _Mocks + ) -> None: now = datetime.now(tz=timezone.utc) for idx, comp_id in enumerate(mocks.microgrid.pv_inverter_ids): mocks.streamer.update_stream( diff --git a/tests/timeseries/mock_microgrid.py b/tests/timeseries/mock_microgrid.py index 818bc9120..20b9abc7b 100644 --- a/tests/timeseries/mock_microgrid.py +++ b/tests/timeseries/mock_microgrid.py @@ -14,6 +14,7 @@ Component, ComponentCategory, ComponentData, + ComponentId, Connection, EVChargerCableState, EVChargerComponentState, @@ -42,8 +43,8 @@ class MockMicrogrid: # pylint: disable=too-many-instance-attributes """Setup a MockApi instance with multiple component layouts for tests.""" - grid_id = 1 - _grid_meter_id = 4 + grid_id = ComponentId(1) + _grid_meter_id = ComponentId(4) chp_id_suffix = 5 evc_id_suffix = 6 @@ -91,7 +92,9 @@ def __init__( # pylint: disable=too-many-arguments,too-many-positional-argument self._components: set[Component] = ( { - Component(1, ComponentCategory.GRID, None, GridMetadata(fuse)), + Component( + ComponentId(1), ComponentCategory.GRID, None, GridMetadata(fuse) + ), } if graph is None else graph.components() @@ -109,7 +112,7 @@ def __init__( # pylint: disable=too-many-arguments,too-many-positional-argument self._connect_to = self.grid_id - def filter_comp(category: ComponentCategory) -> list[int]: + def filter_comp(category: ComponentCategory) -> list[ComponentId]: if graph is None: return [] return sorted( @@ -121,7 +124,7 @@ def filter_comp(category: ComponentCategory) -> list[int]: ) ) - def inverters(comp_type: InverterType) -> list[int]: + def inverters(comp_type: InverterType) -> list[ComponentId]: if graph is None: return [] @@ -135,15 +138,15 @@ def inverters(comp_type: InverterType) -> list[int]: ] ) - self.chp_ids: list[int] = filter_comp(ComponentCategory.CHP) - self.battery_ids: list[int] = filter_comp(ComponentCategory.BATTERY) - self.evc_ids: list[int] = filter_comp(ComponentCategory.EV_CHARGER) - self.meter_ids: list[int] = filter_comp(ComponentCategory.METER) + self.chp_ids: list[ComponentId] = filter_comp(ComponentCategory.CHP) + self.battery_ids: list[ComponentId] = filter_comp(ComponentCategory.BATTERY) + self.evc_ids: list[ComponentId] = filter_comp(ComponentCategory.EV_CHARGER) + self.meter_ids: list[ComponentId] = filter_comp(ComponentCategory.METER) - self.battery_inverter_ids: list[int] = inverters(InverterType.BATTERY) - self.pv_inverter_ids: list[int] = inverters(InverterType.SOLAR) + self.battery_inverter_ids: list[ComponentId] = inverters(InverterType.BATTERY) + self.pv_inverter_ids: list[ComponentId] = inverters(InverterType.SOLAR) - self.bat_inv_map: dict[int, int] = ( + self.bat_inv_map: dict[ComponentId, ComponentId] = ( {} if graph is None else { @@ -156,17 +159,19 @@ def inverters(comp_type: InverterType) -> list[int]: } ) - self.evc_component_states: dict[int, EVChargerComponentState] = {} - self.evc_cable_states: dict[int, EVChargerCableState] = {} + self.evc_component_states: dict[ComponentId, EVChargerComponentState] = {} + self.evc_cable_states: dict[ComponentId, EVChargerCableState] = {} - self._streaming_coros: list[tuple[int, Coroutine[None, None, None]]] = [] + self._streaming_coros: list[tuple[ComponentId, Coroutine[None, None, None]]] = ( + [] + ) """The streaming coroutines for each component. The tuple stores the component id we are streaming for as the first item and the coroutine as the second item. """ - self._streaming_tasks: dict[int, asyncio.Task[None]] = {} + self._streaming_tasks: dict[ComponentId, asyncio.Task[None]] = {} """The streaming tasks for each component. The key is the component id we are streaming for in this task. @@ -249,14 +254,16 @@ def _done_callback(task: asyncio.Task[None]) -> None: return self.mock_client async def _comp_data_send_task( - self, comp_id: int, make_comp_data: Callable[[int, datetime], ComponentData] + self, + comp_id: ComponentId, + make_comp_data: Callable[[int, datetime], ComponentData], ) -> None: for value in range(1, self._num_values + 1): timestamp = datetime.now(tz=timezone.utc) - val_to_send = value + int(comp_id / 10) + val_to_send = value + int(int(comp_id) / 10) # for inverters with component_id > 100, send only half the messages. - if comp_id % 10 == self.inverter_id_suffix: - if comp_id < 100 or value <= 5: + if int(comp_id) % 10 == self.inverter_id_suffix: + if int(comp_id) < 100 or value <= 5: await self.mock_client.send(make_comp_data(val_to_send, timestamp)) else: await self.mock_client.send(make_comp_data(val_to_send, timestamp)) @@ -264,7 +271,7 @@ async def _comp_data_send_task( await self.mock_client.close_channel(comp_id) - def _start_meter_streaming(self, meter_id: int) -> None: + def _start_meter_streaming(self, meter_id: ComponentId) -> None: if not self._api_client_streaming: return self._streaming_coros.append( @@ -284,7 +291,7 @@ def _start_meter_streaming(self, meter_id: int) -> None: ) ) - def _start_battery_streaming(self, bat_id: int) -> None: + def _start_battery_streaming(self, bat_id: ComponentId) -> None: if not self._api_client_streaming: return self._streaming_coros.append( @@ -299,7 +306,7 @@ def _start_battery_streaming(self, bat_id: int) -> None: ) ) - def _start_inverter_streaming(self, inv_id: int) -> None: + def _start_inverter_streaming(self, inv_id: ComponentId) -> None: if not self._api_client_streaming: return self._streaming_coros.append( @@ -317,7 +324,7 @@ def _start_inverter_streaming(self, inv_id: int) -> None: ) ) - def _start_ev_charger_streaming(self, evc_id: int) -> None: + def _start_ev_charger_streaming(self, evc_id: ComponentId) -> None: if not self._api_client_streaming: return self._streaming_coros.append( @@ -351,7 +358,7 @@ def add_consumer_meters(self, count: int = 1) -> None: count: number of consumer meters to add. """ for _ in range(count): - meter_id = self._id_increment * 10 + self.meter_id_suffix + meter_id = ComponentId(self._id_increment * 10 + self.meter_id_suffix) self._id_increment += 1 self.meter_ids.append(meter_id) self._components.add( @@ -371,7 +378,7 @@ def add_chps(self, count: int, no_meters: bool = False) -> None: no_meters: if True, do not add a meter for each CHP. """ for _ in range(count): - chp_id = self._id_increment * 10 + self.chp_id_suffix + chp_id = ComponentId(self._id_increment * 10 + self.chp_id_suffix) self.chp_ids.append(chp_id) self._components.add( Component( @@ -382,7 +389,7 @@ def add_chps(self, count: int, no_meters: bool = False) -> None: if no_meters: self._connections.add(Connection(self._connect_to, chp_id)) else: - meter_id = self._id_increment * 10 + self.meter_id_suffix + meter_id = ComponentId(self._id_increment * 10 + self.meter_id_suffix) self.meter_ids.append(meter_id) self._components.add( Component( @@ -404,9 +411,9 @@ def add_batteries(self, count: int, no_meter: bool = False) -> None: no_meter: if True, do not add a meter for each battery set. """ for _ in range(count): - meter_id = self._id_increment * 10 + self.meter_id_suffix - inv_id = self._id_increment * 10 + self.inverter_id_suffix - bat_id = self._id_increment * 10 + self.battery_id_suffix + meter_id = ComponentId(self._id_increment * 10 + self.meter_id_suffix) + inv_id = ComponentId(self._id_increment * 10 + self.inverter_id_suffix) + bat_id = ComponentId(self._id_increment * 10 + self.battery_id_suffix) self._id_increment += 1 self.battery_inverter_ids.append(inv_id) @@ -448,8 +455,8 @@ def add_solar_inverters(self, count: int, no_meter: bool = False) -> None: no_meter: if True, do not add a meter for each inverter. """ for _ in range(count): - meter_id = self._id_increment * 10 + self.meter_id_suffix - inv_id = self._id_increment * 10 + self.inverter_id_suffix + meter_id = ComponentId(self._id_increment * 10 + self.meter_id_suffix) + inv_id = ComponentId(self._id_increment * 10 + self.inverter_id_suffix) self._id_increment += 1 self.pv_inverter_ids.append(inv_id) @@ -484,7 +491,7 @@ def add_ev_chargers(self, count: int) -> None: count: Number of ev chargers to add to the microgrid. """ for _ in range(count): - evc_id = self._id_increment * 10 + self.evc_id_suffix + evc_id = ComponentId(self._id_increment * 10 + self.evc_id_suffix) self._id_increment += 1 self.evc_ids.append(evc_id) diff --git a/tests/timeseries/mock_resampler.py b/tests/timeseries/mock_resampler.py index 976c2fbe2..e2f640be2 100644 --- a/tests/timeseries/mock_resampler.py +++ b/tests/timeseries/mock_resampler.py @@ -9,7 +9,7 @@ from datetime import datetime from frequenz.channels import Broadcast, Receiver, Sender -from frequenz.client.microgrid import ComponentMetricId +from frequenz.client.microgrid import ComponentId, ComponentMetricId from frequenz.quantities import Quantity from pytest_mock import MockerFixture @@ -31,11 +31,11 @@ def __init__( # pylint: disable=too-many-arguments,too-many-positional-argument self, mocker: MockerFixture, resampler_config: ResamplerConfig, - bat_inverter_ids: list[int], - pv_inverter_ids: list[int], - evc_ids: list[int], - chp_ids: list[int], - meter_ids: list[int], + bat_inverter_ids: list[ComponentId], + pv_inverter_ids: list[ComponentId], + evc_ids: list[ComponentId], + chp_ids: list[ComponentId], + meter_ids: list[ComponentId], namespaces: int, ) -> None: """Create a `MockDataPipeline` instance.""" @@ -48,7 +48,7 @@ def __init__( # pylint: disable=too-many-arguments,too-many-positional-argument self._input_channels_receivers: dict[str, list[Receiver[Sample[Quantity]]]] = {} def metric_senders( - comp_ids: list[int], + comp_ids: list[ComponentId], metric_id: ComponentMetricId, ) -> list[Sender[Sample[Quantity]]]: senders: list[Sender[Sample[Quantity]]] = [] @@ -106,7 +106,7 @@ def metric_senders( ) def multi_phase_senders( - ids: list[int], + ids: list[ComponentId], metrics: tuple[ComponentMetricId, ComponentMetricId, ComponentMetricId], ) -> list[list[Sender[Sample[Quantity]]]]: senders: list[list[Sender[Sample[Quantity]]]] = [] @@ -148,7 +148,9 @@ def multi_phase_senders( ] return senders - def current_senders(ids: list[int]) -> list[list[Sender[Sample[Quantity]]]]: + def current_senders( + ids: list[ComponentId], + ) -> list[list[Sender[Sample[Quantity]]]]: return multi_phase_senders( ids, ( @@ -158,7 +160,9 @@ def current_senders(ids: list[int]) -> list[list[Sender[Sample[Quantity]]]]: ), ) - def voltage_senders(ids: list[int]) -> list[list[Sender[Sample[Quantity]]]]: + def voltage_senders( + ids: list[ComponentId], + ) -> list[list[Sender[Sample[Quantity]]]]: return multi_phase_senders( ids, ( @@ -169,7 +173,7 @@ def voltage_senders(ids: list[int]) -> list[list[Sender[Sample[Quantity]]]]: ) def power_3_phase_senders( - ids: list[int], + ids: list[ComponentId], ) -> list[list[Sender[Sample[Quantity]]]]: return multi_phase_senders( ids, diff --git a/tests/utils/component_data_streamer.py b/tests/utils/component_data_streamer.py index f059a238d..a2ac218c4 100644 --- a/tests/utils/component_data_streamer.py +++ b/tests/utils/component_data_streamer.py @@ -8,7 +8,7 @@ from dataclasses import replace from datetime import datetime, timezone -from frequenz.client.microgrid import ComponentData +from frequenz.client.microgrid import ComponentData, ComponentId from frequenz.sdk._internal._asyncio import cancel_and_await @@ -32,8 +32,8 @@ def __init__(self, mock_microgrid: MockMicrogridClient) -> None: mock_microgrid: Mock microgrid. """ self._mock_microgrid = mock_microgrid - self._component_data: dict[int, ComponentData] = {} - self._streaming_tasks: dict[int, asyncio.Task[None]] = {} + self._component_data: dict[ComponentId, ComponentData] = {} + self._streaming_tasks: dict[ComponentId, asyncio.Task[None]] = {} def start_streaming( self, component_data: ComponentData, sampling_rate: float @@ -56,7 +56,7 @@ def start_streaming( self._stream_data(component_id, sampling_rate) ) - def get_current_component_data(self, component_id: int) -> ComponentData: + def get_current_component_data(self, component_id: ComponentId) -> ComponentData: """Get component data that are currently streamed or was streaming recently. Args: @@ -90,7 +90,7 @@ def update_stream(self, new_component_data: ComponentData) -> None: self._component_data[cid] = new_component_data - async def stop_streaming(self, component_id: int) -> None: + async def stop_streaming(self, component_id: ComponentId) -> None: """Stop sending data from this component.""" if task := self._streaming_tasks.pop(component_id, None): await cancel_and_await(task) @@ -101,7 +101,9 @@ async def stop(self) -> None: *[self.stop_streaming(cid) for cid in self._streaming_tasks] ) - async def _stream_data(self, component_id: int, sampling_rate: float) -> None: + async def _stream_data( + self, component_id: ComponentId, sampling_rate: float + ) -> None: while component_id in self._component_data: data = self._component_data[component_id] new_data = replace(data, timestamp=datetime.now(tz=timezone.utc)) diff --git a/tests/utils/component_data_wrapper.py b/tests/utils/component_data_wrapper.py index c76612caa..275f4a6b1 100644 --- a/tests/utils/component_data_wrapper.py +++ b/tests/utils/component_data_wrapper.py @@ -21,6 +21,7 @@ BatteryData, BatteryError, BatteryRelayState, + ComponentId, EVChargerCableState, EVChargerComponentState, EVChargerData, @@ -39,7 +40,7 @@ class BatteryDataWrapper(BatteryData): def __init__( self, - component_id: int, + component_id: ComponentId, timestamp: datetime, soc: float = math.nan, soc_lower_bound: float = math.nan, @@ -97,7 +98,7 @@ class InverterDataWrapper(InverterData): def __init__( # pylint: disable=too-many-locals self, - component_id: int, + component_id: ComponentId, timestamp: datetime, active_power: float = math.nan, active_power_per_phase: tuple[float, float, float] = ( @@ -165,7 +166,7 @@ class EvChargerDataWrapper(EVChargerData): def __init__( # pylint: disable=too-many-locals self, - component_id: int, + component_id: ComponentId, timestamp: datetime, active_power: float = math.nan, active_power_per_phase: tuple[float, float, float] = ( @@ -233,7 +234,7 @@ class MeterDataWrapper(MeterData): def __init__( self, - component_id: int, + component_id: ComponentId, timestamp: datetime, active_power: float = math.nan, active_power_per_phase: tuple[float, float, float] = ( diff --git a/tests/utils/component_graph_utils.py b/tests/utils/component_graph_utils.py index 380870485..7e3cd81a8 100644 --- a/tests/utils/component_graph_utils.py +++ b/tests/utils/component_graph_utils.py @@ -9,6 +9,7 @@ from frequenz.client.microgrid import ( Component, ComponentCategory, + ComponentId, Connection, InverterType, ) @@ -48,8 +49,8 @@ def create_component_graph_structure( Returns: Create set of components and set of connections between them. """ - grid_id = 1 - main_meter_id = 2 + grid_id = ComponentId(1) + main_meter_id = ComponentId(2) components = { Component(grid_id, ComponentCategory.GRID), @@ -57,15 +58,15 @@ def create_component_graph_structure( } connections = {Connection(grid_id, main_meter_id)} - junction_id: int = grid_id + junction_id = grid_id if component_graph_config.grid_side_meter: junction_id = main_meter_id start_idx = 3 for _ in range(component_graph_config.batteries_num): - meter_id = start_idx - inv_id = start_idx + 1 - battery_id = start_idx + 2 + meter_id = ComponentId(start_idx) + inv_id = ComponentId(int(start_idx) + 1) + battery_id = ComponentId(start_idx + 2) start_idx += 3 components.add(Component(meter_id, ComponentCategory.METER)) @@ -79,8 +80,8 @@ def create_component_graph_structure( connections.add(Connection(inv_id, battery_id)) for _ in range(component_graph_config.solar_inverters_num): - meter_id = start_idx - inv_id = start_idx + 1 + meter_id = ComponentId(start_idx) + inv_id = ComponentId(start_idx + 1) start_idx += 2 components.add(Component(meter_id, ComponentCategory.METER)) @@ -91,7 +92,7 @@ def create_component_graph_structure( connections.add(Connection(meter_id, inv_id)) for _ in range(component_graph_config.ev_chargers): - ev_id = start_idx + ev_id = ComponentId(start_idx) start_idx += 1 components.add(Component(ev_id, ComponentCategory.EV_CHARGER)) diff --git a/tests/utils/graph_generator.py b/tests/utils/graph_generator.py index f4f85c224..9bbd6dc93 100644 --- a/tests/utils/graph_generator.py +++ b/tests/utils/graph_generator.py @@ -9,6 +9,7 @@ from frequenz.client.microgrid import ( Component, ComponentCategory, + ComponentId, ComponentType, Connection, GridMetadata, @@ -33,7 +34,7 @@ def __init__(self) -> None: """Create a new instance.""" self._id_increment = 0 - def new_id(self) -> dict[ComponentCategory, int]: + def new_id(self) -> dict[ComponentCategory, ComponentId]: """Get the next available component id. Usage example: @@ -59,8 +60,8 @@ def new_id(self) -> dict[ComponentCategory, int]: a dict containing the next available id for each component category. """ id_per_category = { - category: self._id_increment * 10 + suffix - for category, suffix in self.SUFFIXES.items() + cat: ComponentId(self._id_increment * 10 + suffix) + for cat, suffix in self.SUFFIXES.items() } self._id_increment += 1 return id_per_category @@ -186,7 +187,9 @@ def grid() -> Component: Returns: a new grid component with default id. """ - return Component(1, ComponentCategory.GRID, None, GridMetadata(None)) + return Component( + ComponentId(1), ComponentCategory.GRID, None, GridMetadata(None) + ) def to_graph(self, components: Any) -> _MicrogridComponentGraph: """Convert a list of components to a graph. diff --git a/tests/utils/mock_microgrid_client.py b/tests/utils/mock_microgrid_client.py index 902bcdd0c..efea6ce07 100644 --- a/tests/utils/mock_microgrid_client.py +++ b/tests/utils/mock_microgrid_client.py @@ -12,11 +12,13 @@ Component, ComponentCategory, ComponentData, + ComponentId, Connection, EVChargerData, InverterData, Location, MeterData, + MicrogridId, ) from pytest_mock import MockerFixture @@ -35,7 +37,7 @@ def __init__( self, components: set[Component], connections: set[Connection], - microgrid_id: int = 8, + microgrid_id: MicrogridId = MicrogridId(8), location: Location = Location(latitude=52.520008, longitude=13.404954), ): """Create mock microgrid with given components and connections. @@ -61,7 +63,7 @@ def __init__( meter_channels = self._create_meter_channels() ev_charger_channels = self._create_ev_charger_channels() - self._all_channels: dict[int, Broadcast[Any]] = { + self._all_channels: dict[ComponentId, Broadcast[Any]] = { **bat_channels, **inv_channels, **meter_channels, @@ -154,7 +156,7 @@ async def send(self, data: ComponentData) -> None: # noqa: DOC503 else: raise RuntimeError(f"{type(data)} is not supported in MockMicrogridClient.") - async def close_channel(self, cid: int) -> None: + async def close_channel(self, cid: ComponentId) -> None: """Close channel for given component id. Args: @@ -163,7 +165,7 @@ async def close_channel(self, cid: int) -> None: if cid in self._all_channels: await self._all_channels[cid].close() - def _create_battery_channels(self) -> dict[int, Broadcast[BatteryData]]: + def _create_battery_channels(self) -> dict[ComponentId, Broadcast[BatteryData]]: """Create channels for the batteries. Returns: @@ -182,7 +184,7 @@ def _create_battery_channels(self) -> dict[int, Broadcast[BatteryData]]: for bid in batteries } - def _create_meter_channels(self) -> dict[int, Broadcast[MeterData]]: + def _create_meter_channels(self) -> dict[ComponentId, Broadcast[MeterData]]: """Create channels for the meters. Returns: @@ -200,7 +202,7 @@ def _create_meter_channels(self) -> dict[int, Broadcast[MeterData]]: cid: Broadcast[MeterData](name="meter_data_" + str(cid)) for cid in meters } - def _create_inverter_channels(self) -> dict[int, Broadcast[InverterData]]: + def _create_inverter_channels(self) -> dict[ComponentId, Broadcast[InverterData]]: """Create channels for the inverters. Returns: @@ -219,7 +221,9 @@ def _create_inverter_channels(self) -> dict[int, Broadcast[InverterData]]: for cid in inverters } - def _create_ev_charger_channels(self) -> dict[int, Broadcast[EVChargerData]]: + def _create_ev_charger_channels( + self, + ) -> dict[ComponentId, Broadcast[EVChargerData]]: """Create channels for the ev chargers. Returns: @@ -240,10 +244,10 @@ def _create_ev_charger_channels(self) -> dict[int, Broadcast[EVChargerData]]: def _create_mock_api( self, - bat_channels: dict[int, Broadcast[BatteryData]], - inv_channels: dict[int, Broadcast[InverterData]], - meter_channels: dict[int, Broadcast[MeterData]], - ev_charger_channels: dict[int, Broadcast[EVChargerData]], + bat_channels: dict[ComponentId, Broadcast[BatteryData]], + inv_channels: dict[ComponentId, Broadcast[InverterData]], + meter_channels: dict[ComponentId, Broadcast[MeterData]], + ev_charger_channels: dict[ComponentId, Broadcast[EVChargerData]], ) -> MagicMock: """Create mock of MicrogridApiClient. @@ -288,8 +292,8 @@ def _create_mock_api( def _get_battery_receiver( self, - component_id: int, - channels: dict[int, Broadcast[BatteryData]], + component_id: ComponentId, + channels: dict[ComponentId, Broadcast[BatteryData]], maxsize: int = RECEIVER_MAX_SIZE, ) -> Receiver[BatteryData]: """Return receiver of the broadcast channel for given component_id. @@ -308,8 +312,8 @@ def _get_battery_receiver( def _get_meter_receiver( self, - component_id: int, - channels: dict[int, Broadcast[MeterData]], + component_id: ComponentId, + channels: dict[ComponentId, Broadcast[MeterData]], maxsize: int = RECEIVER_MAX_SIZE, ) -> Receiver[MeterData]: """Return receiver of the broadcast channel for given component_id. @@ -328,8 +332,8 @@ def _get_meter_receiver( def _get_ev_charger_receiver( self, - component_id: int, - channels: dict[int, Broadcast[EVChargerData]], + component_id: ComponentId, + channels: dict[ComponentId, Broadcast[EVChargerData]], maxsize: int = RECEIVER_MAX_SIZE, ) -> Receiver[EVChargerData]: """Return receiver of the broadcast channel for given component_id. @@ -348,8 +352,8 @@ def _get_ev_charger_receiver( def _get_inverter_receiver( self, - component_id: int, - channels: dict[int, Broadcast[InverterData]], + component_id: ComponentId, + channels: dict[ComponentId, Broadcast[InverterData]], maxsize: int = RECEIVER_MAX_SIZE, ) -> Receiver[InverterData]: """Return receiver of the broadcast channel for given component_id.