Skip to content

Commit 3eae96b

Browse files
authored
Temperature streaming from the BatteryPool (#552)
Closes #466
2 parents 82084a6 + 759228e commit 3eae96b

File tree

11 files changed

+303
-10
lines changed

11 files changed

+303
-10
lines changed

RELEASE_NOTES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
* Add a new quantity class `Frequency` for frequency values.
2323

2424
- `FormulaEngine` arithmetics now supports scalar multiplication with floats and addition with Quantities
25+
- Add a new method for streaming average temperature values for the battery pool.
2526

2627
## Bug Fixes
2728

src/frequenz/sdk/actor/_data_sourcing/microgrid_api_source.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ def get_channel_name(self) -> str:
8484
ComponentMetricId.CAPACITY: lambda msg: msg.capacity,
8585
ComponentMetricId.POWER_LOWER_BOUND: lambda msg: msg.power_lower_bound,
8686
ComponentMetricId.POWER_UPPER_BOUND: lambda msg: msg.power_upper_bound,
87+
ComponentMetricId.TEMPERATURE: lambda msg: msg.temperature,
8788
}
8889

8990
_InverterDataMethods: Dict[ComponentMetricId, Callable[[InverterData], float]] = {

src/frequenz/sdk/microgrid/component/_component.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,3 +139,5 @@ class ComponentMetricId(Enum):
139139

140140
ACTIVE_POWER_LOWER_BOUND = "active_power_lower_bound"
141141
ACTIVE_POWER_UPPER_BOUND = "active_power_upper_bound"
142+
143+
TEMPERATURE = "temperature"

src/frequenz/sdk/microgrid/component/_component_data.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -141,8 +141,8 @@ class BatteryData(ComponentData):
141141
This will be a positive number, or zero if no charging is possible.
142142
"""
143143

144-
temperature_max: float
145-
"""The maximum temperature of all the blocks in a battery, in Celcius (°C)."""
144+
temperature: float
145+
"""The (average) temperature reported by the battery, in Celcius (°C)."""
146146

147147
_relay_state: battery_pb.RelayState.ValueType
148148
"""State of the battery relay."""
@@ -172,7 +172,7 @@ def from_proto(cls, raw: microgrid_pb.ComponentData) -> BatteryData:
172172
capacity=raw.battery.properties.capacity,
173173
power_lower_bound=raw.battery.data.dc.power.system_bounds.lower,
174174
power_upper_bound=raw.battery.data.dc.power.system_bounds.upper,
175-
temperature_max=raw.battery.data.temperature.max,
175+
temperature=raw.battery.data.temperature.avg,
176176
_relay_state=raw.battery.state.relay_state,
177177
_component_state=raw.battery.state.component_state,
178178
_errors=list(raw.battery.errors),

src/frequenz/sdk/timeseries/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
Percentage,
4646
Power,
4747
Quantity,
48+
Temperature,
4849
Voltage,
4950
)
5051
from ._resampling import ResamplerConfig
@@ -63,6 +64,7 @@
6364
"Current",
6465
"Energy",
6566
"Power",
67+
"Temperature",
6668
"Voltage",
6769
"Frequency",
6870
"Percentage",

src/frequenz/sdk/timeseries/_quantities.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"Energy",
2121
"Frequency",
2222
"Percentage",
23+
"Temperature",
2324
)
2425

2526

@@ -386,6 +387,38 @@ def __call__(cls, *_args: Any, **_kwargs: Any) -> NoReturn:
386387
)
387388

388389

390+
class Temperature(
391+
Quantity,
392+
metaclass=_NoDefaultConstructible,
393+
exponent_unit_map={
394+
0: "°C",
395+
},
396+
):
397+
"""A temperature quantity (in degrees Celsius)."""
398+
399+
@classmethod
400+
def from_celsius(cls, value: float) -> Self:
401+
"""Initialize a new temperature quantity.
402+
403+
Args:
404+
value: The temperature in degrees Celsius.
405+
406+
Returns:
407+
A new temperature quantity.
408+
"""
409+
power = cls.__new__(cls)
410+
power._base_value = value
411+
return power
412+
413+
def as_celsius(self) -> float:
414+
"""Return the temperature in degrees Celsius.
415+
416+
Returns:
417+
The temperature in degrees Celsius.
418+
"""
419+
return self._base_value
420+
421+
389422
class Power(
390423
Quantity,
391424
metaclass=_NoDefaultConstructible,

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

Lines changed: 89 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
from ...microgrid import connection_manager
1414
from ...microgrid.component import ComponentCategory, ComponentMetricId, InverterType
15-
from ...timeseries import Energy, Percentage, Sample
15+
from ...timeseries import Energy, Percentage, Sample, Temperature
1616
from ._component_metrics import ComponentMetricsData
1717
from ._result_types import Bound, PowerMetrics
1818

@@ -59,7 +59,7 @@ def battery_inverter_mapping(batteries: Iterable[int]) -> dict[int, int]:
5959

6060
# Formula output types class have no common interface
6161
# Print all possible types here.
62-
T = TypeVar("T", Sample[Percentage], Sample[Energy], PowerMetrics)
62+
T = TypeVar("T", Sample[Percentage], Sample[Energy], PowerMetrics, Sample[Temperature])
6363

6464

6565
class MetricCalculator(ABC, Generic[T]):
@@ -234,6 +234,93 @@ def calculate(
234234
)
235235

236236

237+
class TemperatureCalculator(MetricCalculator[Sample[Temperature]]):
238+
"""Define how to calculate temperature metrics."""
239+
240+
def __init__(self, batteries: Set[int]) -> None:
241+
"""Create class instance.
242+
243+
Args:
244+
batteries: What batteries should be used for calculation.
245+
"""
246+
super().__init__(batteries)
247+
248+
self._metrics = [
249+
ComponentMetricId.TEMPERATURE,
250+
]
251+
252+
@classmethod
253+
def name(cls) -> str:
254+
"""Return name of the calculator.
255+
256+
Returns:
257+
Name of the calculator
258+
"""
259+
return "temperature"
260+
261+
@property
262+
def battery_metrics(self) -> Mapping[int, list[ComponentMetricId]]:
263+
"""Return what metrics are needed for each battery.
264+
265+
Returns:
266+
Map between battery id and set of required metrics id.
267+
"""
268+
return {bid: self._metrics for bid in self._batteries}
269+
270+
@property
271+
def inverter_metrics(self) -> Mapping[int, list[ComponentMetricId]]:
272+
"""Return what metrics are needed for each inverter.
273+
274+
Returns:
275+
Map between inverter id and set of required metrics id.
276+
"""
277+
return {}
278+
279+
def calculate(
280+
self,
281+
metrics_data: dict[int, ComponentMetricsData],
282+
working_batteries: set[int],
283+
) -> Sample[Temperature] | None:
284+
"""Aggregate the metrics_data and calculate high level metric for temperature.
285+
286+
Missing components will be ignored. Formula will be calculated for all
287+
working batteries that are in metrics_data.
288+
289+
Args:
290+
metrics_data: Components metrics data, that should be used to calculate the
291+
result.
292+
working_batteries: working batteries. These batteries will be used
293+
to calculate the result. It should be subset of the batteries given in a
294+
constructor.
295+
296+
Returns:
297+
High level metric calculated from the given metrics.
298+
Return None if there are no component metrics.
299+
"""
300+
timestamp = _MIN_TIMESTAMP
301+
temperature_sum: float = 0.0
302+
temperature_count: int = 0
303+
for battery_id in working_batteries:
304+
if battery_id not in metrics_data:
305+
continue
306+
metrics = metrics_data[battery_id]
307+
temperature = metrics.get(ComponentMetricId.TEMPERATURE)
308+
if temperature is None:
309+
continue
310+
timestamp = max(timestamp, metrics.timestamp)
311+
temperature_sum += temperature
312+
temperature_count += 1
313+
if timestamp == _MIN_TIMESTAMP:
314+
return None
315+
316+
temperature_avg = temperature_sum / temperature_count
317+
318+
return Sample[Temperature](
319+
timestamp=timestamp,
320+
value=Temperature.from_celsius(value=temperature_avg),
321+
)
322+
323+
237324
class SoCCalculator(MetricCalculator[Sample[Percentage]]):
238325
"""Define how to calculate SoC metrics."""
239326

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

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,14 @@
2727
FormulaGeneratorConfig,
2828
FormulaType,
2929
)
30-
from .._quantities import Energy, Percentage, Power
30+
from .._quantities import Energy, Percentage, Power, Temperature
3131
from ._methods import MetricAggregator, SendOnUpdate
32-
from ._metric_calculator import CapacityCalculator, PowerBoundsCalculator, SoCCalculator
32+
from ._metric_calculator import (
33+
CapacityCalculator,
34+
PowerBoundsCalculator,
35+
SoCCalculator,
36+
TemperatureCalculator,
37+
)
3338
from ._result_types import PowerMetrics
3439

3540

@@ -382,6 +387,24 @@ def soc(self) -> MetricAggregator[Sample[Percentage]]:
382387

383388
return self._active_methods[method_name]
384389

390+
@property
391+
def temperature(self) -> MetricAggregator[Sample[Temperature]]:
392+
"""Fetch the average temperature of the batteries in the pool.
393+
394+
Returns:
395+
A MetricAggregator that will calculate and stream the average temperature
396+
of all batteries in the pool.
397+
"""
398+
method_name = SendOnUpdate.name() + "_" + TemperatureCalculator.name()
399+
if method_name not in self._active_methods:
400+
calculator = TemperatureCalculator(self._batteries)
401+
self._active_methods[method_name] = SendOnUpdate(
402+
metric_calculator=calculator,
403+
working_batteries=self._working_batteries,
404+
min_update_interval=self._min_update_interval,
405+
)
406+
return self._active_methods[method_name]
407+
385408
@property
386409
def capacity(self) -> MetricAggregator[Sample[Energy]]:
387410
"""Get receiver to receive new capacity metrics when they change.

0 commit comments

Comments
 (0)