Skip to content

Commit 042e76b

Browse files
committed
Use generic ComponentStatusTracker in ComponentPoolStatusTracker
This allows us to completely decouple `ComponentPoolStatusTracker` from anything specific to batteries. And when a `ComponentPoolStatusTracker` instance is created, users can pass a specialized class that implements `ComponentStatusTracker`, like the `BatteryStatusTracker`. Signed-off-by: Sahas Subramanian <[email protected]>
1 parent de05503 commit 042e76b

File tree

5 files changed

+58
-10
lines changed

5 files changed

+58
-10
lines changed

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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
# pylint: enable=no-name-in-module
1919
from frequenz.channels import Receiver, Sender
2020
from frequenz.channels.util import Timer, select, selected_from
21+
from typing_extensions import override
2122

2223
from ..._internal._asyncio import cancel_and_await
2324
from ...microgrid import connection_manager
@@ -27,7 +28,12 @@
2728
ComponentData,
2829
InverterData,
2930
)
30-
from ._component_status import ComponentStatus, ComponentStatusEnum, SetPowerResult
31+
from ._component_status import (
32+
ComponentStatus,
33+
ComponentStatusEnum,
34+
ComponentStatusTracker,
35+
SetPowerResult,
36+
)
3137

3238
_logger = logging.getLogger(__name__)
3339

@@ -121,7 +127,7 @@ def is_blocked(self) -> bool:
121127
return self.blocked_until > datetime.now(tz=timezone.utc)
122128

123129

124-
class BatteryStatusTracker:
130+
class BatteryStatusTracker(ComponentStatusTracker):
125131
"""Class for tracking if battery is working.
126132
127133
Status updates are sent out only when there is a status change.
@@ -156,6 +162,7 @@ class BatteryStatusTracker:
156162
A working inverter in any other inverter state will be reported as failing.
157163
"""
158164

165+
@override
159166
def __init__( # pylint: disable=too-many-arguments
160167
self,
161168
component_id: int,

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

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@
1212
from frequenz.channels.util import Merge
1313

1414
from ..._internal._asyncio import cancel_and_await
15-
from ._battery_status_tracker import BatteryStatusTracker
1615
from ._component_status import (
1716
ComponentPoolStatus,
1817
ComponentStatus,
1918
ComponentStatusEnum,
19+
ComponentStatusTracker,
2020
SetPowerResult,
2121
)
2222

@@ -30,12 +30,13 @@ class ComponentPoolStatusTracker:
3030
components changes.
3131
"""
3232

33-
def __init__( # noqa: DOC502 (RuntimeError raised from BatteryStatusTracker)
33+
def __init__( # pylint: disable=too-many-arguments
3434
self,
3535
component_ids: set[int],
3636
component_status_sender: Sender[ComponentPoolStatus],
3737
max_data_age_sec: float,
3838
max_blocking_duration_sec: float,
39+
component_status_tracker_type: type[ComponentStatusTracker],
3940
) -> None:
4041
"""Create ComponentPoolStatusTracker instance.
4142
@@ -49,15 +50,14 @@ def __init__( # noqa: DOC502 (RuntimeError raised from BatteryStatusTracker)
4950
sending data.
5051
max_blocking_duration_sec: This value tell what should be the maximum
5152
timeout used for blocking failing component.
52-
53-
Raises:
54-
RuntimeError: When managing batteries, if any battery has no adjacent
55-
inverter.
53+
component_status_tracker_type: component status tracker to use
54+
for tracking the status of the components.
5655
"""
5756
self._component_ids = component_ids
5857
self._max_data_age_sec = max_data_age_sec
5958
self._max_blocking_duration_sec = max_blocking_duration_sec
6059
self._component_status_sender = component_status_sender
60+
self._component_status_tracker_type = component_status_tracker_type
6161

6262
# At first no component is working, we will get notification when they start
6363
# working.
@@ -68,7 +68,7 @@ def __init__( # noqa: DOC502 (RuntimeError raised from BatteryStatusTracker)
6868
"component_request_status"
6969
)
7070
self._set_power_result_sender = self._set_power_result_channel.new_sender()
71-
self._component_status_trackers: list[BatteryStatusTracker] = []
71+
self._component_status_trackers: list[ComponentStatusTracker] = []
7272
self._merged_status_receiver = self._make_merged_status_receiver()
7373

7474
self._task = asyncio.create_task(self._run())
@@ -97,7 +97,7 @@ def _make_merged_status_receiver(
9797
channel: Broadcast[ComponentStatus] = Broadcast(
9898
f"component_{component_id}_status"
9999
)
100-
tracker = BatteryStatusTracker(
100+
tracker = self._component_status_tracker_type(
101101
component_id=component_id,
102102
max_data_age_sec=self._max_data_age_sec,
103103
max_blocking_duration_sec=self._max_blocking_duration_sec,

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

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,12 @@
66

77
import enum
88
import typing
9+
from abc import ABC, abstractmethod
910
from collections import abc
1011
from dataclasses import dataclass
1112

13+
from frequenz.channels import Receiver, Sender
14+
1215

1316
@dataclass
1417
class ComponentPoolStatus:
@@ -72,3 +75,35 @@ class SetPowerResult:
7275

7376
failed: typing.Iterable[int]
7477
"""Component IDs for which the last set power command failed."""
78+
79+
80+
class ComponentStatusTracker(ABC):
81+
"""Interface for specialized component status trackers to implement."""
82+
83+
@abstractmethod
84+
def __init__( # pylint: disable=too-many-arguments
85+
self,
86+
component_id: int,
87+
max_data_age_sec: float,
88+
max_blocking_duration_sec: float,
89+
status_sender: Sender[ComponentStatus],
90+
set_power_result_receiver: Receiver[SetPowerResult],
91+
) -> None:
92+
"""Create class instance.
93+
94+
Args:
95+
component_id: Id of this battery
96+
max_data_age_sec: If component stopped sending data, then
97+
this is the maximum time when its last message should be considered as
98+
valid. After that time, component won't be used until it starts sending
99+
data.
100+
max_blocking_duration_sec: This value tell what should be the maximum
101+
timeout used for blocking failing component.
102+
status_sender: Channel to send status updates.
103+
set_power_result_receiver: Channel to receive results of the requests to the
104+
components.
105+
"""
106+
107+
@abstractmethod
108+
async def stop(self) -> None:
109+
"""Stop tracking battery status."""

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
ComponentCategory,
3737
InverterData,
3838
)
39+
from ._battery_status_tracker import BatteryStatusTracker
3940
from ._component_pool_status_tracker import ComponentPoolStatusTracker
4041
from ._component_status import ComponentPoolStatus
4142
from ._distribution_algorithm import (
@@ -223,6 +224,7 @@ def __init__(
223224
component_status_sender=battery_status_sender,
224225
max_blocking_duration_sec=30.0,
225226
max_data_age_sec=10.0,
227+
component_status_tracker_type=BatteryStatusTracker,
226228
)
227229

228230
def _get_bounds(

tests/actor/test_battery_pool_status.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
from frequenz.channels import Broadcast
88
from pytest_mock import MockerFixture
99

10+
from frequenz.sdk.actor.power_distributing._battery_status_tracker import (
11+
BatteryStatusTracker,
12+
)
1013
from frequenz.sdk.actor.power_distributing._component_pool_status_tracker import (
1114
ComponentPoolStatusTracker,
1215
)
@@ -46,6 +49,7 @@ async def test_batteries_status(self, mocker: MockerFixture) -> None:
4649
component_status_sender=battery_status_channel.new_sender(),
4750
max_data_age_sec=5,
4851
max_blocking_duration_sec=30,
52+
component_status_tracker_type=BatteryStatusTracker,
4953
)
5054
await asyncio.sleep(0.1)
5155

0 commit comments

Comments
 (0)