Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Allowing State Trigger Decorator Function to be Called While Executing Another Function #716

Answered by craigbarratt
jlkermit asked this question in Q&A
Discussion options

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
You must be logged in to vote

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

Comment options

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.

You must be logged in to vote
2 replies
Comment options

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
Comment options

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

Comment options

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.

You must be logged in to vote
0 replies
Answer selected by jlkermit
Comment options

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!

You must be logged in to vote
0 replies
Comment options

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.

You must be logged in to vote
0 replies
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Category
Q&A
Labels
None yet

AltStyle によって変換されたページ (->オリジナル) /