Skip to content

Commit 2f16cce

Browse files
committed
Added state_hold_false=None to @state_trigger and task.wait_until()
This requires the trigger expression to be ``False`` for at least that period (including 0) before a successful trigger. Proposed by @tchef69 (#89).
1 parent 2298017 commit 2f16cce

File tree

5 files changed

+265
-25
lines changed

5 files changed

+265
-25
lines changed

custom_components/pyscript/eval.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -455,7 +455,7 @@ async def do_service_call(func, ast_ctx, data):
455455
kwarg_check = {
456456
"task_unique": {"kill_me"},
457457
"time_active": {"hold_off"},
458-
"state_trigger": {"state_hold", "state_check_now"},
458+
"state_trigger": {"state_hold", "state_check_now", "state_hold_false"},
459459
}
460460
for dec_name in trig_args:
461461
if dec_name not in kwarg_check and "kwargs" in trig_args[dec_name]:

custom_components/pyscript/trigger.py

+84-6
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,8 @@ async def wait_until(
151151
event_trigger=None,
152152
timeout=None,
153153
state_hold=None,
154+
state_hold_false=None,
155+
__test_handshake__=None,
154156
):
155157
"""Wait for zero or more triggers, until an optional timeout."""
156158
if state_trigger is None and time_trigger is None and event_trigger is None:
@@ -169,6 +171,12 @@ async def wait_until(
169171
state_trig_waiting = False
170172
state_trig_notify_info = [None, None]
171173

174+
#
175+
# at startup we start our state_hold_false window,
176+
# although it could get updated if state_check_now is set.
177+
#
178+
state_false_time = time.monotonic()
179+
172180
if state_trigger is not None:
173181
state_trig = []
174182
if isinstance(state_trigger, str):
@@ -212,7 +220,13 @@ async def wait_until(
212220
exc = state_trig_eval.get_exception_obj()
213221
if exc is not None:
214222
raise exc
215-
if state_hold is not None and state_trig_ok:
223+
if state_hold_false is not None:
224+
#
225+
# if state_trig_ok we wait until it is false;
226+
# otherwise we consider now to be the start of the false hold time
227+
#
228+
state_false_time = None if state_trig_ok else time.monotonic()
229+
elif state_hold is not None and state_trig_ok:
216230
state_trig_waiting = True
217231
state_trig_notify_info = [None, {"trigger_type": "state"}]
218232
last_state_trig_time = time.monotonic()
@@ -248,6 +262,14 @@ async def wait_until(
248262
Event.notify_add(event_trigger[0], notify_q)
249263
time0 = time.monotonic()
250264

265+
if __test_handshake__:
266+
#
267+
# used for testing to avoid race conditions
268+
# we use this as a handshake that we are about to
269+
# listen to the queue
270+
#
271+
State.set(__test_handshake__[0], __test_handshake__[1])
272+
251273
while True:
252274
ret = None
253275
this_timeout = None
@@ -319,6 +341,26 @@ async def wait_until(
319341
if exc is not None:
320342
break
321343

344+
if state_hold_false is not None:
345+
if state_false_time is None:
346+
if state_trig_ok:
347+
#
348+
# wasn't False, so ignore
349+
#
350+
continue
351+
#
352+
# first False, so remember when it is
353+
#
354+
state_false_time = time.monotonic()
355+
elif state_trig_ok:
356+
too_soon = time.monotonic() - state_false_time < state_hold_false
357+
state_false_time = None
358+
if too_soon:
359+
#
360+
# was False but not for long enough, so start over
361+
#
362+
continue
363+
322364
if state_hold is not None:
323365
if state_trig_ok:
324366
if not state_trig_waiting:
@@ -594,7 +636,8 @@ def __init__(
594636
self.trig_cfg = trig_cfg
595637
self.state_trigger = trig_cfg.get("state_trigger", {}).get("args", None)
596638
self.state_trigger_kwargs = trig_cfg.get("state_trigger", {}).get("kwargs", {})
597-
self.state_hold_dur = self.state_trigger_kwargs.get("state_hold", None)
639+
self.state_hold = self.state_trigger_kwargs.get("state_hold", None)
640+
self.state_hold_false = self.state_trigger_kwargs.get("state_hold_false", None)
598641
self.state_check_now = self.state_trigger_kwargs.get("state_check_now", False)
599642
self.time_trigger = trig_cfg.get("time_trigger", {}).get("args", None)
600643
self.event_trigger = trig_cfg.get("event_trigger", {}).get("args", None)
@@ -727,6 +770,11 @@ async def trigger_watch(self):
727770
last_state_trig_time = None
728771
state_trig_waiting = False
729772
state_trig_notify_info = [None, None]
773+
#
774+
# at startup we start our state_hold_false window,
775+
# although it could get updated if state_check_now is set.
776+
#
777+
state_false_time = time.monotonic()
730778

731779
while True:
732780
timeout = None
@@ -761,7 +809,7 @@ async def trigger_watch(self):
761809
if time_next is not None:
762810
timeout = (time_next - now).total_seconds()
763811
if state_trig_waiting:
764-
time_left = last_state_trig_time + self.state_hold_dur - time.monotonic()
812+
time_left = last_state_trig_time + self.state_hold - time.monotonic()
765813
if timeout is None or time_left < timeout:
766814
timeout = time_left
767815
state_trig_timeout = True
@@ -798,7 +846,9 @@ async def trigger_watch(self):
798846
new_vars, func_args = notify_info
799847

800848
if not ident_any_values_changed(func_args, self.state_trig_ident_any):
849+
#
801850
# if var_name not in func_args we are state_check_now
851+
#
802852
if "var_name" in func_args and not ident_values_changed(
803853
func_args, self.state_trig_ident
804854
):
@@ -810,10 +860,38 @@ async def trigger_watch(self):
810860
if exc is not None:
811861
self.state_trig_eval.get_logger().error(exc)
812862
trig_ok = False
863+
864+
if self.state_hold_false is not None:
865+
if "var_name" not in func_args:
866+
#
867+
# this is state_check_now check
868+
# if immediately true, force wait until False
869+
# otherwise start False wait now
870+
#
871+
state_false_time = None if trig_ok else time.monotonic()
872+
continue
873+
if state_false_time is None:
874+
if trig_ok:
875+
#
876+
# wasn't False, so ignore
877+
#
878+
continue
879+
#
880+
# first False, so remember when it is
881+
#
882+
state_false_time = time.monotonic()
883+
elif trig_ok:
884+
too_soon = time.monotonic() - state_false_time < self.state_hold_false
885+
state_false_time = None
886+
if too_soon:
887+
#
888+
# was False but not for long enough, so start over
889+
#
890+
continue
813891
else:
814892
trig_ok = False
815893

816-
if self.state_hold_dur is not None:
894+
if self.state_hold is not None:
817895
if trig_ok:
818896
if not state_trig_waiting:
819897
state_trig_waiting = True
@@ -823,14 +901,14 @@ async def trigger_watch(self):
823901
"trigger %s got %s trigger; now waiting for state_hold of %g seconds",
824902
notify_type,
825903
self.name,
826-
self.state_hold_dur,
904+
self.state_hold,
827905
)
828906
else:
829907
_LOGGER.debug(
830908
"trigger %s got %s trigger; still waiting for state_hold of %g seconds",
831909
notify_type,
832910
self.name,
833-
self.state_hold_dur,
911+
self.state_hold,
834912
)
835913
continue
836914
if state_trig_waiting:

docs/new_features.rst

+7-2
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,13 @@ Planned new features post 1.0.0 include:
2323

2424
The new features since 1.0.0 in master include:
2525

26-
- None so far
26+
- Added ``state_hold_false=None`` optional period in seconds to ``@state_trigger`` and ``task.wait_until()``.
27+
This requires the trigger expression to be ``False`` for at least that period (including 0) before a
28+
successful trigger. Proposed by @tchef69 (#89).
2729

2830
Bug fixes since 1.0.0 in master include:
2931

30-
- the deprecated function ``state.get_attr`` was missing an ``await``, which caused an exception; in 1.0.0 use ``state.getattr`` instead (#88).
32+
- state setting now copies the attributes, to avoid a strange ``MappingProxyType`` recursion error
33+
inside HASS, reported by @github392 (#87).
34+
- the deprecated function ``state.get_attr`` was missing an ``await``, which caused an exception; in 1.0.0 use
35+
``state.getattr``, reported and fixed by @dlashua (#88).

docs/reference.rst

+34-2
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@ function.
257257

258258
.. code:: python
259259
260-
@state_trigger(str_expr, ..., state_hold=None, state_check_now=False)
260+
@state_trigger(str_expr, ..., state_hold=None, state_hold_false=None, state_check_now=False)
261261
262262
``@state_trigger`` takes one or more string arguments that contain any expression based on one or
263263
more state variables, and evaluates to ``True`` or ``False`` (or non-zero or zero). Whenever any
@@ -291,6 +291,29 @@ Optional arguments are:
291291
still ``True`` then the ``state_hold`` time is not restarted - the trigger will still occur
292292
that number of seconds after the first state trigger.
293293

294+
``state_hold_false=None``
295+
If set, the ``@state_trigger`` expression must evaluate to ``False`` for this duration in seconds
296+
before the next trigger can occur. The default value of ``None`` means that triggers can occur
297+
without the trigger expression having to be ``False`` between triggers. A value of ``0`` requires
298+
the expression become ``False`` between triggers, but with no minimum time in that state.
299+
If the expression evaluates to ``True`` during the ``state_hold_false`` period, that trigger is
300+
ignored, and when the expression next is ``False`` the `state_hold_false`` period starts over.
301+
302+
For example, by default the expression ``"int(sensor.temp_outside) >= 50"`` will trigger every
303+
time ``sensor.temp_outside`` changes to a value that is 50 or more. If instead
304+
``state_hold_false=0``, the trigger will only occur when ``sensor.temp_outside`` changes the first
305+
time to 50 or more. It has to go back below 50 for ``state_hold_false`` seconds before a new
306+
trigger can occur. To summarize, the default behavior is level triggered, and setting ``state_hold_false``
307+
makes it edge triggered.
308+
309+
The ``state_hold_false`` period applies at startup, although the expression is not checked at
310+
startup if ``state_check_now==False``. So if ``state_hold_false=0`` the first trigger after startup
311+
will succeed, whether or not the expression was previously ``False``. That behavior can be changed
312+
by setting ``state_check_now=True``; the expression is checked at startup, and if ``True`` the
313+
trigger will not occur, and a wait for the next ``False`` will begin. So setting ``state_check_now=True``
314+
and ``state_hold_false`` enforces the need for the expression to be ``False`` before the first
315+
trigger.
316+
294317
All state variables in HASS have string values. So you’ll have to do comparisons against string
295318
values or cast the variable to an integer or float. These two examples are essentially equivalent
296319
(note the use of single quotes inside the outer double quotes):
@@ -852,9 +875,18 @@ It takes the following keyword arguments (all are optional):
852875
delays returning for this amount of time. If the state trigger expression changes to ``False``
853876
during that time, the trigger is canceled and a wait for a new trigger begins. If the state
854877
trigger expression changes, but is still ``True`` then the ``state_hold`` time is not
855-
restarted - ``task.wait_until() will return that number of seconds after the first state
878+
restarted - ``task.wait_until()`` will return that number of seconds after the first state
856879
trigger (unless a different trigger type or a ``timeout`` occurs first). This setting also
857880
applies to the initial check when ``state_check_now=True``.
881+
- ``state_hold_false=None`` requires the expression evaluate to ``False`` for this duration in seconds
882+
before a subsequent state trigger occurs. The default value of ``None`` means that the trigger can
883+
occur without the trigger expression having to be ``False``. A value of ``0`` requires the
884+
expression become ``False`` before the trigger, but with no minimum time in that state.
885+
With the default of ``state_check_now=True``, the state trigger expression is checked at startup,
886+
and if ``True`` the trigger will not occur, and a wait for the next ``False`` will begin.
887+
If ``state_check_now==False``, the ``state_hold_false`` period applies at startup, although the
888+
expression is not checked at startup. So if ``state_hold_false=0`` the first trigger after startup
889+
will succeed, whether or not the expression was previously ``False``.
858890

859891
When a trigger occurs, the return value is a ``dict`` containing the same keyword values that are
860892
passed into the function when the corresponding decorator trigger occurs. There will always be a key

0 commit comments

Comments
 (0)