Skip to content

Commit e7a9626

Browse files
authored
Merge pull request #2518 from cebtenzzre/fix-scheduler-offsets
scheduler: improve handling of offsets
2 parents da0ab2e + da11625 commit e7a9626

File tree

12 files changed

+311
-124
lines changed

12 files changed

+311
-124
lines changed

appdaemon/adapi.py

Lines changed: 65 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from appdaemon.models.config.app import AppConfig
2525
from appdaemon.parse import resolve_time_str
2626
from appdaemon.state import StateCallbackType
27+
from .types import TimeDeltaLike
2728

2829
T = TypeVar("T")
2930

@@ -1413,9 +1414,9 @@ async def listen_state(
14131414
namespace: str | None = None,
14141415
new: str | Callable[[Any], bool] | None = None,
14151416
old: str | Callable[[Any], bool] | None = None,
1416-
duration: str | int | float | timedelta | None = None,
1417+
duration: TimeDeltaLike | None = None,
14171418
attribute: str | None = None,
1418-
timeout: str | int | float | timedelta | None = None,
1419+
timeout: TimeDeltaLike | None = None,
14191420
immediate: bool = False,
14201421
oneshot: bool = False,
14211422
pin: bool | None = None,
@@ -1432,9 +1433,9 @@ async def listen_state(
14321433
namespace: str | None = None,
14331434
new: str | Callable[[Any], bool] | None = None,
14341435
old: str | Callable[[Any], bool] | None = None,
1435-
duration: str | int | float | timedelta | None = None,
1436+
duration: TimeDeltaLike | None = None,
14361437
attribute: str | None = None,
1437-
timeout: str | int | float | timedelta | None = None,
1438+
timeout: TimeDeltaLike | None = None,
14381439
immediate: bool = False,
14391440
oneshot: bool = False,
14401441
pin: bool | None = None,
@@ -1450,9 +1451,9 @@ async def listen_state(
14501451
namespace: str | None = None,
14511452
new: str | Callable[[Any], bool] | None = None,
14521453
old: str | Callable[[Any], bool] | None = None,
1453-
duration: str | int | float | timedelta | None = None,
1454+
duration: TimeDeltaLike | None = None,
14541455
attribute: str | None = None,
1455-
timeout: str | int | float | timedelta | None = None,
1456+
timeout: TimeDeltaLike | None = None,
14561457
immediate: bool = False,
14571458
oneshot: bool = False,
14581459
pin: bool | None = None,
@@ -1486,7 +1487,7 @@ async def listen_state(
14861487
careful when comparing them. The ``self.get_state()`` method is useful for checking the data type of
14871488
the desired attribute. If ``old`` is a callable (lambda, function, etc), then it will be called with
14881489
the old state, and the callback will only be invoked if the callable returns ``True``.
1489-
duration (str | int | float | timedelta, optional): If supplied, the callback will not be invoked unless the
1490+
duration (TimeDeltaLike, optional): If supplied, the callback will not be invoked unless the
14901491
desired state is maintained for that amount of time. This requires that a specific attribute is
14911492
specified (or the default of ``state`` is used), and should be used in conjunction with either or both
14921493
of the ``new`` and ``old`` parameters. When the callback is called, it is supplied with the values of
@@ -1500,7 +1501,7 @@ async def listen_state(
15001501
the default behavior is to use the value of ``state``. Using the value ``all`` will cause the callback
15011502
to get triggered for any change in state, and the new/old values used for the callback will be the
15021503
entire state dict rather than the individual value of an attribute.
1503-
timeout (str | int | float | timedelta, optional): If given, the callback will be automatically removed
1504+
timeout (TimeDeltaLike, optional): If given, the callback will be automatically removed
15041505
after that amount of time. If activity for the listened state has occurred that would trigger a
15051506
duration timer, the duration timer will still be fired even though the callback has been removed.
15061507
immediate (bool, optional): If given, it enables the countdown for a delay parameter to start at the time.
@@ -2102,7 +2103,7 @@ async def listen_event(
21022103
event: str | None = None,
21032104
*,
21042105
namespace: str | None = None,
2105-
timeout: str | int | float | timedelta | None = None,
2106+
timeout: TimeDeltaLike | None = None,
21062107
oneshot: bool = False,
21072108
pin: bool | None = None,
21082109
pin_thread: int | None = None,
@@ -2117,7 +2118,7 @@ async def listen_event(
21172118
event: list[str],
21182119
*,
21192120
namespace: str | None = None,
2120-
timeout: str | int | float | timedelta | None = None,
2121+
timeout: TimeDeltaLike | None = None,
21212122
oneshot: bool = False,
21222123
pin: bool | None = None,
21232124
pin_thread: int | None = None,
@@ -2131,7 +2132,7 @@ async def listen_event(
21312132
event: str | Iterable[str] | None = None,
21322133
*, # Arguments after this are keyword only
21332134
namespace: str | Literal["global"] | None = None,
2134-
timeout: str | int | float | timedelta | None = None,
2135+
timeout: TimeDeltaLike | None = None,
21352136
oneshot: bool = False,
21362137
pin: bool | None = None,
21372138
pin_thread: int | None = None,
@@ -2294,7 +2295,7 @@ async def fire_event(
22942295
self,
22952296
event: str,
22962297
namespace: str | None = None,
2297-
timeout: str | int | float | timedelta | None = -1, # Used by utils.sync_decorator
2298+
timeout: TimeDeltaLike | None = -1, # Used by utils.sync_decorator
22982299
**kwargs,
22992300
) -> None:
23002301
"""Fires an event on the AppDaemon bus, for apps and plugins.
@@ -2747,7 +2748,7 @@ async def reset_timer(self, handle: str) -> bool:
27472748
return await self.AD.sched.reset_timer(self.name, handle)
27482749

27492750
@utils.sync_decorator
2750-
async def info_timer(self, handle: str) -> tuple[dt.datetime, int, dict] | None:
2751+
async def info_timer(self, handle: str) -> tuple[dt.datetime, float, dict] | None:
27512752
"""Get information about a previously created timer.
27522753
27532754
Args:
@@ -2757,24 +2758,27 @@ async def info_timer(self, handle: str) -> tuple[dt.datetime, int, dict] | None:
27572758
A tuple with the following values or ``None`` if handle is invalid or timer no longer exists.
27582759
27592760
- `time` - datetime object representing the next time the callback will be fired
2760-
- `interval` - repeat interval if applicable, `0` otherwise.
2761+
- `interval` - repeat interval in seconds if applicable, `0` otherwise.
27612762
- `kwargs` - the values supplied when the callback was initially created.
27622763
27632764
Examples:
27642765
>>> if (info := self.info_timer(handle)) is not None:
27652766
>>> time, interval, kwargs = info
27662767
27672768
"""
2768-
return await self.AD.sched.info_timer(handle, self.name)
2769+
if (result := await self.AD.sched.info_timer(handle, self.name)) is not None:
2770+
time, interval, kwargs = result
2771+
return time, interval.total_seconds(), kwargs
2772+
return None
27692773

27702774
@utils.sync_decorator
27712775
async def run_in(
27722776
self,
27732777
callback: Callable,
2774-
delay: str | int | float | timedelta,
2778+
delay: TimeDeltaLike,
27752779
*args,
2776-
random_start: int | None = None,
2777-
random_end: int | None = None,
2780+
random_start: TimeDeltaLike | None = None,
2781+
random_end: TimeDeltaLike | None = None,
27782782
pin: bool | None = None,
27792783
pin_thread: int | None = None,
27802784
**kwargs,
@@ -2826,8 +2830,8 @@ async def run_in(
28262830
name=self.name,
28272831
aware_dt=exec_time,
28282832
callback=sched_func,
2829-
random_start=random_start,
2830-
random_end=random_end,
2833+
random_start=utils.parse_timedelta_or_none(random_start),
2834+
random_end=utils.parse_timedelta_or_none(random_end),
28312835
pin=pin,
28322836
pin_thread=pin_thread,
28332837
)
@@ -2838,8 +2842,8 @@ async def run_once(
28382842
callback: Callable,
28392843
start: str | dt.time | dt.datetime | None = None,
28402844
*args,
2841-
random_start: int | None = None,
2842-
random_end: int | None = None,
2845+
random_start: TimeDeltaLike | None = None,
2846+
random_end: TimeDeltaLike | None = None,
28432847
pin: bool | None = None,
28442848
pin_thread: int | None = None,
28452849
**kwargs,
@@ -2908,8 +2912,8 @@ async def run_at(
29082912
callback: Callable,
29092913
start: str | dt.time | dt.datetime,
29102914
*args,
2911-
random_start: int | None = None,
2912-
random_end: int | None = None,
2915+
random_start: TimeDeltaLike | None = None,
2916+
random_end: TimeDeltaLike | None = None,
29132917
pin: bool | None = None,
29142918
pin_thread: int | None = None,
29152919
**kwargs,
@@ -2962,6 +2966,9 @@ async def run_at(
29622966
29632967
"""
29642968
start = "now" if start is None else start
2969+
random_start_td = utils.parse_timedelta_or_none(random_start)
2970+
random_end_td = utils.parse_timedelta_or_none(random_end)
2971+
29652972
match start:
29662973
case str() as start_str if start.startswith("sun"):
29672974
if start.startswith("sunrise"):
@@ -2982,14 +2989,14 @@ async def run_at(
29822989
self.AD.sched.insert_schedule,
29832990
name=self.name,
29842991
aware_dt=start,
2985-
interval=timedelta(days=1).total_seconds()
2992+
interval=timedelta(days=1)
29862993
) # fmt: skip
29872994

29882995
func = functools.partial(
29892996
func,
29902997
callback=functools.partial(callback, *args, **kwargs),
2991-
random_start=random_start,
2992-
random_end=random_end,
2998+
random_start=random_start_td,
2999+
random_end=random_end_td,
29933000
pin=pin,
29943001
pin_thread=pin_thread,
29953002
)
@@ -3001,8 +3008,8 @@ async def run_daily(
30013008
callback: Callable,
30023009
start: str | dt.time | dt.datetime | None = None,
30033010
*args,
3004-
random_start: int | None = None,
3005-
random_end: int | None = None,
3011+
random_start: TimeDeltaLike | None = None,
3012+
random_end: TimeDeltaLike | None = None,
30063013
pin: bool | None = None,
30073014
pin_thread: int | None = None,
30083015
**kwargs,
@@ -3094,8 +3101,8 @@ async def run_hourly(
30943101
callback: Callable,
30953102
start: str | dt.time | dt.datetime | None = None,
30963103
*args,
3097-
random_start: int | None = None,
3098-
random_end: int | None = None,
3104+
random_start: TimeDeltaLike | None = None,
3105+
random_end: TimeDeltaLike | None = None,
30993106
pin: bool | None = None,
31003107
pin_thread: int | None = None,
31013108
**kwargs,
@@ -3155,8 +3162,8 @@ async def run_minutely(
31553162
callback: Callable,
31563163
start: str | dt.time | dt.datetime | None = None,
31573164
*args,
3158-
random_start: int | None = None,
3159-
random_end: int | None = None,
3165+
random_start: TimeDeltaLike | None = None,
3166+
random_end: TimeDeltaLike | None = None,
31603167
pin: bool | None = None,
31613168
pin_thread: int | None = None,
31623169
**kwargs,
@@ -3216,10 +3223,10 @@ async def run_every(
32163223
self,
32173224
callback: Callable,
32183225
start: str | dt.time | dt.datetime | None = None,
3219-
interval: str | int | float | timedelta = 0,
3226+
interval: TimeDeltaLike = 0,
32203227
*args,
3221-
random_start: int | None = None,
3222-
random_end: int | None = None,
3228+
random_start: TimeDeltaLike | None = None,
3229+
random_end: TimeDeltaLike | None = None,
32233230
pin: bool | None = None,
32243231
pin_thread: int | None = None,
32253232
**kwargs,
@@ -3317,9 +3324,9 @@ def timed_callback(self, **kwargs): ... # example callback
33173324
aware_dt=next_period,
33183325
callback=functools.partial(callback, *args, **kwargs),
33193326
repeat=True,
3320-
interval=interval.total_seconds(),
3321-
random_start=random_start,
3322-
random_end=random_end,
3327+
interval=interval,
3328+
random_start=utils.parse_timedelta_or_none(random_start),
3329+
random_end=utils.parse_timedelta_or_none(random_end),
33233330
pin=pin,
33243331
pin_thread=pin_thread,
33253332
)
@@ -3330,9 +3337,9 @@ async def run_at_sunset(
33303337
callback: Callable,
33313338
*args,
33323339
repeat: bool = True,
3333-
offset: str | int | float | timedelta | None = None,
3334-
random_start: int | None = None,
3335-
random_end: int | None = None,
3340+
offset: TimeDeltaLike | None = None,
3341+
random_start: TimeDeltaLike | None = None,
3342+
random_end: TimeDeltaLike | None = None,
33363343
pin: bool | None = None,
33373344
pin_thread: int | None = None,
33383345
**kwargs,
@@ -3383,20 +3390,20 @@ async def run_at_sunset(
33833390
"""
33843391
now = await self.AD.sched.get_now()
33853392
sunset = await self.AD.sched.todays_sunset()
3386-
td = utils.parse_timedelta(offset)
3387-
if sunset + td < now:
3393+
offset_td = utils.parse_timedelta(offset)
3394+
if sunset + offset_td < now:
33883395
sunset = await self.AD.sched.next_sunset()
33893396

3390-
self.logger.debug(f"Registering run_at_sunset at {sunset + td} with {args}, {kwargs}")
3397+
self.logger.debug(f"Registering run_at_sunset at {sunset + offset_td} with {args}, {kwargs}")
33913398
return await self.AD.sched.insert_schedule(
33923399
name=self.name,
33933400
aware_dt=sunset,
33943401
callback=functools.partial(callback, *args, **kwargs),
33953402
repeat=repeat,
33963403
type_="next_setting",
3397-
offset=offset,
3398-
random_start=random_start,
3399-
random_end=random_end,
3404+
offset=offset_td,
3405+
random_start=utils.parse_timedelta_or_none(random_start),
3406+
random_end=utils.parse_timedelta_or_none(random_end),
34003407
pin=pin,
34013408
pin_thread=pin_thread,
34023409
)
@@ -3407,9 +3414,9 @@ async def run_at_sunrise(
34073414
callback: Callable,
34083415
*args,
34093416
repeat: bool = True,
3410-
offset: str | int | float | timedelta | None = None,
3411-
random_start: int | None = None,
3412-
random_end: int | None = None,
3417+
offset: TimeDeltaLike | None = None,
3418+
random_start: TimeDeltaLike | None = None,
3419+
random_end: TimeDeltaLike | None = None,
34133420
pin: bool | None = None,
34143421
pin_thread: int | None = None,
34153422
**kwargs,
@@ -3460,19 +3467,19 @@ async def run_at_sunrise(
34603467
"""
34613468
now = await self.AD.sched.get_now()
34623469
sunrise = await self.AD.sched.todays_sunrise()
3463-
td = utils.parse_timedelta(offset)
3464-
if sunrise + td < now:
3470+
offset_td = utils.parse_timedelta(offset)
3471+
if sunrise + offset_td < now:
34653472
sunrise = await self.AD.sched.next_sunrise()
3466-
self.logger.debug(f"Registering run_at_sunrise at {sunrise + td} with {args}, {kwargs}")
3473+
self.logger.debug(f"Registering run_at_sunrise at {sunrise + offset_td} with {args}, {kwargs}")
34673474
return await self.AD.sched.insert_schedule(
34683475
name=self.name,
34693476
aware_dt=sunrise,
34703477
callback=functools.partial(callback, *args, **kwargs),
34713478
repeat=repeat,
34723479
type_="next_rising",
3473-
offset=offset,
3474-
random_start=random_start,
3475-
random_end=random_end,
3480+
offset=offset_td,
3481+
random_start=utils.parse_timedelta_or_none(random_start),
3482+
random_end=utils.parse_timedelta_or_none(random_end),
34763483
pin=pin,
34773484
pin_thread=pin_thread,
34783485
)
@@ -3484,7 +3491,7 @@ async def run_at_sunrise(
34843491
def dash_navigate(
34853492
self,
34863493
target: str,
3487-
timeout: str | int | float | timedelta | None = -1, # Used by utils.sync_decorator
3494+
timeout: TimeDeltaLike | None = -1, # Used by utils.sync_decorator
34883495
ret: str | None = None,
34893496
sticky: int = 0,
34903497
deviceid: str | None = None,

appdaemon/entity.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from typing import TYPE_CHECKING, Any, overload
99

1010
from appdaemon import utils
11+
from .types import TimeDeltaLike
1112

1213
from .exceptions import TimeOutException
1314
from .state import StateCallbackType
@@ -192,9 +193,9 @@ async def listen_state(
192193
callback: StateCallbackType,
193194
new: str | Callable[[Any], bool] | None = None,
194195
old: str | Callable[[Any], bool] | None = None,
195-
duration: str | int | float | timedelta | None = None,
196+
duration: TimeDeltaLike | None = None,
196197
attribute: str | None = None,
197-
timeout: str | int | float | timedelta | None = None,
198+
timeout: TimeDeltaLike | None = None,
198199
immediate: bool = False,
199200
oneshot: bool = False,
200201
pin: bool | None = None,
@@ -223,7 +224,7 @@ async def listen_state(self, callback: StateCallbackType, **kwargs: Any) -> str:
223224
careful when comparing them. The ``self.get_state()`` method is useful for checking the data type of
224225
the desired attribute. If ``old`` is a callable (lambda, function, etc), then it will be called with
225226
the old state, and the callback will only be invoked if the callable returns ``True``.
226-
duration (str | int | float | timedelta, optional): If supplied, the callback will not be invoked unless the
227+
duration (TimeDeltaLike, optional): If supplied, the callback will not be invoked unless the
227228
desired state is maintained for that amount of time. This requires that a specific attribute is
228229
specified (or the default of ``state`` is used), and should be used in conjunction with either or both
229230
of the ``new`` and ``old`` parameters. When the callback is called, it is supplied with the values of
@@ -237,7 +238,7 @@ async def listen_state(self, callback: StateCallbackType, **kwargs: Any) -> str:
237238
the default behavior is to use the value of ``state``. Using the value ``all`` will cause the callback
238239
to get triggered for any change in state, and the new/old values used for the callback will be the
239240
entire state dict rather than the individual value of an attribute.
240-
timeout (str | int | float | timedelta, optional): If given, the callback will be automatically removed
241+
timeout (TimeDeltaLike, optional): If given, the callback will be automatically removed
241242
after that amount of time. If activity for the listened state has occurred that would trigger a
242243
duration timer, the duration timer will still be fired even though the callback has been removed.
243244
immediate (bool, optional): If given, it enables the countdown for a delay parameter to start at the time.

0 commit comments

Comments
 (0)