Skip to content

Commit 465eab2

Browse files
committed
Merge branch 'v1.x.x' into generic-power-manager
The RELEASE_NOTES.md file has diverged and needs to be synced from the main branch, before it can be updated without conflicts. Signed-off-by: Sahas Subramanian <[email protected]>
2 parents 91a3c82 + cb84598 commit 465eab2

File tree

8 files changed

+187
-73
lines changed

8 files changed

+187
-73
lines changed

RELEASE_NOTES.md

Lines changed: 4 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -2,83 +2,16 @@
22

33
## Summary
44

5-
This version ships an experimental version of the **Power Manager**, adds preliminary support for n:m relations between inverters and batteries and includes user documentation.
5+
<!-- Here goes a general summary of what this release is about -->
66

77
## Upgrading
88

9-
- `microgrid.battery_pool()` method now accepts a priority value.
10-
11-
- `microgrid.grid()`
12-
13-
* Similar to `microgrid.battery_pool()`, the Grid is now similarily accessed.
14-
15-
- `BatteryPool`'s control methods
16-
17-
* Original methods `{set_power/charge/discharge}` are now replaced by `propose_{power/charge/discharge}`
18-
* The `propose_*` methods send power proposals to the `PowerManagingActor`, where it can be overridden by proposals from other actors.
19-
* They no longer have the `adjust_power` flag, because the `PowerManagingActor` will always adjust power to fit within the available bounds.
20-
* They no longer have a `include_broken_batteries` parameter. The feature has been removed.
21-
22-
- `BatteryPool`'s reporting methods
23-
24-
* `power_bounds` is replaced by `power_status`
25-
* The `power_status` method streams objects containing:
26-
+ bounds adjusted to the actor's priorities
27-
+ the latest target power for the set of batteries
28-
+ the results from the power distributor for the last request
29-
30-
- Move `microgrid.ComponentGraph` class to `microgrid.component_graph.ComponentGraph`, exposing only the high level interface functions through the `microgrid` package.
31-
32-
- An actor that is crashing will no longer instantly restart but induce an artificial delay to avoid potential spam-restarting.
9+
<!-- Here goes notes on how to upgrade from previous versions, including deprecations and what they should be replaced with -->
3310

3411
## New Features
3512

36-
- New and improved documentation.
37-
38-
* A new *User Guide* section was added, with:
39-
40-
+ A glossary.
41-
+ An introduction to actors.
42-
43-
* A new *Tutorials* section was added, with:
44-
45-
+ A getting started tutorial.
46-
47-
- In `OrderedRingBuffer`:
48-
- Rename `datetime_to_index` to `to_internal_index` to avoid confusion between the internal index and the external index.
49-
- Add `index_to_datetime` method to convert external index to corresponding datetime.
50-
- Remove `__setitem__` method to enforce usage of dedicated `update` method only.
51-
- In `OrderedRingBuffer` and `MovingWindow`:
52-
- Support for integer indices is added.
53-
- Add `count_covered` method to count the number of elements covered by the used time range.
54-
- Add `fill_value` option to window method to impute missing values. By default missing values are imputed with `NaN`.
55-
- Add `at` method to `MovingWindow` to access a single element and use it in `__getitem__` magic to fully support single element access.
56-
57-
- The PowerDistributingActor now supports n:m relations between inverters and batteries.
58-
59-
This means that one or more inverters can be connected to one or more batteries.
60-
61-
- A `PowerManagingActor` implementation.
62-
63-
- Allow configuration of the `resend_latest` flag in channels owned by the `ChannelRegistry`.
64-
65-
- Add consumption and production operators that will replace the logical meters production and consumption function variants.
66-
67-
- Consumption and production power formulas have been removed.
68-
69-
- The documentation was improved to:
70-
71-
* Show signatures with types.
72-
* Show the inherited members.
73-
* Documentation for pre-releases are now published.
74-
* Show the full tag name as the documentation version.
75-
* All development branches now have their documentation published (there is no `next` version anymore).
76-
* Fix the order of the documentation versions.
13+
<!-- Here goes the main new features and examples or instructions on how to use them -->
7714

7815
## Bug Fixes
7916

80-
- Fix rendering of diagrams in the documentation.
81-
- The `__getitem__` magic of the `MovingWindow` is fixed to support the same functionality that the `window` method provides.
82-
- Fixes incorrect implementation of single element access in `__getitem__` magic of `MovingWindow`.
83-
- Fix incorrect grid current calculations in locations where the calculations depended on current measurements from an inverter.
84-
- Fix power failure report to exclude any failed power from the succeeded power.
17+
<!-- Here goes notable bug fixes that are worth a special mention or explanation -->

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ dependencies = [
3838
"numpy >= 1.24.2, < 2",
3939
"protobuf >= 4.21.6, < 5",
4040
"pydantic >= 2.3, < 3",
41+
"timezonefinder >= 6.2.0, < 7",
4142
"tqdm >= 4.38.0, < 5",
4243
"typing_extensions >= 4.6.1, < 5",
4344
"watchfiles >= 0.15.0",

src/frequenz/sdk/microgrid/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@
124124

125125
from ..actor import ResamplerConfig
126126
from ..timeseries.grid import initialize as initialize_grid
127-
from . import _data_pipeline, client, component, connection_manager, fuse
127+
from . import _data_pipeline, client, component, connection_manager, fuse, metadata
128128
from ._data_pipeline import (
129129
battery_pool,
130130
ev_charger_pool,
@@ -161,4 +161,5 @@ async def initialize(host: str, port: int, resampler_config: ResamplerConfig) ->
161161
"grid",
162162
"frequency",
163163
"logical_meter",
164+
"metadata",
164165
]

src/frequenz/sdk/microgrid/client/_client.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from frequenz.api.microgrid import microgrid_pb2 as microgrid_pb
1616
from frequenz.api.microgrid.microgrid_pb2_grpc import MicrogridStub
1717
from frequenz.channels import Broadcast, Receiver, Sender
18+
from google.protobuf.empty_pb2 import Empty # pylint: disable=no-name-in-module
1819

1920
from ..._internal._constants import RECEIVER_MAX_SIZE
2021
from ..component import (
@@ -30,6 +31,7 @@
3031
_component_metadata_from_protobuf,
3132
_component_type_from_protobuf,
3233
)
34+
from ..metadata import Location, Metadata
3335
from ._connection import Connection
3436
from ._retry import LinearBackoff, RetryStrategy
3537

@@ -62,6 +64,14 @@ async def components(self) -> Iterable[Component]:
6264
Iterator whose elements are all the components in the microgrid.
6365
"""
6466

67+
@abstractmethod
68+
async def metadata(self) -> Metadata:
69+
"""Fetch the microgrid metadata.
70+
71+
Returns:
72+
the microgrid metadata.
73+
"""
74+
6575
@abstractmethod
6676
async def connections(
6777
self,
@@ -259,6 +269,36 @@ async def components(self) -> Iterable[Component]:
259269

260270
return result
261271

272+
async def metadata(self) -> Metadata:
273+
"""Fetch the microgrid metadata.
274+
275+
If there is an error fetching the metadata, the microgrid ID and
276+
location will be set to None.
277+
278+
Returns:
279+
the microgrid metadata.
280+
"""
281+
microgrid_metadata: microgrid_pb.MicrogridMetadata | None = None
282+
try:
283+
microgrid_metadata = await self.api.GetMicrogridMetadata(
284+
Empty(),
285+
timeout=int(DEFAULT_GRPC_CALL_TIMEOUT),
286+
) # type: ignore[misc]
287+
except grpc.aio.AioRpcError:
288+
_logger.exception("The microgrid metadata is not available.")
289+
290+
if not microgrid_metadata:
291+
return Metadata()
292+
293+
location: Location | None = None
294+
if microgrid_metadata.location:
295+
location = Location(
296+
latitude=microgrid_metadata.location.latitude,
297+
longitude=microgrid_metadata.location.longitude,
298+
)
299+
300+
return Metadata(microgrid_id=microgrid_metadata.microgrid_id, location=location)
301+
262302
async def connections(
263303
self,
264304
starts: set[int] | None = None,

src/frequenz/sdk/microgrid/connection_manager.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from .client import MicrogridApiClient
1717
from .client._client import MicrogridGrpcClient
1818
from .component_graph import ComponentGraph, _MicrogridComponentGraph
19+
from .metadata import Location, Metadata
1920

2021
# Not public default host and port
2122
_DEFAULT_MICROGRID_HOST = "[::1]"
@@ -74,6 +75,24 @@ def component_graph(self) -> ComponentGraph:
7475
component graph
7576
"""
7677

78+
@property
79+
@abstractmethod
80+
def microgrid_id(self) -> int | None:
81+
"""Get the ID of the microgrid if available.
82+
83+
Returns:
84+
the ID of the microgrid if available, None otherwise.
85+
"""
86+
87+
@property
88+
@abstractmethod
89+
def location(self) -> Location | None:
90+
"""Get the location of the microgrid if available.
91+
92+
Returns:
93+
the location of the microgrid if available, None otherwise.
94+
"""
95+
7796
async def _update_api(self, host: str, port: int) -> None:
7897
self._host = host
7998
self._port = port
@@ -103,6 +122,9 @@ def __init__(
103122
# So create empty graph here, and update it in `run` method.
104123
self._graph = _MicrogridComponentGraph()
105124

125+
self._metadata: Metadata
126+
"""The metadata of the microgrid."""
127+
106128
@property
107129
def api_client(self) -> MicrogridApiClient:
108130
"""Get MicrogridApiClient.
@@ -112,6 +134,24 @@ def api_client(self) -> MicrogridApiClient:
112134
"""
113135
return self._api
114136

137+
@property
138+
def microgrid_id(self) -> int | None:
139+
"""Get the ID of the microgrid if available.
140+
141+
Returns:
142+
the ID of the microgrid if available, None otherwise.
143+
"""
144+
return self._metadata.microgrid_id
145+
146+
@property
147+
def location(self) -> Location | None:
148+
"""Get the location of the microgrid if available.
149+
150+
Returns:
151+
the location of the microgrid if available, None otherwise.
152+
"""
153+
return self._metadata.location
154+
115155
@property
116156
def component_graph(self) -> ComponentGraph:
117157
"""Get component graph.
@@ -133,9 +173,11 @@ async def _update_api(self, host: str, port: int) -> None:
133173
target = f"{host}:{port}"
134174
grpc_channel = grpcaio.insecure_channel(target)
135175
self._api = MicrogridGrpcClient(grpc_channel, target)
176+
self._metadata = await self._api.metadata()
136177
await self._graph.refresh_from_api(self._api)
137178

138179
async def _initialize(self) -> None:
180+
self._metadata = await self._api.metadata()
139181
await self._graph.refresh_from_api(self._api)
140182

141183

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# License: MIT
2+
# Copyright © 2023 Frequenz Energy-as-a-Service GmbH
3+
4+
"""Metadata that describes a microgrid."""
5+
6+
from dataclasses import dataclass
7+
from zoneinfo import ZoneInfo
8+
9+
from timezonefinder import TimezoneFinder
10+
11+
_timezone_finder = TimezoneFinder()
12+
13+
14+
@dataclass(frozen=True, kw_only=True)
15+
class Location:
16+
"""Metadata for the location of microgrid."""
17+
18+
latitude: float | None = None
19+
"""The latitude of the microgrid in degree."""
20+
21+
longitude: float | None = None
22+
"""The longitude of the microgrid in degree."""
23+
24+
timezone: ZoneInfo | None = None
25+
"""The timezone of the microgrid.
26+
27+
The timezone will be set to None if the latitude or longitude points
28+
are not set or the timezone cannot be found given the location points.
29+
"""
30+
31+
def __post_init__(self) -> None:
32+
"""Initialize the timezone of the microgrid."""
33+
if self.latitude is None or self.longitude is None or self.timezone is not None:
34+
return
35+
36+
timezone = _timezone_finder.timezone_at(lat=self.latitude, lng=self.longitude)
37+
if timezone:
38+
# The dataclass is frozen, so it needs to use __setattr__ to set the timezone.
39+
object.__setattr__(self, "timezone", ZoneInfo(key=timezone))
40+
41+
42+
@dataclass(frozen=True, kw_only=True)
43+
class Metadata:
44+
"""Metadata for the microgrid."""
45+
46+
microgrid_id: int | None = None
47+
"""The ID of the microgrid."""
48+
49+
location: Location | None = None
50+
"""The location of the microgrid."""

0 commit comments

Comments
 (0)