Skip to content

Event based triggers

stefanuytterhoeven edited this page Jul 20, 2021 · 8 revisions

Here's more detail on event triggers and some examples of using them.

Pyscript now supports Jupyter frontends. There is a Jupyter notebook tutorial that you can read, or you can download and run interactively in Jupyter notebook connected to your live HASS with pyscript.

HASS Standard Triggers

Here's a list of standard HASS events. The names are constants that are defined in homeassistant.const; for example the name EVENT_CALL_SERVICE is defined to be "call_service".

Event name What it means
EVENT_CALL_SERVICE A service is being called
EVENT_COMPONENT_LOADED A component has been loaded
EVENT_CORE_CONFIG_UPDATE A configuration update has happened
EVENT_HOMEASSISTANT_CLOSE HASS is closing
EVENT_HOMEASSISTANT_START HASS is starting
EVENT_HOMEASSISTANT_STARTED HASS has started
EVENT_HOMEASSISTANT_STOP HASS is stopping
EVENT_HOMEASSISTANT_FINAL_WRITE HASS is about to stop
EVENT_LOGBOOK_ENTRY A logbook entry is added
EVENT_PLATFORM_DISCOVERED Platform discovered
EVENT_SERVICE_REGISTERED A service has been registered
EVENT_SERVICE_REMOVED A service has been removed
EVENT_STATE_CHANGED A state has changed
EVENT_THEMES_UPDATED Themes are updated
EVENT_TIMER_OUT_OF_SYNC Time is out of sync
EVENT_TIME_CHANGED A 1 second heartbeat

Only a handful of these will typically be of interest. The EVENT_HOMEASSISTANT_START and EVENT_HOMEASSISTANT_STARTED events happen before pyscript starts. The startup @time_trigger (with no arguments) is actually triggered by the EVENT_HOMEASSISTANT_STARTED event. Similarly, most components will be loaded and services will be registered during startup before EVENT_HOMEASSISTANT_STARTED, so those corresponding events won't occur after pyscript is started. The EVENT_TIME_CHANGED is just a 1 second heartbeat and you are better off using a periodic @time_trigger.

If you want to eavesdrop on service calls, you could have a trigger on EVENT_CALL_SERVICE. The event parameters are:

  • domain
  • service
  • service_data

This trigger will log a message on every service call:

from homeassistant.const import EVENT_CALL_SERVICE

@event_trigger(EVENT_CALL_SERVICE) 
def monitor_service_events(domain=None, service=None, service_data=None):
    log.info(f"{domain}.{service} called with service_data={service_data}")

Trigger conditions can be added using a Python string expression in the 2nd argument. For example, to only trigger when domain is light:

@event_trigger("call_service", "domain == 'light'") 
def monitor_lights_service_events(service=None, service_data=None):
    log.info(f"light.{service} called with service_data={service_data}")

In this example the domain function argument was removed since it will always be light, and the event name was replaced by its value instead of importing it (although best practices would be to import and use the constant name).

You could also trigger on state change events. However, it's much better and more efficient to use @state_trigger instead. But if you want to see some or all state changes (rather than specific ones) you could trigger on EVENT_STATE_CHANGED. The parameters are:

  • entity_id - the string name of the state variable
  • new_state - the new value and attributes
  • old_state - the old value and attributes

Here's an example that triggers on all state changes for state variables whose names starts with "light.":

from homeassistant.const import EVENT_STATE_CHANGED

@event_trigger(EVENT_STATE_CHANGED, "entity_id.startswith('light.')") 
def monitor_state_change(entity_id=None, new_state=None, old_state=None):
    log.info(f"entity {entity_id} changed from {old_state} to {new_state}")

If you run this test you will see that old_state and new_state are objects. The actual string value is new_state.state and attributes are new_state.attributes, which is a dict. So you could update the log message to

from homeassistant.const import EVENT_STATE_CHANGED

@event_trigger(EVENT_STATE_CHANGED, "entity_id.startswith('light.')") 
def monitor_state_change(entity_id=None, new_state=None, old_state=None):
    old_value = old_state.state if old_state else None
    log.info(f"entity {entity_id} changed from {old_value} to {new_state.state} and attributes are now {new_state.attributes}")

Note that old_state could be None, which the code checks for.

Be careful about what event triggers you add, since every event of that type across HASS will cause your conditions to be checked and potentially your function to be run. A large HASS implementation will have a lot of state changes and service calls. Also, please don't set a state variable inside a function that is trigged by EVENT_STATE_CHANGED, or make a service call that is triggered by EVENT_CALL_SERVICE, unless the trigger condition is False in those cases. Otherwise bad things will happen...

Finally, if you are not sure what data is attached to an event, create a trigger with no additional conditions, and use the kwargs form to capture all the arguments in a dict:

from homeassistant.const import EVENT_STATE_CHANGED

@event_trigger(EVENT_STATE_CHANGED) 
def monitor_state_change(**kwargs):
    log.info(f"got EVENT_STATE_CHANGED with kwargs={kwargs}")

User Defined Events

HASS allows user-defined events to be fired. They have a string name, and optional parameters. You can use @event_trigger in just the same way as the examples above. When you create your own trigger name, you should make sure it isn't the same as any of the built-in triggers.

Here's an example:

@event_trigger("my_event")
def got_my_event(**kwargs):
    log.info(f"got my_event: kwargs={kwargs}")

You can fire the event and trigger the function with:

event.fire("my_event", my_param1=100, fruit="apple")

This will produce this output:

got my_event: kwargs={'trigger_type': 'event', 'event_type': 'my_event', 'my_param1': 100, 'fruit': 'apple'}

You can see the function is called with the two event parameters, and also a handful of other parameters that tell the function the type of trigger and name of the event.

Rather than use the catch-all **kwargs, you could explicitly specify the parameters. In this example we'll also add a trigger condition, so that the function only runs when my_param1 is between 50 and 150 (you could also include state variables in the condition if you want):

@event_trigger("my_event", "50 <= my_param1 <= 150")
def got_my_event(my_param1=None, fruit=None):
    log.info(f"got my_event: my_param1={my_param1}, fruit={fruit}")

Now fire the event three times; you should see that only one of the events causes the function to run because of the additional condition:

event.fire("my_event", my_param1=40, fruit="apple")
event.fire("my_event", my_param1=110, fruit="orange")
event.fire("my_event", my_param1=180, fruit="banana")

The output is:

got my_event: my_param1=110, fruit=orange