2020from frequenz .channels .util import Timer , select , selected_from
2121from typing_extensions import override
2222
23- from ..._internal ._asyncio import cancel_and_await
24- from ...microgrid import connection_manager
25- from ...microgrid .component import (
23+ from ....microgrid import connection_manager
24+ from ....microgrid .component import (
2625 BatteryData ,
2726 ComponentCategory ,
2827 ComponentData ,
2928 InverterData ,
3029)
30+ from ..._background_service import BackgroundService
3131from ._component_status import (
3232 ComponentStatus ,
3333 ComponentStatusEnum ,
@@ -55,55 +55,54 @@ class _ComponentStreamStatus:
5555
5656@dataclass
5757class _BlockingStatus :
58- min_duration_sec : float
59- """The minimum blocking duration (in seconds) ."""
58+ min_duration : timedelta
59+ """The minimum blocking duration."""
6060
61- max_duration_sec : float
62- """The maximum blocking duration (in seconds) ."""
61+ max_duration : timedelta
62+ """The maximum blocking duration."""
6363
64- last_blocking_duration_sec : float = 0.0
65- """Last blocking duration (in seconds) ."""
64+ last_blocking_duration : timedelta = timedelta ( seconds = 0.0 )
65+ """Last blocking duration."""
6666
6767 blocked_until : datetime | None = None
6868 """Until when the battery is blocked."""
6969
7070 def __post_init__ (self ) -> None :
71- assert self .min_duration_sec <= self .max_duration_sec , (
72- f"Minimum blocking duration ({ self .min_duration_sec } ) cannot be greater "
73- f"than maximum blocking duration ({ self .max_duration_sec } )"
71+ assert self .min_duration <= self .max_duration , (
72+ f"Minimum blocking duration ({ self .min_duration } ) cannot be greater "
73+ f"than maximum blocking duration ({ self .max_duration } )"
7474 )
75- self .last_blocking_duration_sec = self .min_duration_sec
75+ self .last_blocking_duration = self .min_duration
76+ self ._timedelta_zero = timedelta (seconds = 0.0 )
7677
77- def block (self ) -> float :
78+ def block (self ) -> timedelta :
7879 """Block battery.
7980
8081 Battery can be unblocked using `self.unblock()` method.
8182
8283 Returns:
83- For how long (in seconds) the battery is blocked.
84+ The duration for which the battery is blocked.
8485 """
8586 now = datetime .now (tz = timezone .utc )
8687
8788 # If is not blocked
8889 if self .blocked_until is None :
89- self .last_blocking_duration_sec = self .min_duration_sec
90- self .blocked_until = now + timedelta (
91- seconds = self .last_blocking_duration_sec
92- )
93- return self .last_blocking_duration_sec
90+ self .last_blocking_duration = self .min_duration
91+ self .blocked_until = now + self .last_blocking_duration
92+ return self .last_blocking_duration
9493
9594 # If still blocked, then do nothing
9695 if self .blocked_until > now :
97- return 0.0
96+ return self . _timedelta_zero
9897
9998 # If previous blocking time expired, then blocked it once again.
10099 # Increase last blocking time, unless it reach the maximum.
101- self .last_blocking_duration_sec = min (
102- 2 * self .last_blocking_duration_sec , self .max_duration_sec
100+ self .last_blocking_duration = min (
101+ 2 * self .last_blocking_duration , self .max_duration
103102 )
104- self .blocked_until = now + timedelta ( seconds = self .last_blocking_duration_sec )
103+ self .blocked_until = now + self .last_blocking_duration
105104
106- return self .last_blocking_duration_sec
105+ return self .last_blocking_duration
107106
108107 def unblock (self ) -> None :
109108 """Unblock battery.
@@ -127,7 +126,7 @@ def is_blocked(self) -> bool:
127126 return self .blocked_until > datetime .now (tz = timezone .utc )
128127
129128
130- class BatteryStatusTracker (ComponentStatusTracker ):
129+ class BatteryStatusTracker (ComponentStatusTracker , BackgroundService ):
131130 """Class for tracking if battery is working.
132131
133132 Status updates are sent out only when there is a status change.
@@ -166,20 +165,19 @@ class BatteryStatusTracker(ComponentStatusTracker):
166165 def __init__ ( # pylint: disable=too-many-arguments
167166 self ,
168167 component_id : int ,
169- max_data_age_sec : float ,
170- max_blocking_duration_sec : float ,
168+ max_data_age : timedelta ,
169+ max_blocking_duration : timedelta ,
171170 status_sender : Sender [ComponentStatus ],
172171 set_power_result_receiver : Receiver [SetPowerResult ],
173172 ) -> None :
174173 """Create class instance.
175174
176175 Args:
177176 component_id: Id of this battery
178- max_data_age_sec: If component stopped sending data, then
179- this is the maximum time when its last message should be considered as
180- valid. After that time, component won't be used until it starts sending
181- data.
182- max_blocking_duration_sec: This value tell what should be the maximum
177+ max_data_age: If component stopped sending data, then this is the maximum
178+ time when its last message should be considered as valid. After that
179+ time, component won't be used until it starts sending data.
180+ max_blocking_duration: This value tell what should be the maximum
183181 timeout used for blocking failing component.
184182 status_sender: Channel to send status updates.
185183 set_power_result_receiver: Channel to receive results of the requests to the
@@ -188,13 +186,18 @@ def __init__( # pylint: disable=too-many-arguments
188186 Raises:
189187 RuntimeError: If battery has no adjacent inverter.
190188 """
191- self ._max_data_age = max_data_age_sec
189+ BackgroundService .__init__ (self , name = f"BatteryStatusTracker({ component_id } )" )
190+ self ._max_data_age = max_data_age
191+ self ._status_sender = status_sender
192+ self ._set_power_result_receiver = set_power_result_receiver
193+
192194 # First battery is considered as not working.
193195 # Change status after first messages are received.
194196 self ._last_status : ComponentStatusEnum = ComponentStatusEnum .NOT_WORKING
195197 self ._blocking_status : _BlockingStatus = _BlockingStatus (
196- 1.0 , max_blocking_duration_sec
198+ timedelta ( seconds = 1.0 ), max_blocking_duration
197199 )
200+ self ._timedelta_zero = timedelta (seconds = 0.0 )
198201
199202 inverter_id = self ._find_adjacent_inverter_id (component_id )
200203 if inverter_id is None :
@@ -204,17 +207,22 @@ def __init__( # pylint: disable=too-many-arguments
204207
205208 self ._battery : _ComponentStreamStatus = _ComponentStreamStatus (
206209 component_id ,
207- data_recv_timer = Timer .timeout (timedelta ( seconds = max_data_age_sec ) ),
210+ data_recv_timer = Timer .timeout (max_data_age ),
208211 )
209212 self ._inverter : _ComponentStreamStatus = _ComponentStreamStatus (
210213 inverter_id ,
211- data_recv_timer = Timer .timeout (timedelta ( seconds = max_data_age_sec ) ),
214+ data_recv_timer = Timer .timeout (max_data_age ),
212215 )
213216
214217 # Select needs receivers that can be get in async way only.
215218
216- self ._task : asyncio .Task [None ] = asyncio .create_task (
217- self ._run (status_sender , set_power_result_receiver )
219+ @override
220+ def start (self ) -> None :
221+ """Start the BatteryStatusTracker instance."""
222+ self ._tasks .add (
223+ asyncio .create_task (
224+ self ._run (self ._status_sender , self ._set_power_result_receiver )
225+ )
218226 )
219227
220228 @property
@@ -226,10 +234,6 @@ def battery_id(self) -> int:
226234 """
227235 return self ._battery .component_id
228236
229- async def stop (self ) -> None :
230- """Stop tracking battery status."""
231- await cancel_and_await (self ._task )
232-
233237 def _handle_status_battery (self , bat_data : BatteryData ) -> None :
234238 self ._battery .last_msg_correct = (
235239 self ._is_message_reliable (bat_data )
@@ -259,9 +263,9 @@ def _handle_status_set_power_result(self, result: SetPowerResult) -> None:
259263 ):
260264 duration = self ._blocking_status .block ()
261265
262- if duration > 0 :
266+ if duration > self . _timedelta_zero :
263267 _logger .warning (
264- "battery %d failed last response. block it for %f sec " ,
268+ "battery %d failed last response. block it for %s " ,
265269 self .battery_id ,
266270 duration ,
267271 )
@@ -345,7 +349,7 @@ async def _run(
345349 if (
346350 datetime .now (tz = timezone .utc )
347351 - self ._battery .last_msg_timestamp
348- ) < timedelta ( seconds = self ._max_data_age ) :
352+ ) < self ._max_data_age :
349353 # This means that we have received data from the battery
350354 # since the timer triggered, but the timer event arrived
351355 # late, so we can ignore it.
@@ -356,7 +360,7 @@ async def _run(
356360 if (
357361 datetime .now (tz = timezone .utc )
358362 - self ._inverter .last_msg_timestamp
359- ) < timedelta ( seconds = self ._max_data_age ) :
363+ ) < self ._max_data_age :
360364 # This means that we have received data from the inverter
361365 # since the timer triggered, but the timer event arrived
362366 # late, so we can ignore it.
@@ -505,7 +509,7 @@ def _is_timestamp_outdated(self, timestamp: datetime) -> bool:
505509 _True if timestamp is to old, False otherwise
506510 """
507511 now = datetime .now (tz = timezone .utc )
508- diff = ( now - timestamp ). total_seconds ()
512+ diff = now - timestamp
509513 return diff > self ._max_data_age
510514
511515 def _is_message_reliable (self , message : ComponentData ) -> bool :
0 commit comments