-
Notifications
You must be signed in to change notification settings - Fork 58
Allowing State Trigger Decorator Function to be Called While Executing Another Function #716
-
Is there a way to allow a function with a state decorator to be called while another function that is running is manipulating the state of the entity that is in the state decorator. I tried using task.wait_until(state_trigger='entity id'), but it didn't work, for obvious reasons I think.
What I'm asking is there a function available similar to 'DoEvents' found in the non-async language VBA?
Consider the following code:
TEST_ENTITY_ID = 'input_number.test_numeric_input' _state_trigger_dict = {} #key: trigger string expr, value: trigger function name _ignore_state_event = False @time_trigger("once(now)") def set_value(): global _ignore_state_event log.info(f"Setting global variable _ignore_state_event: True") _ignore_state_event = True for i in range(0,3,1): log.info(f"Setting entity value: {i}") state.set(TEST_ENTITY_ID,i) log.info(f"Setting global variable _ignore_state_event: False") _ignore_state_event = False @state_trigger(TEST_ENTITY_ID) def state_monitor_factory(**kwargs): global _ignore_state_event entity_id = kwargs['var_name'] old_value = kwargs['old_value'] new_value = kwargs['value'] if _ignore_state_event == True: log.info(f"Ignoring state monitor trigger for entity: {entity_id}") return log.info(f"State monitor triggered ({entity_id}) value: {new_value}") _state_trigger_dict[TEST_ENTITY_ID] = state_monitor_factory
Here is the log:
2025-04-09 11:36:52.431 INFO (MainThread) [custom_components.pyscript.global_ctx] Reloaded /config/pyscript/test.py 2025-04-09 11:36:52.433 INFO (MainThread) [custom_components.pyscript.file.test.set_value] Setting global variable _ignore_state_event: True 2025-04-09 11:36:52.433 INFO (MainThread) [custom_components.pyscript.file.test.set_value] Setting entity value: 0 2025-04-09 11:36:52.434 INFO (MainThread) [custom_components.pyscript.file.test.set_value] Setting entity value: 1 2025-04-09 11:36:52.434 INFO (MainThread) [custom_components.pyscript.file.test.set_value] Setting entity value: 2 2025-04-09 11:36:52.434 INFO (MainThread) [custom_components.pyscript.file.test.set_value] Setting global variable _ignore_state_event: False 2025-04-09 11:36:52.435 INFO (MainThread) [custom_components.pyscript.file.test.state_monitor_factory] State monitor triggered (input_number.test_numeric_input) value: 0 2025-04-09 11:36:52.435 INFO (MainThread) [custom_components.pyscript.file.test.state_monitor_factory] State monitor triggered (input_number.test_numeric_input) value: 1 2025-04-09 11:36:52.435 INFO (MainThread) [custom_components.pyscript.file.test.state_monitor_factory] State monitor triggered (input_number.test_numeric_input) value: 2
Beta Was this translation helpful? Give feedback.
All reactions
Trigger functions run asynchronously after they are triggered. There's no guarantee that the statements inside one trigger function will be executed before or after another function's statements when triggered at almost the same time. Statements could even be interleaved between functions (actually, the granularity is finer than that: one function could give up control in the middle of evaluating an expression, and execution of the other function could continue next). This means you can suffer from various race conditions if you don't make your code robust to indeterminate relative order of execution.
As you saw, when you change the state variable in one function, it takes a bit of time f...
Replies: 4 comments 2 replies
-
Hello @jlkermit 👋
Maybe you can try the following to rely more on a pyscriptive approach using decorators to check whether to run or not:
state.set("pyscript.ignore_me", false) @time_trigger("once(now)") def set_value(): state.set("pyscript.ignore_me", true) ... state.set("pyscript.ignore_me", false) @state_trigger(TEST_ENTITY_ID) @state_active("pyscript.ignore_me == false") def state_monitor_factory(**kwargs): ...
But I'm not sure it will solve your problem - sorry currently travelling so can't test it out. Let me know.
Beta Was this translation helpful? Give feedback.
All reactions
-
Yes, that worked.
I was hoping for a more pythonic method. I tried the function "task.create" with the same results, unfortunately, as my original code. Side note, I am pretty sure my original code worked as I intended in Pyscript version 1.6.1. I may have been hallucinating though ;).
TEST_ENTITY_ID = 'input_number.test_numeric_input' _state_trigger_dict = {} #key: trigger string expr, value: trigger function name _ignore_state_event = False @time_trigger("once(now)") def call_set_value(): log.info(f"Creating async task for function: async_set_value") task_id = task.create(async_set_value) log.info(f"Returned async task ID: {task_id}") def async_set_value(): global _ignore_state_event task.unique("set_value") log.info(f"Setting global variable _ignore_state_event: True") _ignore_state_event = True for i in range(0,3,1): log.info(f"Setting entity value: {i}") state.set(TEST_ENTITY_ID,i) log.info(f"Setting global variable _ignore_state_event: False") _ignore_state_event = False @state_trigger(TEST_ENTITY_ID) def state_monitor_factory(**kwargs): global _ignore_state_event entity_id = kwargs['var_name'] old_value = kwargs['old_value'] new_value = kwargs['value'] if _ignore_state_event == True: log.info(f"Ignoring state monitor trigger for entity: {entity_id}") return log.info(f"State monitor triggered ({entity_id}) value: {new_value}") _state_trigger_dict[f"monitor {TEST_ENTITY_ID}"] = state_monitor_factory
Beta Was this translation helpful? Give feedback.
All reactions
-
It's possible. There have been several reports of issues with the way 1.6.2 & 1.6.3 handle async code. A fix was proposed in one of the threads and might end up released in 1.6.4.
I’d recommend you try to downgrade for now or try again once a new version is released to see if that fixes your original code
Beta Was this translation helpful? Give feedback.
All reactions
-
Trigger functions run asynchronously after they are triggered. There's no guarantee that the statements inside one trigger function will be executed before or after another function's statements when triggered at almost the same time. Statements could even be interleaved between functions (actually, the granularity is finer than that: one function could give up control in the middle of evaluating an expression, and execution of the other function could continue next). This means you can suffer from various race conditions if you don't make your code robust to indeterminate relative order of execution.
As you saw, when you change the state variable in one function, it takes a bit of time for HASS to generate the state changed event, which means the 2nd function actually gets triggered after the first one finishes, so your flag is already reset. But that's not guaranteed.
@IgnusG's solution is more robust, but is still not guaranteed - the state trigger might not be evaluated until after the first function finishes.
There are several solutions I can think of. One is to use an async queue to communicate state updates that should be ignored. The function that is updating the state which wants to skip the state monitor function writes the new state value to the queue. The state monitor function checks the queue, and if the values match then it ignores the update. Here's some untested pseudo code:
import asyncio
ignore_q = asyncio.Queue()
TEST_ENTITY_ID = 'input_number.test_numeric_input'
@time_trigger("once(now)")
async def set_value():
for i in range(0,3,1):
await ignore_q.put(i)
state.set(TEST_ENTITY_ID, i)
@state_trigger(TEST_ENTITY_ID)
def state_monitor_factory(**kwargs):
try:
value = queue.get_nowait()
if value == kwargs['value']:
return # queue had value we should ignore
except:
break;
...
However, there's an unlikely but still potential race condition that the state triggers happen out of order, so the ignore values in the queue could be extracted in the wrong order. So this isn't a 100% robust solution.
You could use a map to count the number of times each state set should be ignored. You'll need a lock to prevent race conditions while it is updated in each function.
import asyncio
from typing import Dict
# Shared reference counting map and lock
_ref_counts: Dict[int, int] = {}
_lock = asyncio.Lock()
async def set_value():
...
async with _lock:
count = _ref_counts.get(value, 0)
count += 1
_ref_counts[value] = count
def state_monitor_factory(**kwargs):
ignore = True
value = kwargs['value']
async with _lock:
if value not in _ref_counts:
ignore = False
else:
count = _ref_counts[value]
count -= 1
if count <= 0:
del _ref_counts[value]
else:
_ref_counts[value] = count
if ignore:
return
The simplest solution would be to set an attribute of the state variable, assuming that doesn't break anything that uses the state variable:
@time_trigger("once(now)")
async def set_value():
for i in range(0,3,1):
state.set("input_number.test_numeric_input", i, ignore_me=True)
@state_trigger("input_number.test_numeric_input.ignore_me != True")
def state_monitor_factory(**kwargs):
...
This is robust since the information about what to ignore is part of the state update, so is immune to any order-of-execution issues.
These are all untested.
Beta Was this translation helpful? Give feedback.
All reactions
-
Appreciate the detailed response. Your last suggestion is very simple and does work. I didn't think to manipulate an attribute. Further I didn't know a 'state_trigger' would fire on entity value changed when the 'state_trigger' expression was an entity attribute.
Thanks!
Beta Was this translation helpful? Give feedback.
All reactions
-
Yes - the last point is subtle but important. Even if the expression involves an attribute, any change to the entity is monitored (HASS doesn't provide for finer-grained events) and the expression is evaluated each time there is any change.
Beta Was this translation helpful? Give feedback.