Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 2 additions & 124 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ theme:
features:
- content.code.annotate
- content.code.copy
- navigation.indexes
- navigation.instant
- navigation.tabs
- navigation.top
Expand Down Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand Down
18 changes: 17 additions & 1 deletion tests/timeseries/test_logical_meter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down