Skip to content

Commit c486c10

Browse files
committed
started tests and reworked datetime parsing and timer resetting
1 parent a3b9327 commit c486c10

File tree

10 files changed

+1055
-464
lines changed

10 files changed

+1055
-464
lines changed

appdaemon/adapi.py

Lines changed: 66 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -2506,7 +2506,7 @@ async def parse_datetime(
25062506
2019-08-16 06:33:17
25072507
"""
25082508
return await self.AD.sched.parse_datetime(
2509-
time_str=time_str,
2509+
input_=time_str,
25102510
name=name or self.name,
25112511
aware=aware,
25122512
today=today,
@@ -2551,10 +2551,10 @@ async def now_is_between(self, start_time: str, end_time: str, now: str) -> bool
25512551
@utils.sync_decorator
25522552
async def now_is_between(
25532553
self,
2554-
start_time: str | dt.datetime,
2555-
end_time: str | dt.datetime,
2554+
start_time: str | dt.time | dt.datetime,
2555+
end_time: str | dt.time | dt.datetime,
25562556
name: str | None = None,
2557-
now: str | None = None,
2557+
now: dt.datetime | None = None,
25582558
) -> bool:
25592559
"""Determine if the current `time` is within the specified start and end times.
25602560
@@ -2590,7 +2590,7 @@ async def now_is_between(
25902590
>>> #do something
25912591
25922592
"""
2593-
return await self.AD.sched.now_is_between(start_time, end_time, name or self.name, now)
2593+
return await self.AD.sched.now_is_between(start_time=start_time, end_time=end_time, now=now)
25942594

25952595
@utils.sync_decorator
25962596
async def sunrise(self, aware: bool = False, today: bool = False, days_offset: int = 0) -> dt.datetime:
@@ -2791,7 +2791,7 @@ async def run_in(
27912791
callback: Function that will be called after the specified delay. It must conform to the standard scheduler
27922792
callback format documented `here <APPGUIDE.html#scheduler-callbacks>`__.
27932793
delay (str, int, float, datetime.timedelta): Delay before the callback is executed. Numbers will be
2794-
interpreted as seconds. Strings can be in the format of ``HH:MM``, ``HH:MM:SS``, or
2794+
interpreted as seconds. Strings can be in the format of ``SS``, ``MM:SS``, ``HH:MM:SS``, or
27952795
``DD days, HH:MM:SS``. If a ``timedelta`` object is given, it will be used as is.
27962796
*args: Arbitrary positional arguments to be provided to the callback function when it is triggered.
27972797
random_start (int, optional): Start of range of the random time.
@@ -2822,9 +2822,8 @@ async def run_in(
28222822
28232823
28242824
"""
2825-
delay = delay if isinstance(delay, timedelta) else utils.parse_timedelta(delay)
2826-
assert isinstance(delay, timedelta), f"Invalid delay: {delay}"
2827-
self.logger.debug(f"Registering run_in in {delay.total_seconds():.1f}s for {self.name}")
2825+
delay = utils.parse_timedelta(delay)
2826+
self.logger.debug(f"Registering run_in in {utils.format_timedelta(delay)} for {self.name}")
28282827
exec_time = (await self.get_now()) + delay
28292828
sched_func = functools.partial(callback, *args, **kwargs)
28302829
return await self.AD.sched.insert_schedule(
@@ -2911,7 +2910,7 @@ async def run_once(
29112910
async def run_at(
29122911
self,
29132912
callback: Callable,
2914-
start: str | dt.time | dt.datetime | None = None,
2913+
start: str | dt.time | dt.datetime,
29152914
*args,
29162915
random_start: int | None = None,
29172916
random_end: int | None = None,
@@ -2948,49 +2947,55 @@ async def run_at(
29482947
Run at 10:30am today, or 10:30am tomorrow if it is already after 10:30am.
29492948
29502949
>>> def delayed_callback(self, **kwargs): ... # example callback
2951-
>>> handle = self.run_once(self.delayed_callback, datetime.time(10, 30, 0))
2950+
>>> handle = self.run_at(self.delayed_callback, datetime.time(10, 30, 0))
29522951
29532952
Run today at 04:00pm using the `parse_time()` function.
29542953
29552954
>>> def delayed_callback(self, **kwargs): ... # example callback
2956-
>>> handle = self.run_once(self.delayed_callback, "04:00:00 PM")
2955+
>>> handle = self.run_at(self.delayed_callback, "04:00:00 PM")
29572956
29582957
Run at sunset.
29592958
29602959
>>> def delayed_callback(self, **kwargs): ... # example callback
2961-
>>> handle = self.run_once(self.delayed_callback, "sunset")
2960+
>>> handle = self.run_at(self.delayed_callback, "sunset")
29622961
29632962
Run an hour after sunrise.
29642963
29652964
>>> def delayed_callback(self, **kwargs): ... # example callback
2966-
>>> handle = self.run_once(self.delayed_callback, "sunrise + 01:00:00")
2965+
>>> handle = self.run_at(self.delayed_callback, "sunrise + 01:00:00")
29672966
29682967
"""
2968+
start = "now" if start is None else start
29692969
match start:
2970-
case str():
2971-
info = await self.AD.sched._parse_time(start, self.name)
2972-
start = info["datetime"]
2973-
case dt.time():
2974-
if start.tzinfo is None:
2975-
start = start.replace(tzinfo=self.AD.tz)
2976-
date = await self.date()
2977-
start = dt.datetime.combine(date, start)
2978-
case dt.datetime():
2979-
...
2980-
case _:
2981-
raise ValueError("Invalid type for start")
2982-
2983-
self.logger.debug("Registering run_at at %s for %s", start, self.name)
2970+
case str() as start_str if start.startswith("sun"):
2971+
if start.startswith("sunrise"):
2972+
func = self.run_at_sunrise
2973+
elif start.startswith("sunset"):
2974+
func = self.run_at_sunset
2975+
else:
2976+
raise ValueError(f"Invalid sun event: {start_str}")
29842977

2985-
return await self.AD.sched.insert_schedule(
2986-
name=self.name,
2987-
aware_dt=start,
2978+
now = await self.get_now() # type: ignore
2979+
_, offset = utils.parse_time_str(start_str, now=now, location=self.AD.sched.location)
2980+
func = functools.partial(func, *args, repeat=True, offset=offset)
2981+
case _:
2982+
start = await self.AD.sched.parse_datetime(start)
2983+
func = functools.partial(
2984+
self.AD.sched.insert_schedule,
2985+
name=self.name,
2986+
aware_dt=start,
2987+
interval=timedelta(days=1).total_seconds()
2988+
) # fmt: skip
2989+
2990+
func = functools.partial(
2991+
func,
29882992
callback=functools.partial(callback, *args, **kwargs),
29892993
random_start=random_start,
29902994
random_end=random_end,
29912995
pin=pin,
29922996
pin_thread=pin_thread,
29932997
)
2998+
return await func() # type: ignore
29942999

29953000
@utils.sync_decorator
29963001
async def run_daily(
@@ -3052,36 +3057,37 @@ async def run_daily(
30523057
>>> handle = self.run_daily(self.daily_callback, "sunset + 01:00:00")
30533058
30543059
"""
3055-
offset = 0
3056-
sun: Literal["sunrise", "sunset"] | None = None
3060+
start = "now" if start is None else start
30573061
match start:
3058-
case str():
3059-
info = await self.AD.sched._parse_time(start, self.name)
3060-
start, offset, sun = info["datetime"], info["offset"], info["sun"]
3061-
case dt.time():
3062-
if start.tzinfo is None:
3063-
start = start.replace(tzinfo=self.AD.tz)
3064-
date = await self.date()
3065-
start = dt.datetime.combine(date, start)
3066-
case dt.datetime():
3067-
...
3068-
case _:
3069-
raise ValueError("Invalid type for start")
3062+
case str() as start_str if start.startswith("sun"):
3063+
if start.startswith("sunrise"):
3064+
func = self.run_at_sunrise
3065+
elif start.startswith("sunset"):
3066+
func = self.run_at_sunset
3067+
else:
3068+
raise ValueError(f"Invalid sun event: {start_str}")
30703069

3071-
ad_kwargs = dict(
3070+
now = await self.get_now() # type: ignore
3071+
_, offset = utils.parse_time_str(start_str, now=now, location=self.AD.sched.location)
3072+
func = functools.partial(func, *args, repeat=True, offset=offset)
3073+
case _:
3074+
func = functools.partial(
3075+
self.run_every,
3076+
start=start,
3077+
interval=timedelta(days=1).total_seconds()
3078+
) # fmt: skip
3079+
3080+
func = functools.partial(
3081+
func,
3082+
callback=callback,
3083+
*args,
30723084
random_start=random_start,
30733085
random_end=random_end,
30743086
pin=pin,
30753087
pin_thread=pin_thread,
3076-
)
3077-
3078-
match sun:
3079-
case None:
3080-
return await self.run_every(callback, start, timedelta(days=1), *args, **ad_kwargs, **kwargs)
3081-
case "sunrise":
3082-
return await self.run_at_sunrise(callback, *args, repeat=True, offset=offset, **ad_kwargs, **kwargs)
3083-
case "sunset":
3084-
return await self.run_at_sunset(callback, *args, repeat=True, offset=offset, **ad_kwargs, **kwargs)
3088+
**kwargs
3089+
) # fmt: skip
3090+
return await func() # type: ignore
30853091

30863092
@utils.sync_decorator
30873093
async def run_hourly(
@@ -3211,7 +3217,7 @@ async def run_every(
32113217
self,
32123218
callback: Callable,
32133219
start: str | dt.time | dt.datetime | None = None,
3214-
interval: str | int | float | dt.timedelta = 0,
3220+
interval: str | int | float | timedelta = 0,
32153221
*args,
32163222
random_start: int | None = None,
32173223
random_end: int | None = None,
@@ -3298,31 +3304,12 @@ def timed_callback(self, **kwargs): ... # example callback
32983304
32993305
"""
33003306
interval = utils.parse_timedelta(interval)
3301-
assert isinstance(interval, dt.timedelta)
3302-
3303-
match start:
3304-
case str():
3305-
if not start.startswith("now"):
3306-
info = await self.AD.sched._parse_time(start, self.name)
3307-
start = info["datetime"]
3308-
case dt.time():
3309-
if start.tzinfo is None:
3310-
start = start.replace(tzinfo=self.AD.tz)
3311-
date = await self.date()
3312-
start = dt.datetime.combine(date, start)
3313-
case dt.datetime():
3314-
...
3315-
case None:
3316-
pass # This will be handled by get_next_period
3317-
case _:
3318-
raise ValueError("Invalid type for start")
3319-
33203307
next_period = await self.AD.sched.get_next_period(interval, start)
33213308

33223309
self.logger.debug(
33233310
"Registering %s for run_every in %s intervals, starting %s",
33243311
callback.__name__,
3325-
interval,
3312+
utils.format_seconds(interval),
33263313
next_period,
33273314
)
33283315

@@ -3344,7 +3331,7 @@ async def run_at_sunset(
33443331
callback: Callable,
33453332
*args,
33463333
repeat: bool = True,
3347-
offset: int | None = None,
3334+
offset: str | int | float | timedelta | None = None,
33483335
random_start: int | None = None,
33493336
random_end: int | None = None,
33503337
pin: bool | None = None,
@@ -3417,7 +3404,7 @@ async def run_at_sunrise(
34173404
callback: Callable,
34183405
*args,
34193406
repeat: bool = True,
3420-
offset: int | None = None,
3407+
offset: str | int | float | timedelta | None = None,
34213408
random_start: int | None = None,
34223409
random_end: int | None = None,
34233410
pin: bool | None = None,
@@ -3468,7 +3455,7 @@ async def run_at_sunrise(
34683455
>>> self.run_at_sunrise(self.sun, random_start = -60*60, random_end = 30*60)
34693456
34703457
"""
3471-
sunrise = await self.AD.sched.sunrise(today=False, aware=True)
3458+
sunrise = await self.AD.sched.next_sunrise()
34723459
td = utils.parse_timedelta(offset)
34733460
self.logger.debug(f"Registering run_at_sunrise at {sunrise + td} with {args}, {kwargs}")
34743461
return await self.AD.sched.insert_schedule(
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
from dataclasses import dataclass, field
2+
from datetime import datetime
3+
from functools import partial
4+
from typing import Literal
5+
6+
7+
@dataclass(slots=True)
8+
class ScheduleEntry:
9+
name: str
10+
"""Name of the app that registered the callback"""
11+
id: str
12+
"""Unique identifier (handle) for the scheduled callback"""
13+
callback: partial = field(repr=False)
14+
"""Callable to be executed when the callback is triggered"""
15+
basetime: datetime
16+
"""Base time for the callback, without any offset applied"""
17+
timestamp: datetime
18+
"""The resolved time when the callback will be executed, including any offset"""
19+
offset: float | None = None
20+
"""Offset in seconds to be applied to the base time"""
21+
interval: float | None = None
22+
"""Time interval in seconds between executions when it's being restarted"""
23+
repeat: bool = False
24+
"""Whether the callback should be restarted after execution"""
25+
type: Literal["next_rising", "next_setting"] | None = None
26+
pin_app: bool | None = None
27+
pin_thread: int | None = None
28+
kwargs: dict = field(default_factory=dict)
29+
30+
random_start: float | None = None
31+
random_end: float | None = None

0 commit comments

Comments
 (0)