Skip to content

Commit 59283b8

Browse files
committed
updated state trigger state_hold_false startup logic; see #95
1 parent 1ee8497 commit 59283b8

File tree

4 files changed

+71
-67
lines changed

4 files changed

+71
-67
lines changed

custom_components/pyscript/trigger.py

Lines changed: 24 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@
1616
from .const import LOGGER_PATH
1717
from .eval import AstEval
1818
from .event import Event
19-
from .mqtt import Mqtt
2019
from .function import Function
20+
from .mqtt import Mqtt
2121
from .state import STATE_VIRTUAL_ATTRS, State
2222

2323
_LOGGER = logging.getLogger(LOGGER_PATH + ".trigger")
@@ -173,12 +173,8 @@ async def wait_until(
173173
last_state_trig_time = None
174174
state_trig_waiting = False
175175
state_trig_notify_info = [None, None]
176-
177-
#
178-
# at startup we start our state_hold_false window,
179-
# although it could get updated if state_check_now is set.
180-
#
181-
state_false_time = time.monotonic()
176+
state_false_time = None
177+
check_state_expr_on_start = state_check_now or state_hold_false is not None
182178

183179
if state_trigger is not None:
184180
state_trig = []
@@ -214,16 +210,16 @@ async def wait_until(
214210
raise exc
215211

216212
state_trig_ident.update(state_trig_ident_any)
217-
if state_check_now and state_trig_eval:
213+
if check_state_expr_on_start and state_trig_eval:
218214
#
219-
# check straight away to see if the condition is met (to avoid race conditions)
215+
# check straight away to see if the condition is met
220216
#
221217
new_vars = State.notify_var_get(state_trig_ident, {})
222218
state_trig_ok = await state_trig_eval.eval(new_vars)
223219
exc = state_trig_eval.get_exception_obj()
224220
if exc is not None:
225221
raise exc
226-
if state_hold_false is not None:
222+
if state_hold_false is not None and not state_check_now:
227223
#
228224
# if state_trig_ok we wait until it is false;
229225
# otherwise we consider now to be the start of the false hold time
@@ -817,17 +813,14 @@ async def trigger_watch(self):
817813
Event.notify_add(self.event_trigger[0], self.notify_q)
818814
if self.mqtt_trigger is not None:
819815
_LOGGER.debug("trigger %s adding mqtt_trigger %s", self.name, self.mqtt_trigger[0])
820-
await Mqtt.notify_add(self.mqtt_trigger[0], self.notify_q)
816+
await Mqtt.notify_add(self.mqtt_trigger[0], self.notify_q)
821817

822818
last_trig_time = None
823819
last_state_trig_time = None
824820
state_trig_waiting = False
825821
state_trig_notify_info = [None, None]
826-
#
827-
# at startup we start our state_hold_false window,
828-
# although it could get updated if state_check_now is set.
829-
#
830-
state_false_time = time.monotonic()
822+
state_false_time = None
823+
check_state_expr_on_start = self.state_check_now or self.state_hold_false is not None
831824

832825
while True:
833826
timeout = None
@@ -841,7 +834,7 @@ async def trigger_watch(self):
841834
notify_type = "startup"
842835
notify_info = {"trigger_type": "time", "trigger_time": None}
843836
self.run_on_startup = False
844-
elif self.state_check_now:
837+
elif check_state_expr_on_start:
845838
#
846839
# first time only - skip wait and check state trigger
847840
#
@@ -851,7 +844,7 @@ async def trigger_watch(self):
851844
else:
852845
notify_vars = {}
853846
notify_info = [notify_vars, {"trigger_type": notify_type}]
854-
self.state_check_now = False
847+
check_state_expr_on_start = False
855848
else:
856849
if self.time_trigger:
857850
now = dt_now()
@@ -900,7 +893,7 @@ async def trigger_watch(self):
900893

901894
if not ident_any_values_changed(func_args, self.state_trig_ident_any):
902895
#
903-
# if var_name not in func_args we are state_check_now
896+
# if var_name not in func_args we are check_state_expr_on_start
904897
#
905898
if "var_name" in func_args and not ident_values_changed(
906899
func_args, self.state_trig_ident
@@ -917,23 +910,26 @@ async def trigger_watch(self):
917910
if self.state_hold_false is not None:
918911
if "var_name" not in func_args:
919912
#
920-
# this is state_check_now check
913+
# this is check_state_expr_on_start check
921914
# if immediately true, force wait until False
922915
# otherwise start False wait now
923916
#
924917
state_false_time = None if trig_ok else time.monotonic()
925-
continue
918+
if not self.state_check_now:
919+
continue
926920
if state_false_time is None:
927921
if trig_ok:
928922
#
929-
# wasn't False, so ignore
923+
# wasn't False, so ignore after initial check
930924
#
931-
continue
932-
#
933-
# first False, so remember when it is
934-
#
935-
state_false_time = time.monotonic()
936-
elif trig_ok:
925+
if "var_name" in func_args:
926+
continue
927+
else:
928+
#
929+
# first False, so remember when it is
930+
#
931+
state_false_time = time.monotonic()
932+
elif trig_ok and "var_name" in func_args:
937933
too_soon = time.monotonic() - state_false_time < self.state_hold_false
938934
state_false_time = None
939935
if too_soon:

docs/new_features.rst

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,15 @@ Planned new features post 1.0.0 include:
2929

3030
The new features since 1.0.0 in master include:
3131

32-
- Added ``state_hold_false=None`` optional period in seconds to ``@state_trigger()`` and ``task.wait_until()``.
33-
This requires the trigger expression to be ``False`` for at least that period (including 0) before a
34-
successful trigger. Setting this optional parameter makes state triggers edge triggered (ie,
35-
triggers only on transition from ``False`` to ``True``), instead of the default level trigger (ie,
36-
only has to evaluate to ``True``). Proposed by @tchef69 (#89).
3732
- Adding new decorator ``@mqtt_trigger`` by @dlashua (#98, #105).
38-
- All .py files below the pyscript/scripts directory are autoloaded, recursively into all subdirectories.
39-
Any file name or directory starting with ``#`` is skipped, which is an in-place way of disabling
40-
a specific file or directory tree (#97).
33+
- Added ``state_hold_false=None`` optional period in seconds to ``@state_trigger()`` and ``task.wait_until()``.
34+
This requires the trigger expression to be ``False`` for at least that period (including 0) before
35+
a successful trigger. Setting this optional parameter makes state triggers edge triggered (ie,
36+
triggers only on transition from ``False`` to ``True``), instead of the default level trigger
37+
(ie, only has to evaluate to ``True``). Proposed by @tchef69 (#89).
38+
- All .py files below the ``pyscript/scripts`` directory are autoloaded, recursively. Also, any
39+
file name or directory starting with ``#`` is skipped (including top-level and ``apps``), which is
40+
an in-place way of disabling a specific script, app or directory tree (#97).
4141
- ``del`` and new function ``state.delete()` can delete state variables and state variable attributes.
4242
4343
Bug fixes since 1.0.0 in master include:

docs/reference.rst

Lines changed: 16 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -303,27 +303,24 @@ Optional arguments are:
303303
that number of seconds after the first state trigger.
304304

305305
``state_hold_false=None``
306-
If set, the ``@state_trigger`` expression must evaluate to ``False`` for this duration in seconds
307-
before the next trigger can occur. The default value of ``None`` means that triggers can occur
308-
without the trigger expression having to be ``False`` between triggers. A value of ``0`` requires
309-
the expression become ``False`` between triggers, but with no minimum time in that state.
310-
If the expression evaluates to ``True`` during the ``state_hold_false`` period, that trigger is
311-
ignored, and when the expression next is ``False`` the ``state_hold_false`` period starts over.
306+
If set, the state trigger edge is triggered (triggers on a ``False`` to ``True`` transition),
307+
versus the default of level triggered (triggers when ``True``). The ``@state_trigger`` expression
308+
must evaluate to ``False`` for this duration in seconds before the next trigger can occur.
309+
A value of ``0`` requires the expression be ``False`` before a trigger, but with no minimum
310+
time in that state. If the expression evaluates to ``True`` during the ``state_hold_false`` period,
311+
that trigger is ignored, and when the expression next is ``False`` the ``state_hold_false`` period
312+
starts over.
312313

313314
For example, by default the expression ``"int(sensor.temp_outside) >= 50"`` will trigger every
314-
time ``sensor.temp_outside`` changes to a value that is 50 or more. If instead
315-
``state_hold_false=0``, the trigger will only occur when ``sensor.temp_outside`` changes the first
316-
time to 50 or more. It has to go back below 50 for ``state_hold_false`` seconds before a new
317-
trigger can occur. To summarize, the default behavior is level triggered, and setting ``state_hold_false``
318-
makes it edge triggered.
319-
320-
The ``state_hold_false`` period applies at startup, although the expression is not checked at
321-
startup if ``state_check_now==False``. So if ``state_hold_false=0`` the first trigger after startup
322-
will succeed, whether or not the expression was previously ``False``. That behavior can be changed
323-
by setting ``state_check_now=True``; the expression is checked at startup, and if ``True`` the
324-
trigger will not occur, and a wait for the next ``False`` will begin. So setting ``state_check_now=True``
325-
and ``state_hold_false`` enforces the need for the expression to be ``False`` before the first
326-
trigger.
315+
time ``sensor.temp_outside`` changes to a value that is 50 or more. If instead ``state_hold_false=0``,
316+
the trigger will only occur when ``sensor.temp_outside`` changes the first time to 50 or more.
317+
It has to go back below 50 for ``state_hold_false`` seconds before a new trigger can occur.
318+
319+
When ``state_hold_false`` is set, the state trigger expression is evaluated at startup. If ``False``
320+
the ``state_hold_false`` period begins. If ``True``, a wait for the next ``False`` value begins.
321+
If ``state_check_now`` is also set, the trigger will also occur at startup if the expression is
322+
``True`` at startup, while the ``state_hold_false`` logic will continue to wait until the expression
323+
is ``False`` for that period before the next future trigger.
327324

328325
All state variables in HASS have string values. So you’ll have to do comparisons against string
329326
values or cast the variable to an integer or float. These two examples are essentially equivalent

tests/test_function.py

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -420,10 +420,9 @@ def func4(trigger_type=None, event_type=None, **kwargs):
420420
def func4a_hold_false():
421421
global seq_num
422422
423-
while 1:
424-
seq_num += 1
425-
res = task.wait_until(state_trigger="int(pyscript.f4avar2) >= 10", state_hold_false=0, __test_handshake__=["pyscript.done2", seq_num], state_check_now=True)
426-
pyscript.done = [seq_num, res["value"]]
423+
seq_num += 1
424+
res = task.wait_until(state_trigger="int(pyscript.f4avar2) >= 10", state_hold_false=0, __test_handshake__=["pyscript.done2", seq_num], state_check_now=True)
425+
pyscript.done = [seq_num, res.get("value", None)]
427426
428427
@service
429428
def func4b_hold_false():
@@ -442,7 +441,8 @@ def func4c_hold_false():
442441
# this should never trigger
443442
seq_num += 1
444443
res = task.wait_until(state_trigger="int(pyscript.f4bvar2) >= 10", state_hold_false=1000, __test_handshake__=["pyscript.done2", seq_num], state_check_now=True)
445-
pyscript.done = [seq_num, res["value"]]
444+
pyscript.f4bvar2 = 0
445+
pyscript.done = [seq_num, res.get("value", None)]
446446
447447
@state_trigger("pyscript.f5var1")
448448
@time_active("cron(* * * * *)")
@@ -842,18 +842,21 @@ def func9(var_name=None, value=None, old_value=None):
842842
hass.states.async_set("pyscript.f4avar2", 6)
843843
hass.states.async_set("pyscript.f4avar2", 15)
844844
assert literal_eval(await wait_until_done(notify_q)) == [seq_num, "15"]
845-
# these won't retrigger
845+
846+
# will trigger immediately
847+
seq_num += 1
848+
await hass.services.async_call("pyscript", "func4a_hold_false", {})
849+
assert literal_eval(await wait_until_done(notify_q)) == [seq_num, None]
850+
846851
seq_num += 1
847-
assert literal_eval(await wait_until_done(notify_q2)) == seq_num
848-
hass.states.async_set("pyscript.f4avar2", 16)
849-
hass.states.async_set("pyscript.f4avar2", 17)
850-
hass.states.async_set("pyscript.f4avar2", 18)
851852
hass.states.async_set("pyscript.f4avar2", 6)
853+
await hass.services.async_call("pyscript", "func4a_hold_false", {})
854+
assert literal_eval(await wait_until_done(notify_q2)) == seq_num
855+
hass.states.async_set("pyscript.f4avar2", 7)
856+
hass.states.async_set("pyscript.f4avar2", 8)
852857
# this will trigger
853858
hass.states.async_set("pyscript.f4avar2", 19)
854859
assert literal_eval(await wait_until_done(notify_q)) == [seq_num, "19"]
855-
seq_num += 1
856-
assert literal_eval(await wait_until_done(notify_q2)) == seq_num
857860

858861
hass.states.async_set("pyscript.f4bvar2", 6)
859862
seq_num += 1
@@ -875,6 +878,14 @@ def func9(var_name=None, value=None, old_value=None):
875878
assert literal_eval(await wait_until_done(notify_q)) == [seq_num, "20"]
876879
seq_num += 1
877880
assert literal_eval(await wait_until_done(notify_q2)) == seq_num
881+
hass.states.async_set("pyscript.f4bvar2", 21)
882+
hass.states.async_set("pyscript.f4bvar2", 22)
883+
hass.states.async_set("pyscript.f4bvar2", 23)
884+
hass.states.async_set("pyscript.f4bvar2", 1)
885+
hass.states.async_set("pyscript.f4bvar2", 25)
886+
assert literal_eval(await wait_until_done(notify_q)) == [seq_num, "25"]
887+
seq_num += 1
888+
assert literal_eval(await wait_until_done(notify_q2)) == seq_num
878889

879890

880891
async def test_trigger_closures(hass, caplog):

0 commit comments

Comments
 (0)