Skip to content

Commit 3be71ae

Browse files
committed
adds optional argument state_check_now=False to @state_trigger; resolves #65
1 parent 134fc7f commit 3be71ae

File tree

4 files changed

+91
-13
lines changed

4 files changed

+91
-13
lines changed

custom_components/pyscript/eval.py

Lines changed: 1 addition & 1 deletion
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"},
458+
"state_trigger": {"state_hold", "state_check_now"},
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

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -535,7 +535,9 @@ def __init__(
535535
self.global_ctx = global_ctx
536536
self.trig_cfg = trig_cfg
537537
self.state_trigger = trig_cfg.get("state_trigger", {}).get("args", None)
538-
self.state_hold_dur = trig_cfg.get("state_trigger", {}).get("kwargs", {}).get("state_hold", None)
538+
self.state_trigger_kwargs = trig_cfg.get("state_trigger", {}).get("kwargs", {})
539+
self.state_hold_dur = self.state_trigger_kwargs.get("state_hold", None)
540+
self.state_check_now = self.state_trigger_kwargs.get("state_check_now", False)
539541
self.time_trigger = trig_cfg.get("time_trigger", {}).get("args", None)
540542
self.event_trigger = trig_cfg.get("event_trigger", {}).get("args", None)
541543
self.state_active = trig_cfg.get("state_active", {}).get("args", None)
@@ -677,8 +679,20 @@ async def trigger_watch(self):
677679
#
678680
# first time only - skip waiting for other triggers
679681
#
682+
notify_type = "startup"
680683
notify_info = {"trigger_type": "time", "trigger_time": None}
681684
self.run_on_startup = False
685+
elif self.state_check_now:
686+
#
687+
# first time only - skip wait and check state trigger
688+
#
689+
notify_type = "state"
690+
if self.state_trig_ident:
691+
notify_vars = State.notify_var_get(self.state_trig_ident, {})
692+
else:
693+
notify_vars = {}
694+
notify_info = [notify_vars, {"trigger_type": notify_type}]
695+
self.state_check_now = False
682696
else:
683697
if self.time_trigger:
684698
now = dt_now()
@@ -725,7 +739,7 @@ async def trigger_watch(self):
725739
elif notify_type == "state":
726740
new_vars, func_args = notify_info
727741

728-
if func_args["var_name"] not in self.state_trig_ident_any:
742+
if "var_name" not in func_args or func_args["var_name"] not in self.state_trig_ident_any:
729743
if self.state_trig_eval:
730744
trig_ok = await self.state_trig_eval.eval(new_vars)
731745
exc = self.state_trig_eval.get_exception_long()

docs/reference.rst

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ function.
206206

207207
.. code:: python
208208
209-
@state_trigger(str_expr, ..., state_hold=None)
209+
@state_trigger(str_expr, ..., state_hold=None, state_check_now=False)
210210
211211
``@state_trigger`` takes one or more string arguments that contain any expression based on one or
212212
more state variables, and evaluates to ``True`` or ``False`` (or non-zero or zero). Whenever the
@@ -215,12 +215,21 @@ occurs if it evaluates to ``True`` (or non-zero). For each state variable, eg: `
215215
the prior value is also available to the expression as ``domain.name.old`` in case you want to
216216
condition the trigger on the prior value too.
217217

218-
The optional ``state_hold`` is a numeric duration is seconds. If specified, the state trigger
219-
delays executing the trigger function for this amount of time. If the state trigger expression
220-
changes to ``False`` during that time, the trigger is canceled and a wait for a new trigger
221-
begins. If the state trigger expression changes, but is still ``True`` then the ``state_hold``
222-
time is not restarted - the trigger will still occur that number of seconds after the first
223-
state trigger.
218+
Optional arguments are:
219+
220+
``state_hold=None``
221+
A numeric duration in seconds that delays executing the trigger function for this amount of time.
222+
If the state trigger expression changes back to ``False`` during that time, the trigger is
223+
canceled and a wait for a new trigger begins. If the state trigger expression changes, but is
224+
still ``True`` then the ``state_hold`` time is not restarted - the trigger will still occur
225+
that number of seconds after the first state trigger.
226+
227+
``state_check_now=False``
228+
If set, the ``@state_trigger`` expression is evaluated immediately when the trigger function
229+
is defined (typically at startup), and the trigger occurs if the expression evaluates
230+
to ``True`` or non-zero. Normally the expression is only evaluated when a state variable
231+
changes, and not when the trigger function is first defined. This option is the same as
232+
in the ``task.wait_until`` function, except the default value is ``True`` in that case.
224233

225234
Multiple arguments are logically "or"ed together, so the trigger occurs if any of the expressions
226235
evaluate to ``True``. Any argument can alternatively be a list or set of strings, and they are
@@ -301,6 +310,19 @@ and all those values will simply get passed in into kwargs as a ``dict``. That
301310
form to use if you have multiple decorators, since each one passes different variables into the
302311
function (although all of them set ``trigger_type``).
303312

313+
If ``state_check_now`` is set to ``True`` and the trigger occurs during its immediate check, since
314+
there is no underlying state variable change, the trigger function is called with only this arguments:
315+
316+
.. code:: python
317+
318+
kwargs = {
319+
"trigger_type": "state",
320+
}
321+
322+
If the trigger function uses ``var_name==None`` as a keyword argument, it can check if it is ``None``
323+
to determined whether it was called immediately or not. Similarly, if it uses the ``kwargs``
324+
form, if can check if ``var_name`` is in ``kwargs``.
325+
304326
If ``state_hold`` is specified, the arguments to the trigger function reflect the variable change
305327
that cause the first trigger, not any subsequent ones during the ``state_hold`` period. Also, if
306328
the ``@time_active`` or ``@state_active`` decorators are used, they will be evaluated after the

tests/test_function.py

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""Test the pyscript component."""
2+
23
from ast import literal_eval
34
import asyncio
45
from datetime import datetime as dt
@@ -173,7 +174,7 @@ def func_startup_sync(trigger_type=None, trigger_time=None):
173174
log.info(f"func_startup_sync setting pyscript.done = {seq_num}, trigger_type = {trigger_type}, trigger_time = {trigger_time}")
174175
pyscript.done = seq_num
175176
176-
@state_trigger("pyscript.f1var1 == '1'")
177+
@state_trigger("pyscript.f1var1 == '1'", state_check_now=True)
177178
def func1(var_name=None, value=None):
178179
global seq_num
179180
@@ -355,7 +356,7 @@ def func6(var_name=None, value=None):
355356
log.info(f"func6 var = {var_name}, value = {value}")
356357
pyscript.done = [seq_num, var_name, pyscript.f6var1.attr1]
357358
358-
@state_trigger("pyscript.f7var1 == '2' and pyscript.f7var1.old == '1'")
359+
@state_trigger("pyscript.f7var1 == '2' and pyscript.f7var1.old == '1'", state_check_now=True)
359360
@state_active("pyscript.f7var1 == '2' and pyscript.f7var1.old == '1' and pyscript.no_such_variable is None")
360361
def func7(var_name=None, value=None, old_value=None):
361362
global seq_num
@@ -365,7 +366,7 @@ def func7(var_name=None, value=None, old_value=None):
365366
secs = (pyscript.f7var1.last_updated - pyscript.f7var1.last_changed).total_seconds()
366367
pyscript.done = [seq_num, var_name, value, old_value, secs]
367368
368-
@state_trigger("pyscript.f8var1 == '2'")
369+
@state_trigger("pyscript.f8var1 == '2'", state_check_now=True)
369370
@time_active(hold_off=10000)
370371
def func8(var_name=None, value=None):
371372
global seq_num
@@ -681,3 +682,44 @@ def func_trig(var_name=None, value=None):
681682
seq_num += 1
682683
hass.states.async_set("pyscript.var1", 101)
683684
assert literal_eval(await wait_until_done(notify_q)) == seq_num
685+
686+
687+
async def test_state_trigger_check_now(hass, caplog):
688+
"""Test state trigger."""
689+
notify_q = asyncio.Queue(0)
690+
691+
hass.states.async_set("pyscript.fstartup", 1)
692+
693+
await setup_script(
694+
hass,
695+
notify_q,
696+
[dt(2020, 7, 1, 10, 59, 59, 999998), dt(2020, 7, 1, 11, 59, 59, 999998)],
697+
"""
698+
699+
from math import sqrt
700+
from homeassistant.core import Context
701+
702+
seq_num = 0
703+
704+
pyscript.fstartup = 1
705+
706+
@state_trigger("pyscript.fstartup == '1'", state_check_now=True)
707+
def func_startup_sync(trigger_type=None, var_name=None):
708+
global seq_num
709+
710+
seq_num += 1
711+
log.info(f"func_startup_sync setting pyscript.done={seq_num}, trigger_type={trigger_type}, var_name={var_name}")
712+
pyscript.done = [seq_num, trigger_type, var_name]
713+
""",
714+
)
715+
seq_num = 0
716+
717+
seq_num += 1
718+
# fire event to start triggers, and handshake when they are running
719+
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
720+
assert literal_eval(await wait_until_done(notify_q)) == [seq_num, "state", None]
721+
722+
seq_num += 1
723+
hass.states.async_set("pyscript.fstartup", 0)
724+
hass.states.async_set("pyscript.fstartup", 1)
725+
assert literal_eval(await wait_until_done(notify_q)) == [seq_num, "state", "pyscript.fstartup"]

0 commit comments

Comments
 (0)