Skip to content

Commit 890f3f7

Browse files
Add functionality to force power requests
A power request might need to be forced to implement safety mechanisms, even when some components might be seemingly failing (i.e. when there is not proper consumption information, the user wants to slowly discharge batteries to prevent potential peak breaches). Signed-off-by: Daniel Zullo <[email protected]>
1 parent 893fe1c commit 890f3f7

File tree

2 files changed

+31
-13
lines changed

2 files changed

+31
-13
lines changed

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

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,10 @@ def __init__(
195195
max_data_age_sec=10.0,
196196
)
197197

198+
self._cached_metrics: dict[int, InvBatPair | None] = {
199+
bat_id: None for bat_id, _ in self._bat_inv_map.items()
200+
}
201+
198202
def _create_users_tasks(self) -> List[asyncio.Task[None]]:
199203
"""For each user create a task to wait for request.
200204
@@ -208,38 +212,40 @@ def _create_users_tasks(self) -> List[asyncio.Task[None]]:
208212
)
209213
return tasks
210214

211-
def _get_upper_bound(self, batteries: Set[int]) -> int:
215+
def _get_upper_bound(self, batteries: Set[int], use_all: bool) -> int:
212216
"""Get total upper bound of power to be set for given batteries.
213217
214218
Note, output of that function doesn't guarantee that this bound will be
215219
the same when the request is processed.
216220
217221
Args:
218222
batteries: List of batteries
223+
use_all: flag whether all batteries must be used for the power request.
219224
220225
Returns:
221226
Upper bound for `set_power` operation.
222227
"""
223-
pairs_data: List[InvBatPair] = self._get_components_data(batteries)
228+
pairs_data: List[InvBatPair] = self._get_components_data(batteries, use_all)
224229
bound = sum(
225230
min(battery.power_upper_bound, inverter.active_power_upper_bound)
226231
for battery, inverter in pairs_data
227232
)
228233
return floor(bound)
229234

230-
def _get_lower_bound(self, batteries: Set[int]) -> int:
235+
def _get_lower_bound(self, batteries: Set[int], use_all: bool) -> int:
231236
"""Get total lower bound of power to be set for given batteries.
232237
233238
Note, output of that function doesn't guarantee that this bound will be
234239
the same when the request is processed.
235240
236241
Args:
237242
batteries: List of batteries
243+
use_all: flag whether all batteries must be used for the power request.
238244
239245
Returns:
240246
Lower bound for `set_power` operation.
241247
"""
242-
pairs_data: List[InvBatPair] = self._get_components_data(batteries)
248+
pairs_data: List[InvBatPair] = self._get_components_data(batteries, use_all)
243249
bound = sum(
244250
max(battery.power_lower_bound, inverter.active_power_lower_bound)
245251
for battery, inverter in pairs_data
@@ -268,7 +274,7 @@ async def run(self) -> None:
268274

269275
try:
270276
pairs_data: List[InvBatPair] = self._get_components_data(
271-
request.batteries
277+
request.batteries, request.force
272278
)
273279
except KeyError as err:
274280
await user.channel.send(Error(request=request, msg=str(err)))
@@ -375,7 +381,7 @@ def _check_request(self, request: Request) -> Optional[Result]:
375381
Result for the user if the request is wrong, None otherwise.
376382
"""
377383
for battery in request.batteries:
378-
if battery not in self._battery_receivers:
384+
if battery not in self._battery_receivers and request.force is False:
379385
msg = (
380386
f"No battery {battery}, available batteries: "
381387
f"{list(self._battery_receivers.keys())}"
@@ -384,11 +390,11 @@ def _check_request(self, request: Request) -> Optional[Result]:
384390

385391
if not request.adjust_power:
386392
if request.power < 0:
387-
bound = self._get_lower_bound(request.batteries)
393+
bound = self._get_lower_bound(request.batteries, request.force)
388394
if request.power < bound:
389395
return OutOfBound(request=request, bound=bound)
390396
else:
391-
bound = self._get_upper_bound(request.batteries)
397+
bound = self._get_upper_bound(request.batteries, request.force)
392398
if request.power > bound:
393399
return OutOfBound(request=request, bound=bound)
394400

@@ -537,11 +543,14 @@ def _get_components_pairs(
537543

538544
return bat_inv_map, inv_bat_map
539545

540-
def _get_components_data(self, batteries: Set[int]) -> List[InvBatPair]:
546+
def _get_components_data(
547+
self, batteries: Set[int], use_all: bool
548+
) -> List[InvBatPair]:
541549
"""Get data for the given batteries and adjacent inverters.
542550
543551
Args:
544552
batteries: Batteries that needs data.
553+
use_all: flag whether all batteries must be used for the power request.
545554
546555
Raises:
547556
KeyError: If any battery in the given list doesn't exists in microgrid.
@@ -551,11 +560,13 @@ def _get_components_data(self, batteries: Set[int]) -> List[InvBatPair]:
551560
"""
552561
pairs_data: List[InvBatPair] = []
553562
working_batteries = (
554-
self._all_battery_status.get_working_batteries(batteries) or batteries
563+
batteries
564+
if use_all
565+
else self._all_battery_status.get_working_batteries(batteries) or batteries
555566
)
556567

557568
for battery_id in working_batteries:
558-
if battery_id not in self._battery_receivers:
569+
if battery_id not in self._battery_receivers and use_all is False:
559570
raise KeyError(
560571
f"No battery {battery_id}, "
561572
f"available batteries: {list(self._battery_receivers.keys())}"
@@ -564,6 +575,8 @@ def _get_components_data(self, batteries: Set[int]) -> List[InvBatPair]:
564575
inverter_id: int = self._bat_inv_map[battery_id]
565576

566577
data = self._get_battery_inverter_data(battery_id, inverter_id)
578+
if data is None and use_all is True:
579+
data = self._cached_metrics[battery_id]
567580
if data is None:
568581
_logger.warning(
569582
"Skipping battery %d because its message isn't correct.",
@@ -631,7 +644,8 @@ def _get_battery_inverter_data(
631644

632645
# If all values are ok then return them.
633646
if not any(map(isnan, replaceable_metrics)):
634-
return InvBatPair(battery_data, inverter_data)
647+
self._cached_metrics[battery_id] = InvBatPair(battery_data, inverter_data)
648+
return self._cached_metrics[battery_id]
635649

636650
# Replace NaN with the corresponding value in the adjacent component.
637651
# If both metrics are None, return None to ignore this battery.
@@ -653,10 +667,11 @@ def _get_battery_inverter_data(
653667
elif isnan(inv_bound):
654668
inverter_new_metrics[inv_attr] = bat_bound
655669

656-
return InvBatPair(
670+
self._cached_metrics[battery_id] = InvBatPair(
657671
replace(battery_data, **battery_new_metrics),
658672
replace(inverter_data, **inverter_new_metrics),
659673
)
674+
return self._cached_metrics[battery_id]
660675

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

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,6 @@ class Request:
2929
If `False` and the power is outside the batteries' bounds, the request will
3030
fail and be replied to with an `OutOfBound` result.
3131
"""
32+
33+
force: bool = False
34+
"""Whether to force the power request regardless the status of components."""

0 commit comments

Comments
 (0)