Skip to content

Commit 3c5e727

Browse files
authored
Remove support for include_broken_batteries in control commands (#713)
This removes - the `include_broken_batteries` fields in request objects and from function parameter lists, - the caching of battery data, that was necessary when trying to use broken batteries, - corresponding tests Closes #665
2 parents e04ee95 + ace9102 commit 3c5e727

File tree

7 files changed

+7
-386
lines changed

7 files changed

+7
-386
lines changed

RELEASE_NOTES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ This version ships an experimental version of the **Power Manager**, adds prelim
1313
* Original methods `{set_power/charge/discharge}` are now replaced by `propose_{power/charge/discharge}`
1414
* The `propose_*` methods send power proposals to the `PowerManagingActor`, where it can be overridden by proposals from other actors.
1515
* They no longer have the `adjust_power` flag, because the `PowerManagingActor` will always adjust power to fit within the available bounds.
16+
* They no longer have a `include_broken_batteries` parameter. The feature has been removed.
1617

1718
- `BatteryPool`'s reporting methods
1819

src/frequenz/sdk/actor/_power_managing/_base_classes.py

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -104,20 +104,6 @@ class Proposal:
104104
request_timeout: datetime.timedelta = datetime.timedelta(seconds=5.0)
105105
"""The maximum amount of time to wait for the request to be fulfilled."""
106106

107-
include_broken_batteries: bool = False
108-
"""Whether to use all batteries included in the batteries set regardless the status.
109-
110-
If set to `True`, the power distribution algorithm will consider all batteries,
111-
including the broken ones, when distributing power. In such cases, any remaining
112-
power after distributing among the available batteries will be distributed equally
113-
among the unavailable (broken) batteries. If all batteries in the set are
114-
unavailable, the power will be equally distributed among all the unavailable
115-
batteries in the request.
116-
117-
If set to `False`, the power distribution will only take into account the available
118-
batteries, excluding any broken ones.
119-
"""
120-
121107
def __lt__(self, other: Proposal) -> bool:
122108
"""Compare two proposals by their priority.
123109

src/frequenz/sdk/actor/_power_managing/_power_managing_actor.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -157,17 +157,13 @@ async def _send_updated_target_power(
157157
request_timeout = (
158158
proposal.request_timeout if proposal else timedelta(seconds=5.0)
159159
)
160-
include_broken_batteries = (
161-
proposal.include_broken_batteries if proposal else False
162-
)
163160
if target_power is not None:
164161
await self._power_distributing_requests_sender.send(
165162
power_distributing.Request(
166163
power=target_power,
167164
batteries=battery_ids,
168165
request_timeout=request_timeout,
169166
adjust_power=True,
170-
include_broken_batteries=include_broken_batteries,
171167
)
172168
)
173169

src/frequenz/sdk/actor/power_distributing/power_distributing.py

Lines changed: 6 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,12 @@
1414

1515
import asyncio
1616
import logging
17-
import time
1817
from asyncio.tasks import ALL_COMPLETED
1918
from collections import abc
2019
from collections.abc import Iterable
21-
from dataclasses import dataclass
2220
from datetime import timedelta
2321
from math import isnan
24-
from typing import Any, Self, TypeVar, cast
22+
from typing import Any, TypeVar, cast
2523

2624
import grpc
2725
from frequenz.channels import Peekable, Receiver, Sender
@@ -51,40 +49,6 @@
5149
_logger = logging.getLogger(__name__)
5250

5351

54-
@dataclass
55-
class _CacheEntry:
56-
"""Represents an entry in the cache with expiry time."""
57-
58-
inv_bat_pair: InvBatPair
59-
"""The inverter and adjacent battery data pair."""
60-
61-
expiry_time: int
62-
"""The expiration time (taken from the monotonic clock) of the cache entry."""
63-
64-
@classmethod
65-
def from_ttl(
66-
cls, inv_bat_pair: InvBatPair, ttl: timedelta = timedelta(hours=2.5)
67-
) -> Self:
68-
"""Initialize a CacheEntry instance from a TTL (Time-To-Live).
69-
70-
Args:
71-
inv_bat_pair: the inverter and adjacent battery data pair to cache.
72-
ttl: the time a cache entry is kept alive.
73-
74-
Returns:
75-
this class instance.
76-
"""
77-
return cls(inv_bat_pair, time.monotonic_ns() + int(ttl.total_seconds() * 1e9))
78-
79-
def has_expired(self) -> bool:
80-
"""Check whether the cache entry has expired.
81-
82-
Returns:
83-
whether the cache entry has expired.
84-
"""
85-
return time.monotonic_ns() >= self.expiry_time
86-
87-
8852
def _get_all(source: dict[int, frozenset[int]], keys: abc.Set[int]) -> set[int]:
8953
"""Get all values for the given keys from the given map.
9054
@@ -180,10 +144,6 @@ def __init__(
180144
max_data_age_sec=10.0,
181145
)
182146

183-
self._cached_metrics: dict[frozenset[int], _CacheEntry | None] = {
184-
bat_ids: None for bat_ids in self._bat_bats_map.values()
185-
}
186-
187147
def _get_bounds(
188148
self,
189149
pairs_data: list[InvBatPair],
@@ -254,13 +214,13 @@ async def _run(self) -> None: # pylint: disable=too-many-locals
254214
async for request in self._requests_receiver:
255215
try:
256216
pairs_data: list[InvBatPair] = self._get_components_data(
257-
request.batteries, request.include_broken_batteries
217+
request.batteries
258218
)
259219
except KeyError as err:
260220
await self._result_sender.send(Error(request=request, msg=str(err)))
261221
continue
262222

263-
if not pairs_data and not request.include_broken_batteries:
223+
if not pairs_data:
264224
error_msg = (
265225
"No data for at least one of the given "
266226
f"batteries {str(request.batteries)}"
@@ -390,24 +350,10 @@ def _get_power_distribution(
390350
]:
391351
unavailable_inv_ids = unavailable_inv_ids.union(inverter_ids)
392352

393-
if request.include_broken_batteries and not available_bat_ids:
394-
return self.distribution_algorithm.distribute_power_equally(
395-
request.power.as_watts(), unavailable_inv_ids
396-
)
397-
398353
result = self.distribution_algorithm.distribute_power(
399354
request.power.as_watts(), inv_bat_pairs
400355
)
401356

402-
if request.include_broken_batteries and unavailable_inv_ids:
403-
additional_result = self.distribution_algorithm.distribute_power_equally(
404-
result.remaining_power, unavailable_inv_ids
405-
)
406-
407-
for inv_id, power in additional_result.distribution.items():
408-
result.distribution[inv_id] = power
409-
result.remaining_power = 0.0
410-
411357
return result
412358

413359
def _check_request(
@@ -526,15 +472,11 @@ def _get_components_pairs(
526472
{k: frozenset(v) for k, v in inv_invs_map.items()},
527473
)
528474

529-
def _get_components_data(
530-
self, batteries: abc.Set[int], include_broken: bool
531-
) -> list[InvBatPair]:
475+
def _get_components_data(self, batteries: abc.Set[int]) -> list[InvBatPair]:
532476
"""Get data for the given batteries and adjacent inverters.
533477
534478
Args:
535479
batteries: Batteries that needs data.
536-
include_broken: whether all batteries in the batteries set in the
537-
request must be used regardless the status.
538480
539481
Raises:
540482
KeyError: If any battery in the given list doesn't exists in microgrid.
@@ -544,11 +486,7 @@ def _get_components_data(
544486
"""
545487
pairs_data: list[InvBatPair] = []
546488

547-
working_batteries = (
548-
batteries
549-
if include_broken
550-
else self._all_battery_status.get_working_batteries(batteries)
551-
)
489+
working_batteries = self._all_battery_status.get_working_batteries(batteries)
552490

553491
for battery_id in working_batteries:
554492
if battery_id not in self._battery_receivers:
@@ -579,12 +517,6 @@ def _get_components_data(
579517
inverter_ids: frozenset[int] = self._bat_inv_map[next(iter(battery_ids))]
580518

581519
data = self._get_battery_inverter_data(battery_ids, inverter_ids)
582-
if not data and include_broken:
583-
cached_entry = self._cached_metrics[battery_ids]
584-
if cached_entry and not cached_entry.has_expired():
585-
data = cached_entry.inv_bat_pair
586-
else:
587-
data = None
588520
if data is None:
589521
_logger.warning(
590522
"Skipping battery set %s because at least one of its messages isn't correct.",
@@ -669,9 +601,7 @@ def nan_metric_in_list(data: list[DataType], metrics: list[str]) -> bool:
669601
)
670602
return None
671603

672-
inv_bat_pair = InvBatPair(AggregatedBatteryData(battery_data), inverter_data)
673-
self._cached_metrics[battery_ids] = _CacheEntry.from_ttl(inv_bat_pair)
674-
return inv_bat_pair
604+
return InvBatPair(AggregatedBatteryData(battery_data), inverter_data)
675605

676606
async def _create_channels(self) -> None:
677607
"""Create channels to get data of components in microgrid."""

src/frequenz/sdk/actor/power_distributing/request.py

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -32,17 +32,3 @@ class Request:
3232
If `False` and the power is outside the batteries' bounds, the request will
3333
fail and be replied to with an `OutOfBound` result.
3434
"""
35-
36-
include_broken_batteries: bool = False
37-
"""Whether to use all batteries included in the batteries set regardless the status.
38-
39-
If set to `True`, the power distribution algorithm will consider all batteries,
40-
including the broken ones, when distributing power. In such cases, any remaining
41-
power after distributing among the available batteries will be distributed equally
42-
among the unavailable (broken) batteries. If all batteries in the set are
43-
unavailable, the power will be equally distributed among all the unavailable
44-
batteries in the request.
45-
46-
If set to `False`, the power distribution will only take into account the available
47-
batteries, excluding any broken ones.
48-
"""

src/frequenz/sdk/timeseries/battery_pool/_battery_pool.py

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,6 @@ async def propose_power(
7979
power: Power | None,
8080
*,
8181
request_timeout: timedelta = timedelta(seconds=5.0),
82-
include_broken_batteries: bool = False,
8382
bounds: timeseries.Bounds[Power | None] = timeseries.Bounds(None, None),
8483
) -> None:
8584
"""Send a proposal to the power manager for the pool's set of batteries.
@@ -112,11 +111,6 @@ async def propose_power(
112111
specified. If both are `None`, it is equivalent to not having a
113112
proposal or withdrawing a previous one.
114113
request_timeout: The timeout for the request.
115-
include_broken_batteries: if True, the power will be set for all batteries
116-
in the pool, including the broken ones. If False, then the power will be
117-
set only for the working batteries. This is not a guarantee that the
118-
power will be set for all working batteries, as the microgrid API may
119-
still reject the request.
120114
bounds: The power bounds for the proposal. These bounds will apply to
121115
actors with a lower priority, and can be overridden by bounds from
122116
actors with a higher priority. If None, the power bounds will be set
@@ -131,7 +125,6 @@ async def propose_power(
131125
battery_ids=self._battery_pool._batteries,
132126
priority=self._priority,
133127
request_timeout=request_timeout,
134-
include_broken_batteries=include_broken_batteries,
135128
)
136129
)
137130

@@ -140,7 +133,6 @@ async def propose_charge(
140133
power: Power | None,
141134
*,
142135
request_timeout: timedelta = timedelta(seconds=5.0),
143-
include_broken_batteries: bool = False,
144136
) -> None:
145137
"""Set the given charge power for the batteries in the pool.
146138
@@ -164,11 +156,6 @@ async def propose_charge(
164156
If None, the proposed power of higher priority actors will take
165157
precedence as the target power.
166158
request_timeout: The timeout for the request.
167-
include_broken_batteries: if True, the power will be set for all batteries
168-
in the pool, including the broken ones. If False, then the power will be
169-
set only for the working batteries. This is not a guarantee that the
170-
power will be set for all working batteries, as the microgrid API may
171-
still reject the request.
172159
173160
Raises:
174161
ValueError: If the given power is negative.
@@ -183,7 +170,6 @@ async def propose_charge(
183170
battery_ids=self._battery_pool._batteries,
184171
priority=self._priority,
185172
request_timeout=request_timeout,
186-
include_broken_batteries=include_broken_batteries,
187173
)
188174
)
189175

@@ -192,7 +178,6 @@ async def propose_discharge(
192178
power: Power | None,
193179
*,
194180
request_timeout: timedelta = timedelta(seconds=5.0),
195-
include_broken_batteries: bool = False,
196181
) -> None:
197182
"""Set the given discharge power for the batteries in the pool.
198183
@@ -216,11 +201,6 @@ async def propose_discharge(
216201
pool. If None, the proposed power of higher priority actors will take
217202
precedence as the target power.
218203
request_timeout: The timeout for the request.
219-
include_broken_batteries: if True, the power will be set for all batteries
220-
in the pool, including the broken ones. If False, then the power will be
221-
set only for the working batteries. This is not a guarantee that the
222-
power will be set for all working batteries, as the microgrid API may
223-
still reject the request.
224204
225205
Raises:
226206
ValueError: If the given power is negative.
@@ -235,7 +215,6 @@ async def propose_discharge(
235215
battery_ids=self._battery_pool._batteries,
236216
priority=self._priority,
237217
request_timeout=request_timeout,
238-
include_broken_batteries=include_broken_batteries,
239218
)
240219
)
241220

0 commit comments

Comments
 (0)