@@ -38,14 +38,18 @@ def parse_time_offset(offset_str):
3838 value = 0
3939 if len (match ) == 4 :
4040 value = float (match [1 ].replace (" " , "" ))
41- if match [2 ] == "m" or match [ 2 ] == "min" or match [ 2 ] == "minutes" :
41+ if match [2 ] in { "m" , "min" , "mins" , "minute" , "minutes" } :
4242 scale = 60
43- elif match [2 ] == "h" or match [ 2 ] == "hr" or match [ 2 ] == "hours" :
43+ elif match [2 ] in { "h" , "hr" , "hour" , "hours" } :
4444 scale = 60 * 60
45- elif match [2 ] == "d" or match [ 2 ] == "day" or match [ 2 ] == "days" :
45+ elif match [2 ] in { "d" , "day" , "days" } :
4646 scale = 60 * 60 * 24
47- elif match [2 ] == "w" or match [ 2 ] == "week" or match [ 2 ] == "weeks" :
47+ elif match [2 ] in { "w" , "week" , "weeks" } :
4848 scale = 60 * 60 * 24 * 7
49+ elif match [2 ] not in {"" , "s" , "sec" , "second" , "seconds" }:
50+ _LOGGER .error ("can't parse time offset %s" , offset_str )
51+ else :
52+ _LOGGER .error ("can't parse time offset %s" , offset_str )
4953 return value * scale
5054
5155
@@ -352,9 +356,12 @@ async def wait_until(
352356 this_timeout = None
353357 state_trig_timeout = False
354358 time_next = None
359+ startup_time = None
355360 if time_trigger is not None :
356361 now = dt_now ()
357- time_next = cls .timer_trigger_next (time_trigger , now )
362+ if startup_time is None :
363+ startup_time = now
364+ time_next = cls .timer_trigger_next (time_trigger , now , startup_time )
358365 _LOGGER .debug (
359366 "trigger %s wait_until time_next = %s, now = %s" , ast_ctx .name , time_next , now ,
360367 )
@@ -507,26 +514,27 @@ async def wait_until(
507514 return ret
508515
509516 @classmethod
510- def parse_date_time (cls , date_time_str , day_offset , now ):
517+ def parse_date_time (cls , date_time_str , day_offset , now , startup_time ):
511518 """Parse a date time string, returning datetime."""
512519 year = now .year
513520 month = now .month
514521 day = now .day
515522
516- dt_str = date_time_str .strip ().lower ()
523+ dt_str_orig = dt_str = date_time_str .strip ().lower ()
517524 #
518525 # parse the date
519526 #
520- skip = True
521- match0 = re .split (r"^0*(\d+)[-/]0*(\d+)(?:[-/]0*(\d+))?" , dt_str )
522- match1 = re .split (r"^(\w+).*" , dt_str )
523- if len (match0 ) == 5 :
524- if match0 [3 ] is None :
525- month , day = int (match0 [1 ]), int (match0 [2 ])
526- else :
527+ match0 = re .match (r"0*(\d+)[-/]0*(\d+)(?:[-/]0*(\d+))?" , dt_str )
528+ match1 = re .match (r"(\w+)" , dt_str )
529+ if match0 :
530+ if match0 [3 ]:
527531 year , month , day = int (match0 [1 ]), int (match0 [2 ]), int (match0 [3 ])
532+ else :
533+ month , day = int (match0 [1 ]), int (match0 [2 ])
528534 day_offset = 0 # explicit date means no offset
529- elif len (match1 ) == 3 :
535+ dt_str = dt_str [len (match0 .group (0 )) :]
536+ elif match1 :
537+ skip = True
530538 if match1 [1 ] in cls .dow2int :
531539 dow = cls .dow2int [match1 [1 ]]
532540 if dow >= (now .isoweekday () % 7 ):
@@ -539,39 +547,38 @@ def parse_date_time(cls, date_time_str, day_offset, now):
539547 day_offset = 1
540548 else :
541549 skip = False
542- else :
543- skip = False
550+ if skip :
551+ dt_str = dt_str [ len ( match1 . group ( 0 )) :]
544552 if day_offset != 0 :
545553 now = dt .datetime (year , month , day ) + dt .timedelta (days = day_offset )
546554 year = now .year
547555 month = now .month
548556 day = now .day
549557 else :
550558 now = dt .datetime (year , month , day )
551- if skip :
552- i = dt_str .find (" " )
553- if i >= 0 :
554- dt_str = dt_str [i + 1 :].strip ()
555- else :
556- return now
559+ dt_str = dt_str .strip ()
560+ if len (dt_str ) == 0 :
561+ return now
557562
558563 #
559564 # parse the time
560565 #
561- skip = True
562- match0 = re .split (r"0*(\d+):0*(\d+)(?::0*(\d*\.?\d+(?:[eE][-+]?\d+)?))?" , dt_str )
563- if len (match0 ) == 5 :
564- if match0 [3 ] is not None :
566+ match0 = re .match (r"0*(\d+):0*(\d+)(?::0*(\d*\.?\d+(?:[eE][-+]?\d+)?))?" , dt_str )
567+ if match0 :
568+ if match0 [3 ]:
565569 hour , mins , sec = int (match0 [1 ]), int (match0 [2 ]), float (match0 [3 ])
566570 else :
567571 hour , mins , sec = int (match0 [1 ]), int (match0 [2 ]), 0
572+ dt_str = dt_str [len (match0 .group (0 )) :]
568573 elif dt_str .startswith ("sunrise" ) or dt_str .startswith ("sunset" ):
569574 location = sun .get_astral_location (cls .hass )
570575 try :
571576 if dt_str .startswith ("sunrise" ):
572577 time_sun = location .sunrise (dt .date (year , month , day ))
578+ dt_str = dt_str [7 :]
573579 else :
574580 time_sun = location .sunset (dt .date (year , month , day ))
581+ dt_str = dt_str [6 :]
575582 except Exception :
576583 _LOGGER .warning ("'%s' not defined at this latitude" , dt_str )
577584 # return something in the past so it is ignored
@@ -580,27 +587,30 @@ def parse_date_time(cls, date_time_str, day_offset, now):
580587 hour , mins , sec = time_sun .hour , time_sun .minute , time_sun .second
581588 elif dt_str .startswith ("noon" ):
582589 hour , mins , sec = 12 , 0 , 0
590+ dt_str = dt_str [4 :]
583591 elif dt_str .startswith ("midnight" ):
584592 hour , mins , sec = 0 , 0 , 0
593+ dt_str = dt_str [8 :]
594+ elif dt_str .startswith ("now" ) and dt_str_orig == dt_str :
595+ #
596+ # "now" means the first time, and only matches if there was no date specification
597+ #
598+ hour , mins , sec = 0 , 0 , 0
599+ now = startup_time
600+ dt_str = dt_str [3 :]
585601 else :
586602 hour , mins , sec = 0 , 0 , 0
587- skip = False
588603 now += dt .timedelta (seconds = sec + 60 * (mins + 60 * hour ))
589- if skip :
590- i = dt_str .find (" " )
591- if i >= 0 :
592- dt_str = dt_str [i + 1 :].strip ()
593- else :
594- return now
595604 #
596605 # parse the offset
597606 #
598- if len (dt_str ) > 0 and (dt_str [0 ] == "+" or dt_str [0 ] == "-" ):
607+ dt_str = dt_str .strip ()
608+ if len (dt_str ) > 0 :
599609 now = now + dt .timedelta (seconds = parse_time_offset (dt_str ))
600610 return now
601611
602612 @classmethod
603- def timer_active_check (cls , time_spec , now ):
613+ def timer_active_check (cls , time_spec , now , startup_time ):
604614 """Check if the given time matches the time specification."""
605615 results = {"+" : [], "-" : []}
606616 for entry in time_spec if isinstance (time_spec , list ) else [time_spec ]:
@@ -627,10 +637,10 @@ def timer_active_check(cls, time_spec, now):
627637 _LOGGER .error ("Invalid range expression: %s" , exc )
628638 return False
629639
630- start = cls .parse_date_time (dt_start .strip (), 0 , now )
631- end = cls .parse_date_time (dt_end .strip (), 0 , start )
640+ start = cls .parse_date_time (dt_start .strip (), 0 , now , startup_time )
641+ end = cls .parse_date_time (dt_end .strip (), 0 , start , startup_time )
632642
633- if start < end :
643+ if start <= end :
634644 this_match = start <= now <= end
635645 else : # Over midnight
636646 this_match = now >= start or now <= end
@@ -649,7 +659,7 @@ def timer_active_check(cls, time_spec, now):
649659 return result
650660
651661 @classmethod
652- def timer_trigger_next (cls , time_spec , now ):
662+ def timer_trigger_next (cls , time_spec , now , startup_time ):
653663 """Return the next trigger time based on the given time and time specification."""
654664 next_time = None
655665 if not isinstance (time_spec , list ):
@@ -668,39 +678,41 @@ def timer_trigger_next(cls, time_spec, now):
668678 next_time = val
669679
670680 elif len (match1 ) == 3 :
671- this_t = cls .parse_date_time (match1 [1 ].strip (), 0 , now )
672- if this_t <= now :
681+ this_t = cls .parse_date_time (match1 [1 ].strip (), 0 , now , startup_time )
682+ if this_t <= now and this_t != startup_time :
673683 #
674684 # Try tomorrow (won't make a difference if spec has full date)
675685 #
676- this_t = cls .parse_date_time (match1 [1 ].strip (), 1 , now )
677- if now < this_t and (next_time is None or this_t < next_time ):
686+ this_t = cls .parse_date_time (match1 [1 ].strip (), 1 , now , startup_time )
687+ startup = now == this_t and now == startup_time
688+ if (now < this_t or startup ) and (next_time is None or this_t < next_time ):
678689 next_time = this_t
679690
680691 elif len (match2 ) == 5 :
681692 start_str , period_str = match2 [1 ].strip (), match2 [2 ].strip ()
682- start = cls .parse_date_time (start_str , 0 , now )
693+ start = cls .parse_date_time (start_str , 0 , now , startup_time )
683694 period = parse_time_offset (period_str )
684695 if period <= 0 :
685696 _LOGGER .error ("Invalid non-positive period %s in period(): %s" , period , time_spec )
686697 continue
687698
688699 if match2 [3 ] is None :
689- if now < start and (next_time is None or start < next_time ):
700+ startup = now == start and now == startup_time
701+ if (now < start or startup ) and (next_time is None or start < next_time ):
690702 next_time = start
691- if now >= start :
703+ if now >= start and not startup :
692704 secs = period * (1.0 + math .floor ((now - start ).total_seconds () / period ))
693705 this_t = start + dt .timedelta (seconds = secs )
694706 if now < this_t and (next_time is None or this_t < next_time ):
695707 next_time = this_t
696708 continue
697709 end_str = match2 [3 ].strip ()
698- end = cls .parse_date_time (end_str , 0 , now )
710+ end = cls .parse_date_time (end_str , 0 , now , startup_time )
699711 end_offset = 1 if end < start else 0
700712 for day in [- 1 , 0 , 1 ]:
701- start = cls .parse_date_time (start_str , day , now )
702- end = cls .parse_date_time (end_str , day + end_offset , now )
703- if now < start :
713+ start = cls .parse_date_time (start_str , day , now , startup_time )
714+ end = cls .parse_date_time (end_str , day + end_offset , now , startup_time )
715+ if now < start or ( now == start and now == startup_time ) :
704716 if next_time is None or start < next_time :
705717 next_time = start
706718 break
@@ -894,13 +906,17 @@ async def trigger_watch(self):
894906 state_trig_waiting = False
895907 state_trig_notify_info = [None , None ]
896908 state_false_time = None
909+ startup_time = None
897910 check_state_expr_on_start = self .state_check_now or self .state_hold_false is not None
898911
899912 while True :
900913 timeout = None
901914 state_trig_timeout = False
902915 notify_info = None
903916 notify_type = None
917+ now = dt_now ()
918+ if startup_time is None :
919+ startup_time = now
904920 if self .run_on_startup :
905921 #
906922 # first time only - skip waiting for other triggers
@@ -921,8 +937,7 @@ async def trigger_watch(self):
921937 check_state_expr_on_start = False
922938 else :
923939 if self .time_trigger :
924- now = dt_now ()
925- time_next = TrigTime .timer_trigger_next (self .time_trigger , now )
940+ time_next = TrigTime .timer_trigger_next (self .time_trigger , now , startup_time )
926941 _LOGGER .debug (
927942 "trigger %s time_next = %s, now = %s" , self .name , time_next , now ,
928943 )
@@ -1066,7 +1081,7 @@ async def trigger_watch(self):
10661081 self .active_expr .get_logger ().error (exc )
10671082 trig_ok = False
10681083 if trig_ok and self .time_active :
1069- trig_ok = TrigTime .timer_active_check (self .time_active , dt_now () )
1084+ trig_ok = TrigTime .timer_active_check (self .time_active , now , startup_time )
10701085
10711086 if not trig_ok :
10721087 _LOGGER .debug (
0 commit comments