Skip to content

Commit 25128a7

Browse files
authored
Don't return None from battery pool methods (#612)
In the case when no battery is available, the internal values of the `Sample` or `PowerMetric` will be set to `None` instead.
2 parents a8f3d62 + 206e56c commit 25128a7

File tree

4 files changed

+48
-21
lines changed

4 files changed

+48
-21
lines changed

RELEASE_NOTES.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88

99
<!-- Here goes notes on how to upgrade from previous versions, including deprecations and what they should be replaced with -->
1010

11+
- The battery pool metric methods no longer return `None` when no batteries are available. Instead, the value of the `Sample` or `PowerMetric` is set to `None`.
12+
1113
## New Features
1214

1315
<!-- Here goes the main new features and examples or instructions on how to use them -->

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

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ def calculate(
124124
self,
125125
metrics_data: dict[int, ComponentMetricsData],
126126
working_batteries: set[int],
127-
) -> T | None:
127+
) -> T:
128128
"""Aggregate the metrics_data and calculate high level metric.
129129
130130
Missing components will be ignored. Formula will be calculated for all
@@ -191,7 +191,7 @@ def calculate(
191191
self,
192192
metrics_data: dict[int, ComponentMetricsData],
193193
working_batteries: set[int],
194-
) -> Sample[Energy] | None:
194+
) -> Sample[Energy]:
195195
"""Aggregate the metrics_data and calculate high level metric.
196196
197197
Missing components will be ignored. Formula will be calculated for all
@@ -229,7 +229,7 @@ def calculate(
229229
total_capacity += usable_capacity
230230

231231
return (
232-
None
232+
Sample(datetime.now(tz=timezone.utc), None)
233233
if timestamp == _MIN_TIMESTAMP
234234
else Sample[Energy](timestamp, Energy.from_watt_hours(total_capacity))
235235
)
@@ -281,7 +281,7 @@ def calculate(
281281
self,
282282
metrics_data: dict[int, ComponentMetricsData],
283283
working_batteries: set[int],
284-
) -> Sample[Temperature] | None:
284+
) -> Sample[Temperature]:
285285
"""Aggregate the metrics_data and calculate high level metric for temperature.
286286
287287
Missing components will be ignored. Formula will be calculated for all
@@ -312,7 +312,7 @@ def calculate(
312312
temperature_sum += temperature
313313
temperature_count += 1
314314
if timestamp == _MIN_TIMESTAMP:
315-
return None
315+
return Sample(datetime.now(tz=timezone.utc), None)
316316

317317
temperature_avg = temperature_sum / temperature_count
318318

@@ -371,7 +371,7 @@ def calculate(
371371
self,
372372
metrics_data: dict[int, ComponentMetricsData],
373373
working_batteries: set[int],
374-
) -> Sample[Percentage] | None:
374+
) -> Sample[Percentage]:
375375
"""Aggregate the metrics_data and calculate high level metric.
376376
377377
Missing components will be ignored. Formula will be calculated for all
@@ -432,7 +432,7 @@ def calculate(
432432
total_capacity_x100 += usable_capacity_x100
433433

434434
if timestamp == _MIN_TIMESTAMP:
435-
return None
435+
return Sample(datetime.now(tz=timezone.utc), None)
436436

437437
# To avoid zero division error
438438
if total_capacity_x100 == 0:
@@ -598,7 +598,7 @@ def calculate(
598598
self,
599599
metrics_data: dict[int, ComponentMetricsData],
600600
working_batteries: set[int],
601-
) -> PowerMetrics | None:
601+
) -> PowerMetrics:
602602
"""Aggregate the metrics_data and calculate high level metric.
603603
604604
Missing components will be ignored. Formula will be calculated for all
@@ -642,7 +642,11 @@ def calculate(
642642
exclusion_bounds_lower += min(exclusion_lower_bounds)
643643

644644
if timestamp == _MIN_TIMESTAMP:
645-
return None
645+
return PowerMetrics(
646+
timestamp=datetime.now(tz=timezone.utc),
647+
inclusion_bounds=None,
648+
exclusion_bounds=None,
649+
)
646650

647651
return PowerMetrics(
648652
timestamp=timestamp,

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ class PowerMetrics:
2929
"""Timestamp of the metrics."""
3030

3131
# pylint: disable=line-too-long
32-
inclusion_bounds: Bounds
32+
inclusion_bounds: Bounds | None
3333
"""Inclusion power bounds for all batteries in the battery pool instance.
3434
3535
This is the range within which power requests are allowed by the battery pool.
@@ -42,7 +42,7 @@ class PowerMetrics:
4242
details.
4343
"""
4444

45-
exclusion_bounds: Bounds
45+
exclusion_bounds: Bounds | None
4646
"""Exclusion power bounds for all batteries in the battery pool instance.
4747
4848
This is the range within which power requests are NOT allowed by the battery pool.

tests/timeseries/_battery_pool/test_battery_pool.py

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,9 @@ async def run_test_battery_status_channel( # pylint: disable=too-many-arguments
419419
all_pool_result: result metric if all batteries in pool are working
420420
only_first_battery_result: result metric if only first battery in pool is
421421
working
422+
423+
Raises:
424+
ValueError: If the received message is not an instance of PowerMetrics or Sample.
422425
"""
423426
assert len(batteries_in_pool) == 2
424427

@@ -438,7 +441,13 @@ async def run_test_battery_status_channel( # pylint: disable=too-many-arguments
438441
msg = await asyncio.wait_for(
439442
battery_pool_metric_receiver.receive(), timeout=waiting_time_sec
440443
)
441-
assert msg is None
444+
if isinstance(msg, PowerMetrics):
445+
assert msg.inclusion_bounds is None
446+
assert msg.exclusion_bounds is None
447+
elif isinstance(msg, Sample):
448+
assert msg.value is None
449+
else:
450+
raise ValueError("Expected an instance of PowerMetrics or Sample")
442451

443452
# One battery in uncertain state.
444453
await battery_status_sender.send(
@@ -662,7 +671,7 @@ async def run_capacity_test(setup_args: SetupArgs) -> None:
662671
await streamer.stop_streaming(batteries_in_pool[0])
663672
await asyncio.sleep(MAX_BATTERY_DATA_AGE_SEC + 0.2)
664673
msg = await asyncio.wait_for(capacity_receiver.receive(), timeout=waiting_time_sec)
665-
assert msg is None
674+
assert isinstance(msg, Sample) and msg.value is None
666675

667676
# One battery started sending data.
668677
latest_data = streamer.get_current_component_data(batteries_in_pool[0])
@@ -740,7 +749,7 @@ async def run_soc_test(setup_args: SetupArgs) -> None:
740749
Scenario(
741750
batteries_in_pool[1],
742751
{"soc": math.nan},
743-
None,
752+
Sample(now, None),
744753
),
745754
Scenario(
746755
batteries_in_pool[1],
@@ -751,7 +760,7 @@ async def run_soc_test(setup_args: SetupArgs) -> None:
751760
Scenario(
752761
batteries_in_pool[0],
753762
{"capacity": 0, "soc_lower_bound": 10.0, "soc_upper_bound": 100.0},
754-
None,
763+
Sample(now, None),
755764
wait_for_result=False,
756765
),
757766
# Test zero division error
@@ -795,7 +804,7 @@ async def run_soc_test(setup_args: SetupArgs) -> None:
795804
await streamer.stop_streaming(batteries_in_pool[0])
796805
await asyncio.sleep(MAX_BATTERY_DATA_AGE_SEC + 0.2)
797806
msg = await asyncio.wait_for(receiver.receive(), timeout=waiting_time_sec)
798-
assert msg is None
807+
assert isinstance(msg, Sample) and msg.value is None
799808

800809
# One battery started sending data.
801810
latest_data = streamer.get_current_component_data(batteries_in_pool[0])
@@ -885,7 +894,11 @@ async def run_power_bounds_test( # pylint: disable=too-many-locals
885894
"active_power_inclusion_upper_bound": 9000,
886895
"active_power_exclusion_upper_bound": 250,
887896
},
888-
expected_result=None,
897+
expected_result=PowerMetrics(
898+
now,
899+
None,
900+
None,
901+
),
889902
wait_for_result=False,
890903
),
891904
Scenario(
@@ -995,7 +1008,11 @@ async def run_power_bounds_test( # pylint: disable=too-many-locals
9951008
"power_inclusion_lower_bound": math.nan,
9961009
"power_inclusion_upper_bound": math.nan,
9971010
},
998-
None,
1011+
PowerMetrics(
1012+
now,
1013+
None,
1014+
None,
1015+
),
9991016
),
10001017
Scenario(
10011018
batteries_in_pool[0],
@@ -1121,7 +1138,11 @@ async def run_power_bounds_test( # pylint: disable=too-many-locals
11211138
await streamer.stop_streaming(bat_inv_map[batteries_in_pool[1]])
11221139
await asyncio.sleep(MAX_BATTERY_DATA_AGE_SEC + 0.2)
11231140
msg = await asyncio.wait_for(receiver.receive(), timeout=waiting_time_sec)
1124-
assert msg is None
1141+
assert (
1142+
isinstance(msg, PowerMetrics)
1143+
and msg.inclusion_bounds is None
1144+
and msg.exclusion_bounds is None
1145+
)
11251146

11261147
# One battery started sending data.
11271148
latest_data = streamer.get_current_component_data(batteries_in_pool[0])
@@ -1201,7 +1222,7 @@ async def run_temperature_test( # pylint: disable=too-many-locals
12011222
Scenario(
12021223
bat_1,
12031224
{"temperature": math.nan},
1204-
None,
1225+
Sample(now, None),
12051226
),
12061227
Scenario(
12071228
bat_0,
@@ -1238,7 +1259,7 @@ async def run_temperature_test( # pylint: disable=too-many-locals
12381259
await streamer.stop_streaming(bat_0)
12391260
await asyncio.sleep(MAX_BATTERY_DATA_AGE_SEC + 0.2)
12401261
msg = await asyncio.wait_for(receiver.receive(), timeout=waiting_time_sec)
1241-
assert msg is None
1262+
assert isinstance(msg, Sample) and msg.value is None
12421263

12431264
# one battery started sending data.
12441265
latest_data = streamer.get_current_component_data(bat_1)

0 commit comments

Comments
 (0)