diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 091f2b8dc..acc2d56ed 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,129 +1,7 @@ # Frequenz Python SDK Release Notes -## Summary - -This release replaces the `@actor` decorator with a new `Actor` class. - -## Upgrading - - -- The `frequenz.sdk.power` package contained the power distribution algorithm, which is for internal use in the sdk, and is no longer part of the public API. - -- `PowerDistributingActor`'s result type `OutOfBound` has been renamed to `OutOfBounds`, and its member variable `bound` has been renamed to `bounds`. - -- The `@actor` decorator was replaced by the new `Actor` class. The main differences between the new class and the old decorator are: - - * It doesn't start automatically, `start()` needs to be called to start an actor (using the `frequenz.sdk.actor.run()` function is recommended). - * The method to implement the main logic was renamed from `run()` to `_run()`, as it is not intended to be run externally. - * Actors can have an optional `name` (useful for debugging/logging purposes). - * The actor will only be restarted if an unhandled `Exception` is raised by `_run()`. It will not be restarted if the `_run()` method finishes normally. If an unhandled `BaseException` is raised instead, it will be re-raised. For normal cancellation the `_run()` method should handle `asyncio.CancelledError` if the cancellation shouldn't be propagated (this is the same as with the decorator). - * The `_stop()` method is public (`stop()`) and will `cancel()` and `await` for the task to finish, catching the `asyncio.CancelledError`. - * The `join()` method is renamed to `wait()`, but they can also be awaited directly ( `await actor`). - * For deterministic cleanup, actors can now be used as `async` context managers. - - Most actors can be migrated following these steps: - - 1. Remove the decorator - 2. Add `Actor` as a base class - 3. Rename `run()` to `_run()` - 4. Forward the `name` argument (optional but recommended) - - For example, this old actor: - - ```python - from frequenz.sdk.actor import actor - - @actor - class TheActor: - def __init__(self, actor_args) -> None: - # init code - - def run(self) -> None: - # run code - ``` - - Can be migrated as: - - ```python - import asyncio - from frequenz.sdk.actor import Actor - - class TheActor(Actor): - def __init__(self, actor_args, - *, - name: str | None = None, - ) -> None: - super().__init__(name=name) - # init code - - def _run(self) -> None: - # run code - ``` - - Then you can instantiate all your actors first and then run them using: - - ```python - from frequenz.sdk.actor import run - # Init code - actor = TheActor() - other_actor = OtherActor() - # more setup - await run(actor, other_actor) # Start and await for all the actors - ``` - -- The `MovingWindow` is now a `BackgroundService`, so it needs to be started manually with `window.start()`. It is recommended to use it as an `async` context manager if possible though: - - ```python - async with MovingWindow(...) as window: - # The moving windows is started here - use(window) - # The moving window is stopped here - ``` - -- The base actors (`ConfigManagingActor`, `ComponentMetricsResamplingActor`, `DataSourcingActor`, `PowerDistributingActor`) now inherit from the new `Actor` class, if you are using them directly, you need to start them manually with `actor.start()` and you might need to do some other adjustments. - -- The `BatteryPool.power_distribution_results` method has been enhanced to provide power distribution results in the form of `Power` objects, replacing the previous use of `float` values. - -- In the `Request` class: - * The attribute `request_timeout_sec` has been updated and is now named `request_timeout` and it is represented by a `timedelta` object rather than a `float`. - * The attribute `power` is now presented as a `Power` object, as opposed to a `float`. - -- Within the `EVChargerPool.set_bounds` method, the parameter `max_amps` has been redefined as `max_current`, and it is now represented using a `Current` object instead of a `float`. - -- The argument `nones_are_zeros` in `FormulaEngine` and related classes and methods is now a keyword-only argument. - -## New Features - -- Added `DFS` to the component graph - -- `BackgroundService`: This new abstract base class can be used to write other classes that runs one or more tasks in the background. It provides a consistent API to start and stop these services and also takes care of the handling of the background tasks. It can also work as an `async` context manager, giving the service a deterministic lifetime and guaranteed cleanup. - - All classes spawning tasks that are expected to run for an indeterminate amount of time are likely good candidates to use this as a base class. - -- `Actor`: This new class inherits from `BackgroundService` and it replaces the `@actor` decorator. - -- Newly added `min` and `max` functions for Formulas. They can be used as follows: - - ```python - formula1.min(formula2) - ``` - ## Bug Fixes -- Fixes a bug in the ring buffer updating the end timestamp of gaps when they are outdated. - -- Properly handles PV configurations with no or only some meters before the PV component. - - So far we only had configurations like this: `Meter -> Inverter -> PV`. However the scenario with `Inverter -> PV` is also possible and now handled correctly. - -- Fix `consumer_power()` not working certain configurations. - - In microgrids without consumers and no main meter, the formula would never return any values. - -- Fix `pv_power` not working in setups with 2 grid meters by using a new reliable function to search for components in the components graph - -- Fix `consumer_power` and `producer_power` similar to `pv_power` - -- Zero value requests received by the `PowerDistributingActor` will now always be accepted, even when there are non-zero exclusion bounds. +- Fix the **API Reference** link in the documentation website navigation bar. -- Hold on to a reference to all streaming tasks in the microgrid API client, so they don't get garbage collected. +- Fix the consumer power formula diff --git a/mkdocs.yml b/mkdocs.yml index 13fe0f1c9..c86bb14ae 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -24,6 +24,7 @@ theme: features: - content.code.annotate - content.code.copy + - navigation.indexes - navigation.instant - navigation.tabs - navigation.top @@ -115,7 +116,6 @@ plugins: - https://typing-extensions.readthedocs.io/en/stable/objects.inv - https://watchfiles.helpmanual.io/objects.inv - search - - section-index # Preview controls watch: diff --git a/src/frequenz/sdk/timeseries/_formula_engine/_formula_generators/_consumer_power_formula.py b/src/frequenz/sdk/timeseries/_formula_engine/_formula_generators/_consumer_power_formula.py index 2dc18f3fc..393a4d82c 100644 --- a/src/frequenz/sdk/timeseries/_formula_engine/_formula_generators/_consumer_power_formula.py +++ b/src/frequenz/sdk/timeseries/_formula_engine/_formula_generators/_consumer_power_formula.py @@ -106,7 +106,7 @@ def non_consumer_component(component: Component) -> bool: # push all grid meters for idx, grid_meter in enumerate(grid_meters): if idx > 0: - builder.push_oper("-") + builder.push_oper("+") builder.push_component_metric( grid_meter.component_id, nones_are_zeros=False ) diff --git a/tests/timeseries/test_logical_meter.py b/tests/timeseries/test_logical_meter.py index 0153c927e..4effa1b21 100644 --- a/tests/timeseries/test_logical_meter.py +++ b/tests/timeseries/test_logical_meter.py @@ -17,7 +17,7 @@ # pylint: disable=too-many-locals -class TestLogicalMeter: +class TestLogicalMeter: # pylint: disable=too-many-public-methods """Tests for the logical meter.""" async def test_grid_power_1(self, mocker: MockerFixture) -> None: @@ -278,6 +278,22 @@ async def test_consumer_power_no_grid_meter(self, mocker: MockerFixture) -> None 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_2_grid_meters( + self, + mocker: MockerFixture, + ) -> None: + """Test the grid power formula with grid meters.""" + mockgrid = MockMicrogrid(grid_meter=False) + # with no further sucessor these will be detected as grid meters + mockgrid.add_consumer_meters(2) + await mockgrid.start(mocker) + + logical_meter = microgrid.logical_meter() + grid_consumption_recv = logical_meter.grid_consumption_power.new_receiver() + + await mockgrid.mock_resampler.send_meter_power([1.0, 2.0]) + assert (await grid_consumption_recv.receive()).value == Power.from_watts(3.0) + async def test_consumer_power_no_grid_meter_no_consumer_meter( self, mocker: MockerFixture ) -> None: