diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 16a4e2b25..108ca32bc 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -44,6 +44,23 @@ grid_current_recv = grid.current.new_receiver() ``` +- Consumer and producer power formulas were moved from `microgrid.logical_meter()` to `microgrid.consumer()` and `microgrid.producer()`, respectively. + + Previously, + + ```python + logical_meter = microgrid.logical_meter() + consumer_power_recv = logical_meter.consumer_power.new_receiver() + producer_power_recv = logical_meter.producer_power.new_receiver() + ``` + + Now, + + ```python + consumer_power_recv = microgrid.consumer().power.new_receiver() + producer_power_recv = microgrid.producer().power.new_receiver() + ``` + - The `ComponentGraph.components()` parameters `component_id` and `component_category` were renamed to `component_ids` and `component_categories`, respectively. - The `GridFrequency.component` property was renamed to `GridFrequency.source` diff --git a/docs/user-guide/glossary.md b/docs/user-guide/glossary.md index 957b8aaa2..6c0c5eb9b 100644 --- a/docs/user-guide/glossary.md +++ b/docs/user-guide/glossary.md @@ -167,7 +167,7 @@ the component graph are: - figure out how to calculate high level metrics like [`grid_power`][frequenz.sdk.timeseries.grid.Grid.power], -[`consumer_power`][frequenz.sdk.timeseries.logical_meter.LogicalMeter.consumer_power], +[`consumer_power`][frequenz.sdk.timeseries.consumer.Consumer.power], etc. for a microgrid, using the available components. - identify the available {{glossary("battery", "batteries")}} or {{glossary("EV charger", "EV chargers")}} at a site that can be controlled. diff --git a/src/frequenz/sdk/microgrid/__init__.py b/src/frequenz/sdk/microgrid/__init__.py index 0231d6554..c853d3920 100644 --- a/src/frequenz/sdk/microgrid/__init__.py +++ b/src/frequenz/sdk/microgrid/__init__.py @@ -68,8 +68,7 @@ This is the main power consumer at the site of a microgrid, and often the {{glossary("load")}} the microgrid is built to support. The power drawn by the consumer -is available through -[`consumer_power`][frequenz.sdk.timeseries.logical_meter.LogicalMeter.consumer_power] +is available through [`consumer_power`][frequenz.sdk.timeseries.consumer.Consumer.power] In locations without a consumer, this method streams zero values. @@ -80,7 +79,7 @@ similarly the total CHP production in a site can be streamed through [`chp_power`][frequenz.sdk.timeseries.logical_meter.LogicalMeter.chp_power]. And total producer power is available through -[`producer_power`][frequenz.sdk.timeseries.logical_meter.LogicalMeter.producer_power]. +[`producer_power`][frequenz.sdk.timeseries.producer.Producer.power]. As is the case with the other methods, if PV Arrays or CHPs are not available in a microgrid, the corresponding methods stream zero values. @@ -126,10 +125,12 @@ from . import _data_pipeline, client, component, connection_manager, metadata from ._data_pipeline import ( battery_pool, + consumer, ev_charger_pool, frequency, grid, logical_meter, + producer, voltage, ) @@ -150,11 +151,13 @@ async def initialize(host: str, port: int, resampler_config: ResamplerConfig) -> "initialize", "client", "component", + "consumer", "battery_pool", "ev_charger_pool", "grid", "frequency", "logical_meter", "metadata", + "producer", "voltage", ] diff --git a/src/frequenz/sdk/microgrid/_data_pipeline.py b/src/frequenz/sdk/microgrid/_data_pipeline.py index abf11fc82..f80dd8bf3 100644 --- a/src/frequenz/sdk/microgrid/_data_pipeline.py +++ b/src/frequenz/sdk/microgrid/_data_pipeline.py @@ -46,8 +46,10 @@ from ..timeseries.battery_pool._battery_pool_reference_store import ( BatteryPoolReferenceStore, ) + from ..timeseries.consumer import Consumer from ..timeseries.ev_charger_pool import EVChargerPool from ..timeseries.logical_meter import LogicalMeter + from ..timeseries.producer import Producer _REQUEST_RECV_BUFFER_SIZE = 500 @@ -118,6 +120,8 @@ def __init__( self._power_managing_actor: _power_managing.PowerManagingActor | None = None self._logical_meter: LogicalMeter | None = None + self._consumer: Consumer | None = None + self._producer: Producer | None = None self._grid: Grid | None = None self._ev_charger_pools: dict[frozenset[int], EVChargerPool] = {} self._battery_pools: dict[frozenset[int], BatteryPoolReferenceStore] = {} @@ -125,11 +129,7 @@ def __init__( self._voltage_instance: VoltageStreamer | None = None def frequency(self) -> GridFrequency: - """Fetch the grid frequency for the microgrid. - - Returns: - The GridFrequency instance. - """ + """Return the grid frequency measuring point.""" if self._frequency_instance is None: self._frequency_instance = GridFrequency( self._data_sourcing_request_sender(), @@ -139,11 +139,7 @@ def frequency(self) -> GridFrequency: return self._frequency_instance def voltage(self) -> VoltageStreamer: - """Fetch the 3-phase voltage for the microgrid. - - Returns: - The VoltageStreamer instance. - """ + """Return the 3-phase voltage measuring point.""" if not self._voltage_instance: self._voltage_instance = VoltageStreamer( self._resampling_request_sender(), @@ -153,13 +149,7 @@ def voltage(self) -> VoltageStreamer: return self._voltage_instance def logical_meter(self) -> LogicalMeter: - """Return the logical meter instance. - - If a LogicalMeter instance doesn't exist, a new one is created and returned. - - Returns: - A logical meter instance. - """ + """Return the logical meter of the microgrid.""" from ..timeseries.logical_meter import LogicalMeter if self._logical_meter is None: @@ -169,6 +159,28 @@ def logical_meter(self) -> LogicalMeter: ) return self._logical_meter + def consumer(self) -> Consumer: + """Return the consumption measuring point of the microgrid.""" + from ..timeseries.consumer import Consumer + + if self._consumer is None: + self._consumer = Consumer( + channel_registry=self._channel_registry, + resampler_subscription_sender=self._resampling_request_sender(), + ) + return self._consumer + + def producer(self) -> Producer: + """Return the production measuring point of the microgrid.""" + from ..timeseries.producer import Producer + + if self._producer is None: + self._producer = Producer( + channel_registry=self._channel_registry, + resampler_subscription_sender=self._resampling_request_sender(), + ) + return self._producer + def ev_charger_pool( self, ev_charger_ids: set[int] | None = None, @@ -201,13 +213,7 @@ def ev_charger_pool( return self._ev_charger_pools[key] def grid(self) -> Grid: - """Return the grid instance. - - If a Grid instance doesn't exist, a new one is created and returned. - - Returns: - A Grid instance. - """ + """Return the grid measuring point.""" if self._grid is None: initialize_grid( channel_registry=self._channel_registry, @@ -419,32 +425,28 @@ async def initialize(resampler_config: ResamplerConfig) -> None: def frequency() -> GridFrequency: - """Return the grid frequency. - - Returns: - The grid frequency. - """ + """Return the grid frequency measuring point.""" return _get().frequency() def voltage() -> VoltageStreamer: - """Return the 3-phase voltage for the microgrid. - - Returns: - The 3-phase voltage. - """ + """Return the 3-phase voltage measuring point.""" return _get().voltage() def logical_meter() -> LogicalMeter: - """Return the logical meter instance. + """Return the logical meter of the microgrid.""" + return _get().logical_meter() - If a LogicalMeter instance doesn't exist, a new one is created and returned. - Returns: - A logical meter instance. - """ - return _get().logical_meter() +def consumer() -> Consumer: + """Return the [`Consumption`][frequenz.sdk.timeseries.consumer.Consumer] measuring point.""" + return _get().consumer() + + +def producer() -> Producer: + """Return the [`Production`][frequenz.sdk.timeseries.producer.Producer] measuring point.""" + return _get().producer() def ev_charger_pool(ev_charger_ids: set[int] | None = None) -> EVChargerPool: @@ -492,11 +494,7 @@ def battery_pool( def grid() -> Grid: - """Return the grid instance. - - Returns: - The Grid instance. - """ + """Return the grid measuring point.""" return _get().grid() diff --git a/src/frequenz/sdk/timeseries/consumer.py b/src/frequenz/sdk/timeseries/consumer.py new file mode 100644 index 000000000..da6445ee7 --- /dev/null +++ b/src/frequenz/sdk/timeseries/consumer.py @@ -0,0 +1,100 @@ +# License: MIT +# Copyright © 2022 Frequenz Energy-as-a-Service GmbH + +"""The logical component for calculating high level consumer metrics for a microgrid.""" + +import uuid + +from frequenz.channels import Sender + +from ..actor import ChannelRegistry, ComponentMetricRequest +from ._quantities import Power +from .formula_engine import FormulaEngine +from .formula_engine._formula_engine_pool import FormulaEnginePool +from .formula_engine._formula_generators import ConsumerPowerFormula + + +class Consumer: + """Calculate high level consumer metrics in a microgrid. + + Under normal circumstances this is expected to correspond to the gross + consumption of the site excluding active parts and battery. + + Consumer provides methods for fetching power values from different points + in the microgrid. These methods return `FormulaReceiver` objects, which can + be used like normal `Receiver`s, but can also be composed to form + higher-order formula streams. + + !!! note + `Consumer` instances are not meant to be created directly by users. + Use the [`microgrid.consumer`][frequenz.sdk.microgrid.consumer] method + for creating `Consumer` instances. + + Example: + ```python + from datetime import timedelta + + from frequenz.sdk import microgrid + from frequenz.sdk.timeseries import ResamplerConfig + + await microgrid.initialize( + "127.0.0.1", + 50051, + ResamplerConfig(resampling_period=timedelta(seconds=1.0)) + ) + + consumer = microgrid.consumer() + + # Get a receiver for a builtin formula + consumer_power_recv = consumer.power.new_receiver() + async for consumer_power_sample in consumer_power_recv: + print(consumer_power_sample) + ``` + """ + + _formula_pool: FormulaEnginePool + """The formula engine pool to generate consumer metrics.""" + + def __init__( + self, + channel_registry: ChannelRegistry, + resampler_subscription_sender: Sender[ComponentMetricRequest], + ) -> None: + """Initialize the consumer formula generator. + + Args: + channel_registry: The channel registry to use for the consumer. + resampler_subscription_sender: The sender to use for resampler subscriptions. + """ + namespace = f"consumer-{uuid.uuid4()}" + self._formula_pool = FormulaEnginePool( + namespace, + channel_registry, + resampler_subscription_sender, + ) + + @property + def power(self) -> FormulaEngine[Power]: + """Fetch the consumer power for the microgrid. + + This formula produces values that are in the Passive Sign Convention (PSC). + + It will start the formula engine to calculate consumer power if it is + not already running. + + A receiver from the formula engine can be created using the + `new_receiver` method. + + Returns: + A FormulaEngine that will calculate and stream consumer power. + """ + engine = self._formula_pool.from_power_formula_generator( + "consumer_power", + ConsumerPowerFormula, + ) + assert isinstance(engine, FormulaEngine) + return engine + + async def stop(self) -> None: + """Stop all formula engines.""" + await self._formula_pool.stop() diff --git a/src/frequenz/sdk/timeseries/formula_engine/_formula_engine.py b/src/frequenz/sdk/timeseries/formula_engine/_formula_engine.py index b5cc63d2d..462e7e54d 100644 --- a/src/frequenz/sdk/timeseries/formula_engine/_formula_engine.py +++ b/src/frequenz/sdk/timeseries/formula_engine/_formula_engine.py @@ -232,7 +232,7 @@ class FormulaEngine( They are used in the SDK to calculate and stream metrics like [`grid_power`][frequenz.sdk.timeseries.grid.Grid.power], - [`consumer_power`][frequenz.sdk.timeseries.logical_meter.LogicalMeter.consumer_power], + [`consumer_power`][frequenz.sdk.timeseries.consumer.Consumer.power], etc., which are building blocks of the [Frequenz SDK Microgrid Model][frequenz.sdk.microgrid--frequenz-sdk-microgrid-model]. @@ -332,10 +332,8 @@ def from_receiver( from frequenz.sdk.timeseries import Power async def run() -> None: - producer_power_engine = microgrid.logical_meter().producer_power - consumer_power_recv = ( - microgrid.logical_meter().consumer_power.new_receiver() - ) + producer_power_engine = microgrid.producer().power + consumer_power_recv = microgrid.consumer().power.new_receiver() excess_power_recv = ( ( diff --git a/src/frequenz/sdk/timeseries/logical_meter/_logical_meter.py b/src/frequenz/sdk/timeseries/logical_meter/_logical_meter.py index 9705f5850..e52527af8 100644 --- a/src/frequenz/sdk/timeseries/logical_meter/_logical_meter.py +++ b/src/frequenz/sdk/timeseries/logical_meter/_logical_meter.py @@ -13,12 +13,7 @@ from .._quantities import Power, Quantity from ..formula_engine import FormulaEngine from ..formula_engine._formula_engine_pool import FormulaEnginePool -from ..formula_engine._formula_generators import ( - CHPPowerFormula, - ConsumerPowerFormula, - ProducerPowerFormula, - PVPowerFormula, -) +from ..formula_engine._formula_generators import CHPPowerFormula, PVPowerFormula class LogicalMeter: @@ -50,9 +45,9 @@ class LogicalMeter: grid = microgrid.grid() # Get a receiver for a builtin formula - consumer_power_recv = logical_meter.consumer_power.new_receiver() - async for consumer_power_sample in consumer_power_recv: - print(consumer_power_sample) + pv_power_recv = logical_meter.pv_power.new_receiver() + async for pv_power_sample in pv_power_recv: + print(pv_power_sample) # or compose formulas to create a new formula net_power_recv = ( @@ -128,56 +123,6 @@ def start_formula( formula, component_metric_id, nones_are_zeros=nones_are_zeros ) - @property - def consumer_power(self) -> FormulaEngine[Power]: - """Fetch the consumer power for the microgrid. - - Under normal circumstances this is expected to correspond to the gross - consumption of the site excluding active parts and battery. - - This formula produces values that are in the Passive Sign Convention (PSC). - - If a formula engine to calculate consumer power is not already running, it will - be started. - - A receiver from the formula engine can be created using the `new_receiver` - method. - - Returns: - A FormulaEngine that will calculate and stream consumer power. - """ - engine = self._formula_pool.from_power_formula_generator( - "consumer_power", - ConsumerPowerFormula, - ) - assert isinstance(engine, FormulaEngine) - return engine - - @property - def producer_power(self) -> FormulaEngine[Power]: - """Fetch the producer power for the microgrid. - - Under normal circumstances this is expected to correspond to the production - of the sites active parts excluding ev chargers and batteries. - - This formula produces values that are in the Passive Sign Convention (PSC). - - If a formula engine to calculate producer power is not already running, it will - be started. - - A receiver from the formula engine can be created using the `new_receiver` - method. - - Returns: - A FormulaEngine that will calculate and stream producer power. - """ - engine = self._formula_pool.from_power_formula_generator( - "producer_power", - ProducerPowerFormula, - ) - assert isinstance(engine, FormulaEngine) - return engine - @property def pv_power(self) -> FormulaEngine[Power]: """Fetch the PV power in the microgrid. diff --git a/src/frequenz/sdk/timeseries/producer.py b/src/frequenz/sdk/timeseries/producer.py new file mode 100644 index 000000000..0c53a2607 --- /dev/null +++ b/src/frequenz/sdk/timeseries/producer.py @@ -0,0 +1,100 @@ +# License: MIT +# Copyright © 2022 Frequenz Energy-as-a-Service GmbH + +"""The logical component for calculating high level producer metrics for a microgrid.""" + +import uuid + +from frequenz.channels import Sender + +from ..actor import ChannelRegistry, ComponentMetricRequest +from ._quantities import Power +from .formula_engine import FormulaEngine +from .formula_engine._formula_engine_pool import FormulaEnginePool +from .formula_engine._formula_generators import ProducerPowerFormula + + +class Producer: + """Calculate high level producer metrics in a microgrid. + + Under normal circumstances this is expected to correspond to the gross + production of the sites active parts excluding EV chargers and batteries. + + Producer provides methods for fetching power values from different points + in the microgrid. These methods return `FormulaReceiver` objects, which can + be used like normal `Receiver`s, but can also be composed to form + higher-order formula streams. + + !!! note + `Producer` instances are not meant to be created directly by users. + Use the [`microgrid.producer`][frequenz.sdk.microgrid.producer] method + for creating `Producer` instances. + + Example: + ```python + from datetime import timedelta + + from frequenz.sdk import microgrid + from frequenz.sdk.timeseries import ResamplerConfig + + await microgrid.initialize( + "127.0.0.1", + 50051, + ResamplerConfig(resampling_period=timedelta(seconds=1.0)) + ) + + producer = microgrid.producer() + + # Get a receiver for a builtin formula + producer_power_recv = producer.power.new_receiver() + async for producer_power_sample in producer_power_recv: + print(producer_power_sample) + ``` + """ + + _formula_pool: FormulaEnginePool + """The formula engine pool to generate producer metrics.""" + + def __init__( + self, + channel_registry: ChannelRegistry, + resampler_subscription_sender: Sender[ComponentMetricRequest], + ) -> None: + """Initialize the producer formula generator. + + Args: + channel_registry: The channel registry to use for the producer. + resampler_subscription_sender: The sender to use for resampler subscriptions. + """ + namespace = f"producer-{uuid.uuid4()}" + self._formula_pool = FormulaEnginePool( + namespace, + channel_registry, + resampler_subscription_sender, + ) + + @property + def power(self) -> FormulaEngine[Power]: + """Fetch the producer power for the microgrid. + + This formula produces values that are in the Passive Sign Convention (PSC). + + It will start the formula engine to calculate producer power if it is + not already running. + + A receiver from the formula engine can be created using the + `new_receiver` method. + + Returns: + A FormulaEngine that will calculate and stream producer power. + """ + engine = self._formula_pool.from_power_formula_generator( + "producer_power", + ProducerPowerFormula, + ) + assert isinstance(engine, FormulaEngine) + return engine + + async def stop(self) -> None: + """Stop all formula engines.""" + await self._formula_pool.stop() diff --git a/tests/timeseries/test_consumer.py b/tests/timeseries/test_consumer.py new file mode 100644 index 000000000..919bcee3e --- /dev/null +++ b/tests/timeseries/test_consumer.py @@ -0,0 +1,68 @@ +# License: MIT +# Copyright © 2023 Frequenz Energy-as-a-Service GmbH + +"""Test the logical component for calculating high level consumer metrics.""" + +from contextlib import AsyncExitStack + +from pytest_mock import MockerFixture + +from frequenz.sdk import microgrid +from frequenz.sdk.timeseries._quantities import Power + +from .mock_microgrid import MockMicrogrid + + +class TestConsumer: + """Tests for the consumer power formula.""" + + async def test_consumer_power_grid_meter(self, mocker: MockerFixture) -> None: + """Test the consumer power formula with a grid meter.""" + mockgrid = MockMicrogrid(grid_meter=True, mocker=mocker) + mockgrid.add_batteries(2) + mockgrid.add_solar_inverters(2) + + async with mockgrid, AsyncExitStack() as stack: + consumer = microgrid.consumer() + stack.push_async_callback(consumer.stop) + consumer_power_receiver = consumer.power.new_receiver() + + await mockgrid.mock_resampler.send_meter_power([20.0, 2.0, 3.0, 4.0, 5.0]) + assert (await consumer_power_receiver.receive()).value == Power.from_watts( + 6.0 + ) + + async def test_consumer_power_no_grid_meter(self, mocker: MockerFixture) -> None: + """Test the consumer power formula without a grid meter.""" + mockgrid = MockMicrogrid(grid_meter=False, mocker=mocker) + mockgrid.add_consumer_meters() + mockgrid.add_batteries(2) + mockgrid.add_solar_inverters(2) + + async with mockgrid, AsyncExitStack() as stack: + consumer = microgrid.consumer() + stack.push_async_callback(consumer.stop) + consumer_power_receiver = consumer.power.new_receiver() + + await mockgrid.mock_resampler.send_meter_power([20.0, 2.0, 3.0, 4.0, 5.0]) + assert (await consumer_power_receiver.receive()).value == Power.from_watts( + 20.0 + ) + + async def test_consumer_power_no_grid_meter_no_consumer_meter( + self, mocker: MockerFixture + ) -> None: + """Test the consumer power formula without a grid meter.""" + mockgrid = MockMicrogrid(grid_meter=False, mocker=mocker) + mockgrid.add_batteries(2) + mockgrid.add_solar_inverters(2) + + async with mockgrid, AsyncExitStack() as stack: + consumer = microgrid.consumer() + stack.push_async_callback(consumer.stop) + consumer_power_receiver = consumer.power.new_receiver() + + await mockgrid.mock_resampler.send_non_existing_component_value() + assert (await consumer_power_receiver.receive()).value == Power.from_watts( + 0.0 + ) diff --git a/tests/timeseries/test_logical_meter.py b/tests/timeseries/test_logical_meter.py index 14a366030..972c2199c 100644 --- a/tests/timeseries/test_logical_meter.py +++ b/tests/timeseries/test_logical_meter.py @@ -75,132 +75,3 @@ async def test_pv_power_no_pv_components(self, mocker: MockerFixture) -> None: await mockgrid.mock_resampler.send_non_existing_component_value() assert (await pv_power_receiver.receive()).value == Power.zero() - - async def test_consumer_power_grid_meter(self, mocker: MockerFixture) -> None: - """Test the consumer power formula with a grid meter.""" - mockgrid = MockMicrogrid(grid_meter=True, mocker=mocker) - mockgrid.add_batteries(2) - mockgrid.add_solar_inverters(2) - - async with mockgrid, AsyncExitStack() as stack: - logical_meter = microgrid.logical_meter() - stack.push_async_callback(logical_meter.stop) - consumer_power_receiver = logical_meter.consumer_power.new_receiver() - - await mockgrid.mock_resampler.send_meter_power([20.0, 2.0, 3.0, 4.0, 5.0]) - assert (await consumer_power_receiver.receive()).value == Power.from_watts( - 6.0 - ) - - async def test_consumer_power_no_grid_meter(self, mocker: MockerFixture) -> None: - """Test the consumer power formula without a grid meter.""" - mockgrid = MockMicrogrid(grid_meter=False, mocker=mocker) - mockgrid.add_consumer_meters() - mockgrid.add_batteries(2) - mockgrid.add_solar_inverters(2) - - async with mockgrid, AsyncExitStack() as stack: - logical_meter = microgrid.logical_meter() - stack.push_async_callback(logical_meter.stop) - consumer_power_receiver = logical_meter.consumer_power.new_receiver() - - await mockgrid.mock_resampler.send_meter_power([20.0, 2.0, 3.0, 4.0, 5.0]) - assert (await consumer_power_receiver.receive()).value == Power.from_watts( - 20.0 - ) - - async def test_consumer_power_no_grid_meter_no_consumer_meter( - self, mocker: MockerFixture - ) -> None: - """Test the consumer power formula without a grid meter.""" - mockgrid = MockMicrogrid(grid_meter=False, mocker=mocker) - mockgrid.add_batteries(2) - mockgrid.add_solar_inverters(2) - - async with mockgrid, AsyncExitStack() as stack: - logical_meter = microgrid.logical_meter() - stack.push_async_callback(logical_meter.stop) - consumer_power_receiver = logical_meter.consumer_power.new_receiver() - - await mockgrid.mock_resampler.send_non_existing_component_value() - assert (await consumer_power_receiver.receive()).value == Power.from_watts( - 0.0 - ) - - async def test_producer_power(self, mocker: MockerFixture) -> None: - """Test the producer power formula.""" - mockgrid = MockMicrogrid(grid_meter=False, mocker=mocker) - mockgrid.add_solar_inverters(2) - mockgrid.add_chps(2) - - async with mockgrid, AsyncExitStack() as stack: - logical_meter = microgrid.logical_meter() - stack.push_async_callback(logical_meter.stop) - producer_power_receiver = logical_meter.producer_power.new_receiver() - - await mockgrid.mock_resampler.send_meter_power([2.0, 3.0, 4.0, 5.0]) - assert (await producer_power_receiver.receive()).value == Power.from_watts( - 14.0 - ) - - async def test_producer_power_no_chp(self, mocker: MockerFixture) -> None: - """Test the producer power formula without a chp.""" - mockgrid = MockMicrogrid(grid_meter=False, mocker=mocker) - mockgrid.add_solar_inverters(2) - - async with mockgrid, AsyncExitStack() as stack: - logical_meter = microgrid.logical_meter() - stack.push_async_callback(logical_meter.stop) - producer_power_receiver = logical_meter.producer_power.new_receiver() - - await mockgrid.mock_resampler.send_meter_power([2.0, 3.0]) - assert (await producer_power_receiver.receive()).value == Power.from_watts( - 5.0 - ) - - async def test_producer_power_no_pv_no_consumer_meter( - self, mocker: MockerFixture - ) -> None: - """Test the producer power formula without pv and without consumer meter.""" - mockgrid = MockMicrogrid(grid_meter=False, mocker=mocker) - mockgrid.add_chps(1, True) - - async with mockgrid, AsyncExitStack() as stack: - logical_meter = microgrid.logical_meter() - stack.push_async_callback(logical_meter.stop) - producer_power_receiver = logical_meter.producer_power.new_receiver() - - await mockgrid.mock_resampler.send_chp_power([2.0]) - assert (await producer_power_receiver.receive()).value == Power.from_watts( - 2.0 - ) - - async def test_producer_power_no_pv(self, mocker: MockerFixture) -> None: - """Test the producer power formula without pv.""" - mockgrid = MockMicrogrid(grid_meter=False, mocker=mocker) - mockgrid.add_consumer_meters() - mockgrid.add_chps(1) - - async with mockgrid, AsyncExitStack() as stack: - logical_meter = microgrid.logical_meter() - stack.push_async_callback(logical_meter.stop) - producer_power_receiver = logical_meter.producer_power.new_receiver() - - await mockgrid.mock_resampler.send_meter_power([20.0, 2.0]) - assert (await producer_power_receiver.receive()).value == Power.from_watts( - 2.0 - ) - - async def test_no_producer_power(self, mocker: MockerFixture) -> None: - """Test the producer power formula without producers.""" - async with MockMicrogrid( - grid_meter=True, mocker=mocker - ) as mockgrid, AsyncExitStack() as stack: - logical_meter = microgrid.logical_meter() - stack.push_async_callback(logical_meter.stop) - producer_power_receiver = logical_meter.producer_power.new_receiver() - - await mockgrid.mock_resampler.send_non_existing_component_value() - assert (await producer_power_receiver.receive()).value == Power.from_watts( - 0.0 - ) diff --git a/tests/timeseries/test_producer.py b/tests/timeseries/test_producer.py new file mode 100644 index 000000000..a8a700599 --- /dev/null +++ b/tests/timeseries/test_producer.py @@ -0,0 +1,95 @@ +# License: MIT +# Copyright © 2023 Frequenz Energy-as-a-Service GmbH + +"""Test the logical component for calculating high level producer metrics.""" + +from contextlib import AsyncExitStack + +from pytest_mock import MockerFixture + +from frequenz.sdk import microgrid +from frequenz.sdk.timeseries._quantities import Power + +from .mock_microgrid import MockMicrogrid + + +class TestProducer: + """Tests for the producer power formula.""" + + async def test_producer_power(self, mocker: MockerFixture) -> None: + """Test the producer power formula.""" + mockgrid = MockMicrogrid(grid_meter=False, mocker=mocker) + mockgrid.add_solar_inverters(2) + mockgrid.add_chps(2) + + async with mockgrid, AsyncExitStack() as stack: + producer = microgrid.producer() + stack.push_async_callback(producer.stop) + producer_power_receiver = producer.power.new_receiver() + + await mockgrid.mock_resampler.send_meter_power([2.0, 3.0, 4.0, 5.0]) + assert (await producer_power_receiver.receive()).value == Power.from_watts( + 14.0 + ) + + async def test_producer_power_no_chp(self, mocker: MockerFixture) -> None: + """Test the producer power formula without a chp.""" + mockgrid = MockMicrogrid(grid_meter=False, mocker=mocker) + mockgrid.add_solar_inverters(2) + + async with mockgrid, AsyncExitStack() as stack: + producer = microgrid.producer() + stack.push_async_callback(producer.stop) + producer_power_receiver = producer.power.new_receiver() + + await mockgrid.mock_resampler.send_meter_power([2.0, 3.0]) + assert (await producer_power_receiver.receive()).value == Power.from_watts( + 5.0 + ) + + async def test_producer_power_no_pv_no_consumer_meter( + self, mocker: MockerFixture + ) -> None: + """Test the producer power formula without pv and without consumer meter.""" + mockgrid = MockMicrogrid(grid_meter=False, mocker=mocker) + mockgrid.add_chps(1, True) + + async with mockgrid, AsyncExitStack() as stack: + producer = microgrid.producer() + stack.push_async_callback(producer.stop) + producer_power_receiver = producer.power.new_receiver() + + await mockgrid.mock_resampler.send_chp_power([2.0]) + assert (await producer_power_receiver.receive()).value == Power.from_watts( + 2.0 + ) + + async def test_producer_power_no_pv(self, mocker: MockerFixture) -> None: + """Test the producer power formula without pv.""" + mockgrid = MockMicrogrid(grid_meter=False, mocker=mocker) + mockgrid.add_consumer_meters() + mockgrid.add_chps(1) + + async with mockgrid, AsyncExitStack() as stack: + producer = microgrid.producer() + stack.push_async_callback(producer.stop) + producer_power_receiver = producer.power.new_receiver() + + await mockgrid.mock_resampler.send_meter_power([20.0, 2.0]) + assert (await producer_power_receiver.receive()).value == Power.from_watts( + 2.0 + ) + + async def test_no_producer_power(self, mocker: MockerFixture) -> None: + """Test the producer power formula without producers.""" + async with MockMicrogrid( + grid_meter=True, mocker=mocker + ) as mockgrid, AsyncExitStack() as stack: + producer = microgrid.producer() + stack.push_async_callback(producer.stop) + producer_power_receiver = producer.power.new_receiver() + + await mockgrid.mock_resampler.send_non_existing_component_value() + assert (await producer_power_receiver.receive()).value == Power.from_watts( + 0.0 + )