Skip to content

Commit 677fbbc

Browse files
committed
Dispatch: Add running, until and next_run methods
Signed-off-by: Mathias L. Baumann <[email protected]>
1 parent 377a20b commit 677fbbc

File tree

2 files changed

+133
-3
lines changed

2 files changed

+133
-3
lines changed

RELEASE_NOTES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## New Features
44

55
* Update BaseApiClient to get the http2 keepalive feature.
6+
* Some Methods from the high-level API have been moved to this repo: The dispatch class now offers: `until`, `running`, `next_run` and `next_run_after`.
67

78
## Bug Fixes
89

src/frequenz/client/dispatch/types.py

Lines changed: 132 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55

66

77
from dataclasses import dataclass
8-
from datetime import datetime, timedelta
9-
from enum import IntEnum
8+
from datetime import datetime, timedelta, timezone
9+
from enum import Enum, IntEnum
1010
from typing import Any, cast
1111

1212
# pylint: disable=no-name-in-module
@@ -27,7 +27,7 @@
2727
# pylint: enable=no-name-in-module
2828
from frequenz.client.common.microgrid.components import ComponentCategory
2929

30-
from .recurrence import RecurrenceRule
30+
from .recurrence import Frequency, RecurrenceRule, Weekday
3131

3232
ComponentSelector = list[int] | list[ComponentCategory]
3333
"""A component selector specifying which components a dispatch targets.
@@ -115,6 +115,19 @@ class TimeIntervalFilter:
115115
"""Filter by end_time < end_to."""
116116

117117

118+
class RunningState(Enum):
119+
"""The running state of a dispatch."""
120+
121+
RUNNING = "RUNNING"
122+
"""The dispatch is running."""
123+
124+
STOPPED = "STOPPED"
125+
"""The dispatch is stopped."""
126+
127+
DIFFERENT_TYPE = "DIFFERENT_TYPE"
128+
"""The dispatch is for a different type."""
129+
130+
118131
@dataclass(kw_only=True, frozen=True)
119132
class Dispatch: # pylint: disable=too-many-instance-attributes
120133
"""Represents a dispatch operation within a microgrid system."""
@@ -160,6 +173,122 @@ class Dispatch: # pylint: disable=too-many-instance-attributes
160173
update_time: datetime
161174
"""The last update time of the dispatch in UTC. Set when a dispatch is modified."""
162175

176+
def running(self, type_: str) -> RunningState:
177+
"""Check if the dispatch is currently supposed to be running.
178+
179+
Args:
180+
type_: The type of the dispatch that should be running.
181+
182+
Returns:
183+
RUNNING if the dispatch is running,
184+
STOPPED if it is stopped,
185+
DIFFERENT_TYPE if it is for a different type.
186+
"""
187+
if self.type != type_:
188+
return RunningState.DIFFERENT_TYPE
189+
190+
if not self.active:
191+
return RunningState.STOPPED
192+
193+
now = datetime.now(tz=timezone.utc)
194+
195+
if now < self.start_time:
196+
return RunningState.STOPPED
197+
198+
# A dispatch without duration is always running, once it started
199+
if self.duration is None:
200+
return RunningState.RUNNING
201+
202+
if until := self._until(now):
203+
return RunningState.RUNNING if now < until else RunningState.STOPPED
204+
205+
return RunningState.STOPPED
206+
207+
@property
208+
def until(self) -> datetime | None:
209+
"""Time when the dispatch should end.
210+
211+
Returns the time that a running dispatch should end.
212+
If the dispatch is not running, None is returned.
213+
214+
Returns:
215+
The time when the dispatch should end or None if the dispatch is not running.
216+
"""
217+
if not self.active:
218+
return None
219+
220+
now = datetime.now(tz=timezone.utc)
221+
return self._until(now)
222+
223+
@property
224+
def next_run(self) -> datetime | None:
225+
"""Calculate the next run of a dispatch.
226+
227+
Returns:
228+
The next run of the dispatch or None if the dispatch is finished.
229+
"""
230+
return self.next_run_after(datetime.now(tz=timezone.utc))
231+
232+
def next_run_after(self, after: datetime) -> datetime | None:
233+
"""Calculate the next run of a dispatch.
234+
235+
Args:
236+
after: The time to calculate the next run from.
237+
238+
Returns:
239+
The next run of the dispatch or None if the dispatch is finished.
240+
"""
241+
if (
242+
not self.recurrence.frequency
243+
or self.recurrence.frequency == Frequency.UNSPECIFIED
244+
or self.duration is None # Infinite duration
245+
):
246+
if after > self.start_time:
247+
return None
248+
return self.start_time
249+
250+
# Make sure no weekday is UNSPECIFIED
251+
if Weekday.UNSPECIFIED in self.recurrence.byweekdays:
252+
return None
253+
254+
# No type information for rrule, so we need to cast
255+
return cast(
256+
datetime | None,
257+
self.recurrence.prepare(self.start_time).after(after, inc=True),
258+
)
259+
260+
def _until(self, now: datetime) -> datetime | None:
261+
"""Calculate the time when the dispatch should end.
262+
263+
If no previous run is found, None is returned.
264+
265+
Args:
266+
now: The current time.
267+
268+
Returns:
269+
The time when the dispatch should end or None if the dispatch is not running.
270+
271+
Raises:
272+
ValueError: If the dispatch has no duration.
273+
"""
274+
if self.duration is None:
275+
raise ValueError("_until: Dispatch has no duration")
276+
277+
if (
278+
not self.recurrence.frequency
279+
or self.recurrence.frequency == Frequency.UNSPECIFIED
280+
):
281+
return self.start_time + self.duration
282+
283+
latest_past_start: datetime | None = self.recurrence.prepare(
284+
self.start_time
285+
).before(now, inc=True)
286+
287+
if not latest_past_start:
288+
return None
289+
290+
return latest_past_start + self.duration
291+
163292
@classmethod
164293
def from_protobuf(cls, pb_object: PBDispatch) -> "Dispatch":
165294
"""Convert a protobuf dispatch to a dispatch.

0 commit comments

Comments
 (0)