Skip to content

Commit 3f414ed

Browse files
committed
Merge draft implementation from christianparpart
From #485 Also resolve conflicts. Signed-off-by: Sahas Subramanian <[email protected]>
2 parents 64a33ce + 9dfb5a6 commit 3f414ed

File tree

8 files changed

+180
-6
lines changed

8 files changed

+180
-6
lines changed

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_MAX: lambda msg: msg.temperature_max,
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_MAX = "temperature_max"

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

@@ -401,6 +402,38 @@ def __call__(cls, *_args: Any, **_kwargs: Any) -> NoReturn:
401402
)
402403

403404

405+
class Temperature(
406+
Quantity,
407+
metaclass=_NoDefaultConstructible,
408+
exponent_unit_map={
409+
0: "°C",
410+
},
411+
):
412+
"""A temperature quantity (in degrees Celsius)."""
413+
414+
@classmethod
415+
def from_celsius(cls, value: float) -> Self:
416+
"""Initialize a new temperature quantity.
417+
418+
Args:
419+
value: The temperature in degrees Celsius.
420+
421+
Returns:
422+
A new temperature quantity.
423+
"""
424+
power = cls.__new__(cls)
425+
power._base_value = value
426+
return power
427+
428+
def as_celsius(self) -> float:
429+
"""Return the temperature in degrees Celsius.
430+
431+
Returns:
432+
The temperature in degrees Celsius.
433+
"""
434+
return self._base_value
435+
436+
404437
class Power(
405438
Quantity,
406439
metaclass=_NoDefaultConstructible,

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@
33

44
"""Manage a pool of batteries."""
55

6-
from ._result_types import Bound, PowerMetrics
6+
from ._result_types import Bound, PowerMetrics, TemperatureMetrics
77
from .battery_pool import BatteryPool
88

99
__all__ = [
1010
"BatteryPool",
1111
"PowerMetrics",
12+
"TemperatureMetrics",
1213
"Bound",
1314
]

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

Lines changed: 96 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@
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
17-
from ._result_types import Bound, PowerMetrics
17+
from ._result_types import Bound, PowerMetrics, TemperatureMetrics
1818

1919
_logger = logging.getLogger(__name__)
2020
_MIN_TIMESTAMP = datetime.min.replace(tzinfo=timezone.utc)
@@ -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, TemperatureMetrics)
6363

6464

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

236236

237+
class TemperatureCalculator(MetricCalculator[TemperatureMetrics]):
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_MAX,
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+
) -> TemperatureMetrics | 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
302+
temperature_min: float = float("inf")
303+
temperature_max: float = float("-inf")
304+
temperature_count: int = 0
305+
for battery_id in working_batteries:
306+
if battery_id not in metrics_data:
307+
continue
308+
metrics = metrics_data[battery_id]
309+
temperature = metrics.get(ComponentMetricId.TEMPERATURE_MAX)
310+
if temperature is None:
311+
continue
312+
timestamp = max(timestamp, metrics.timestamp)
313+
temperature_sum += temperature
314+
temperature_min = min(temperature_min, temperature)
315+
temperature_max = max(temperature_max, temperature)
316+
temperature_count += 1
317+
if timestamp == _MIN_TIMESTAMP:
318+
return None
319+
320+
temperature_avg = temperature_sum / temperature_count
321+
322+
return TemperatureMetrics(
323+
timestamp=timestamp,
324+
min=Temperature.from_celsius(value=temperature_min),
325+
avg=Temperature.from_celsius(value=temperature_avg),
326+
max=Temperature.from_celsius(value=temperature_max),
327+
)
328+
329+
237330
class SoCCalculator(MetricCalculator[Sample[Percentage]]):
238331
"""Define how to calculate SoC metrics."""
239332

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
from dataclasses import dataclass, field
77
from datetime import datetime
88

9+
from ...timeseries import Temperature
10+
911

1012
@dataclass
1113
class Bound:
@@ -61,3 +63,20 @@ class PowerMetrics:
6163
)
6264
```
6365
"""
66+
67+
68+
@dataclass
69+
class TemperatureMetrics:
70+
"""Container for temperature metrics."""
71+
72+
timestamp: datetime
73+
"""Timestamp of the metrics."""
74+
75+
max: Temperature
76+
"""Maximum temperature of the collection of temperatures."""
77+
78+
min: Temperature
79+
"""Minimum temperature of the collection of temperatures."""
80+
81+
avg: Temperature
82+
"""Average temperature of the collection of temperatures."""

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

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,13 @@
2929
)
3030
from .._quantities import Energy, Percentage, Power
3131
from ._methods import MetricAggregator, SendOnUpdate
32-
from ._metric_calculator import CapacityCalculator, PowerBoundsCalculator, SoCCalculator
33-
from ._result_types import PowerMetrics
32+
from ._metric_calculator import (
33+
CapacityCalculator,
34+
PowerBoundsCalculator,
35+
SoCCalculator,
36+
TemperatureCalculator,
37+
)
38+
from ._result_types import PowerMetrics, TemperatureMetrics
3439

3540

3641
class BatteryPool:
@@ -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[TemperatureMetrics]:
392+
"""Fetch the maximum temperature of the batteries in the pool.
393+
394+
Returns:
395+
A MetricAggregator that will calculate and stream the maximum 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)