Skip to content

Commit 34fb5a5

Browse files
Make StatusTracker AsyncConstructible
Signed-off-by: ela-kotulska-frequenz <[email protected]>
1 parent c308c0d commit 34fb5a5

File tree

2 files changed

+62
-54
lines changed

2 files changed

+62
-54
lines changed

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

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -45,19 +45,22 @@ async def async_new(
4545
max_blocking_duration_sec: This value tell what should be the maximum
4646
timeout used for blocking failing component.
4747
48+
Raises:
49+
RuntimeError: If any battery has no adjacent inverter.
50+
4851
Returns:
4952
New instance of this class.
5053
"""
5154
self: BatteryPoolStatus = BatteryPoolStatus.__new__(cls)
52-
self._batteries = {
53-
id: StatusTracker(id, max_data_age_sec, max_blocking_duration_sec)
55+
56+
tasks = [
57+
StatusTracker.async_new(id, max_data_age_sec, max_blocking_duration_sec)
5458
for id in battery_ids
55-
}
59+
]
60+
61+
trackers = await asyncio.gather(*tasks)
62+
self._batteries = {tracker.battery_id: tracker for tracker in trackers}
5663

57-
await asyncio.gather(
58-
*[bat.async_init() for bat in self._batteries.values()],
59-
return_exceptions=True,
60-
)
6164
return self
6265

6366
def get_working_batteries(self, battery_ids: Set[int]) -> Set[int]:

src/frequenz/sdk/microgrid/_battery/_status.py

Lines changed: 52 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
# Copyright © 2022 Frequenz Energy-as-a-Service GmbH
33
"""Class to return battery status."""
44

5+
from __future__ import annotations
6+
57
import asyncio
68
import logging
79
from dataclasses import dataclass
@@ -15,7 +17,7 @@
1517
from frequenz.api.microgrid.inverter_pb2 import ComponentState as InverterComponentState
1618
from frequenz.channels import Peekable
1719

18-
from .. import ComponentGraph
20+
from ..._internal.asyncio import AsyncConstructible
1921
from .. import get as get_microgrid
2022
from ..component import BatteryData, ComponentCategory, ComponentData, InverterData
2123

@@ -46,9 +48,15 @@ class _ComponentReceiver(Generic[T]):
4648
receiver: Peekable[T]
4749

4850

49-
class StatusTracker:
50-
"""Class for tracking if battery is working."""
51+
class StatusTracker(AsyncConstructible):
52+
"""Class for tracking if battery is working.
53+
54+
To create an instance of this class you should use `async_new` class method.
55+
Standard constructor (__init__) is not supported and using it will raise
56+
`NotSyncConstructible` error.
57+
"""
5158

59+
# Class attributes
5260
_battery_valid_relay: Set[BatteryRelayState.ValueType] = {
5361
BatteryRelayState.RELAY_STATE_CLOSED
5462
}
@@ -64,9 +72,21 @@ class StatusTracker:
6472
InverterComponentState.COMPONENT_STATE_STANDBY,
6573
}
6674

67-
def __init__(
68-
self, battery_id: int, max_data_age_sec: float, max_blocking_duration_sec: float
69-
) -> None:
75+
# Instance attributes
76+
_battery_id: int
77+
_max_data_age: float
78+
_last_status: BatteryStatus
79+
_min_blocking_duration_sec: float
80+
_max_blocking_duration_sec: float
81+
_blocked_until: Optional[datetime]
82+
_last_blocking_duration_sec: float
83+
_battery_receiver: _ComponentReceiver[BatteryData]
84+
_inverter_receiver: _ComponentReceiver[InverterData]
85+
86+
@classmethod
87+
async def async_new(
88+
cls, battery_id: int, max_data_age_sec: float, max_blocking_duration_sec: float
89+
) -> StatusTracker:
7090
"""Create class instance.
7191
7292
Args:
@@ -77,37 +97,25 @@ def __init__(
7797
data.
7898
max_blocking_duration_sec: This value tell what should be the maximum
7999
timeout used for blocking failing component.
80-
"""
81-
self._battery_id: int = battery_id
82-
self._max_data_age: float = max_data_age_sec
83-
84-
self._init_method_called: bool = False
85-
self._last_status: BatteryStatus = BatteryStatus.WORKING
86-
87-
self._min_blocking_duration_sec: float = 1.0
88-
self._max_blocking_duration_sec: float = max_blocking_duration_sec
89-
self._blocked_until: Optional[datetime] = None
90-
self._last_blocking_duration_sec: float = self._min_blocking_duration_sec
91-
92-
@property
93-
def battery_id(self) -> int:
94-
"""Get battery id.
95100
96101
Returns:
97-
Battery id
102+
New instance of this class.
103+
104+
Raises:
105+
RuntimeError: If battery has no adjacent inverter.
98106
"""
99-
return self._battery_id
107+
self: StatusTracker = StatusTracker.__new__(cls)
108+
self._battery_id = battery_id
109+
self._max_data_age = max_data_age_sec
100110

101-
async def async_init(self) -> None:
102-
"""Async part of constructor.
111+
self._last_status = BatteryStatus.WORKING
103112

104-
This should be called in order to subscribe for necessary data.
113+
self._min_blocking_duration_sec = 1.0
114+
self._max_blocking_duration_sec = max_blocking_duration_sec
115+
self._blocked_until = None
116+
self._last_blocking_duration_sec = self._min_blocking_duration_sec
105117

106-
Raises:
107-
RuntimeError: If given battery as no adjacent inverter.
108-
"""
109-
component_graph = get_microgrid().component_graph
110-
inverter_id = self._find_adjacent_inverter_id(component_graph)
118+
inverter_id = self._find_adjacent_inverter_id()
111119
if inverter_id is None:
112120
raise RuntimeError(
113121
f"Can't find inverter adjacent to battery: {self._battery_id}"
@@ -116,22 +124,26 @@ async def async_init(self) -> None:
116124
api_client = get_microgrid().api_client
117125

118126
bat_recv = await api_client.battery_data(self._battery_id)
119-
# Defining battery_receiver and inverter receiver needs await.
120-
# Because of that it is impossible to define it in constructor.
121-
# Setting it to None first would require to check None condition in
122-
# every call.
123-
# pylint: disable=attribute-defined-outside-init
124127
self._battery_receiver = _ComponentReceiver(
125128
self._battery_id, bat_recv.into_peekable()
126129
)
130+
127131
inv_recv = await api_client.inverter_data(inverter_id)
128-
# pylint: disable=attribute-defined-outside-init
129132
self._inverter_receiver = _ComponentReceiver(
130133
inverter_id, inv_recv.into_peekable()
131134
)
132135

133136
await asyncio.sleep(_START_DELAY_SEC)
134-
self._init_method_called = True
137+
return self
138+
139+
@property
140+
def battery_id(self) -> int:
141+
"""Get battery id.
142+
143+
Returns:
144+
Battery id
145+
"""
146+
return self._battery_id
135147

136148
def get_status(self) -> BatteryStatus:
137149
"""Return status of the battery.
@@ -145,11 +157,6 @@ def get_status(self) -> BatteryStatus:
145157
Returns:
146158
Battery status
147159
"""
148-
if not self._init_method_called:
149-
raise RuntimeError(
150-
"`async_init` method not called or not awaited. Run it before first use"
151-
)
152-
153160
bat_msg = self._battery_receiver.receiver.peek()
154161
inv_msg = self._inverter_receiver.receiver.peek()
155162
if bat_msg is None or inv_msg is None:
@@ -353,15 +360,13 @@ def _is_message_reliable(self, message: ComponentData) -> bool:
353360

354361
return not is_outdated
355362

356-
def _find_adjacent_inverter_id(self, graph: ComponentGraph) -> Optional[int]:
363+
def _find_adjacent_inverter_id(self) -> Optional[int]:
357364
"""Find inverter adjacent to this battery.
358365
359-
Args:
360-
graph: Component graph
361-
362366
Returns:
363367
Id of the inverter. If battery hasn't adjacent inverter, then return None.
364368
"""
369+
graph = get_microgrid().component_graph
365370
return next(
366371
(
367372
comp.component_id

0 commit comments

Comments
 (0)