Trace triggers from child automation

I would like to understand why an automation was triggered in the first place. It seems, contexts are just made for this. But I have not found out how this can be done.

My concrete use case is:

  • When I come home, this triggers a device tracker state change.
  • This state change triggers an automation to disarm the alarm.
  • The changed alarm state triggers an automation to notify me that “the alarm was disarmed”.

I want to notify “the alarm was disarmed, because someone came home”.

Is there a way? Contexts seem to be very limited when used in templates. But the info I want to use is there in the logbook…

Can’t you use the trigger variable?

Thanks for the suggestion! I checked and I don’t think there’s something useful in there. It says which state has changed (alarm) but that is not the original state change I am after (the device tracker)…

Here is what is available in the automations trigger variables:

Variables from the automation trace
this:
  entity_id: automation.haushalt_alarm_ausgelost
  state: 'on'
  attributes:
    id: '1696717259516'
    last_triggered: '2024-03-02T10:25:30.134242+00:00'
    mode: single
    current: 0
    friendly_name: Haushalt Alarm ausgelöst
  last_changed: '2024-02-26T23:42:32.991849+00:00'
  last_updated: '2024-03-02T10:25:32.719858+00:00'
  context:
    id: 01HQZB52GNSTGN3YPP5HVEVMCQ
    parent_id: 01HQZB52GEQ0S2ZKTTGJX9M88Q
    user_id: null
trigger:
  id: '0'
  idx: '0'
  alias: null
  platform: state
  entity_id: alarm_control_panel.alarm
  from_state:
    entity_id: alarm_control_panel.alarm
    state: pending
    attributes:
      code_format: number
      changed_by: null
      code_arm_required: false
      previous_state: armed_away
      next_state: triggered
      friendly_name: Alarm
      supported_features: 63
    last_changed: '2024-03-02T10:25:30.129506+00:00'
    last_updated: '2024-03-02T10:25:30.129506+00:00'
    context:
      id: 01HQZB52GEQ0S2ZKTTGJX9M88Q
      parent_id: 01HQZB52G9VZ8GCTBBS60YR4CD
      user_id: null
  to_state:
    entity_id: alarm_control_panel.alarm
    state: disarmed
    attributes:
      code_format: number
      changed_by: null
      code_arm_required: false
      friendly_name: Alarm
      supported_features: 63
    last_changed: '2024-03-02T10:30:46.994007+00:00'
    last_updated: '2024-03-02T10:30:46.994007+00:00'
    context:
      id: 01HQZBEQYGT1SXT9AEJAJ55D6P
      parent_id: 01HQZBEQYG467FEHY0XDNSH82E
      user_id: null
  for: null
  attribute: null
  description: state of alarm_control_panel.alarm

And this is what I’m after:

info shown in the logbook
Haushalt Alarm ausgelöst ausgelöst durch Automatisierung Haushalt Alarm schalten ausgelöst durch Zustand von ak-s20fe

11:30:46 - Vor 2 Stunden - Traces

The trigger should include what the device tracker is.
What is your automation?
Perhaps you can use the trigger I’d as the persons name kind of like what the example at the bottom is doing

As the actual trigger to change the alarm state is before that, I don’t think it will be easy to do this in a consistent way. I think you should set a helper in the automation that sets the alarm state. Because in the automation after that, you would only see that the automation to set the alarm state was the context. But that is not what you wanted to know. Then, in the notification, if the context was the automation to change the alarm state, you can get at the details of why the automation did it.

Also, besides the user, the context refers to things in the database that I think are not readyly accessable for the automation itself, but I may be mistaken. Otherwise, traversing up parent might get you the information you need.

So even if you use trigger id’s in the previous automation, it is still needed to check if the automation that set the helper has recently fired. Unless the user information shows it was a manual operation.

I use an input text helper and have the automation write what it does to that. This make it easy to see which trigger happened and the exact time.

- id: basement_light ms on
  alias: basement_light ms on
  mode: single
  max_exceeded: silent
  trigger:
    - platform: state
      entity_id: binary_sensor.workshop_motion_motion_detected
      to: "on"
      from: "off"
      id: workshop_motion
    - platform: state
      entity_id: binary_sensor.basement_sensor_motion_detected
      to: "on"
      from: "off"
      id: basement_sensor
    - platform: state
      entity_id: binary_sensor.workshop_door_open
      to: "on"
      from: "off"
      id: workshop_door
  condition:
    condition: and
    conditions:
      - "{{ states('input_boolean.lt_basement_light_control_enable') == 'on' }}"
      - "{{ is_state('binary_sensor.away_mode','off') }}"
      - "{{ is_state('switch.basement_light_1','off') or is_state('switch.basement_light_2','off') }}"
  action:
    - service: input_text.set_value
      data_template:
        entity_id: input_text.lt_basement_light_status
        value: "{{ 'on: ' + trigger.id + ' ' + now().strftime('%H:%M:%S.%f')[:-3] }}"
    - delay:
        milliseconds: 50
    - service: script.rs_basement_light_1_switch_turn_on
    - delay:
        milliseconds: 50
    - service: script.rs_basement_light_2_switch_turn_on

Thank you all for your hints and suggestions. I’m known to exaggerate, so I created a universal helper for such questions. My newest template sensor stores a cache of recently fired events with their context IDs.

YAML code
template:
- trigger:
  - platform: event
    event_type: '*'
  sensor:
  - name: Event Cache
    unique_id: event_cache
    state: listening
    attributes:
      timeout: >
        {{ this.attributes.get('timeout', '0:00:10') }}
      cache: >

        {# GET CURRENT STATE OF CACHE #}
        {% set cache = this.attributes.get('cache', {}) %}

        {# DO NOT UPDATE CACHE FOR CHANGES OF THE VERY CACHE #}
        {# ALSO DO NOT OVERWRITE EARLIER EVENTS #}
        {% if trigger.event.data.get('entity_id') == this.entity_id
           or trigger.event.context.id in cache %}
          {{ cache }}
        {% else %}

          {# USE A MACRO TO FLEXIBLY AND SAFELY CONVERT OBJECTS TO DICTIONARIES #}
          {% macro object_as_dict(object, type=None) %}

            {# THE STATE_CHANGED EVENT MIGHT CONTAIN UNSAFE ATTRIBUTES #}
            {% if type == 'event' and object.event_type == 'state_changed' %}              {% set object = {
                   'event_type': 'state_changed',
                   'data': {
                     'entity_id': object.data.entity_id,
                     'old_state': object_as_dict(object.data.old_state, type='state') | from_json,
                     'new_state': object_as_dict(object.data.new_state, type='state') | from_json,
                   },
                   'time_fired': object.time_fired.isoformat(),
                   'context': object.context.as_dict(),
              } %}

            {# A STATE CAN CONTAIN UNSAFE ATTRIBUTES #}
            {% elif type == 'state' %}
              {% set object = object.as_dict() %}
              {% set object = {
                    'entity_id': object.entity_id,
                    'state': object.state,
                    'attributes': object_as_dict(object.attributes if 'attributes' in object else {}, type='attributes') | from_json,
                    'last_changed': object.last_changed,
                    'last_updated': object.last_updated,
                    'context': object.context,
              } %}

            {# HANDLE ONLY SELECTED ATTRIBUTES #}
            {% elif type == 'attributes' %}
              {% set object =
                    dict(dict(
                      **{'icon': object.icon} if 'icon' in object else {}),
                      **{'friendly_name': object.friendly_name} if 'friendly_name' in object else {})
              %}

            {# JUST USE as_dict() PER DEFAULT #}
            {% else %}
              {% set object = object.as_dict() %}
            {% endif %}
            {{ object | to_json }}
          {% endmacro %}

          {# EXTEND CURRENT CACHE WITH NEW EVENT AND STRIP OLD CACHE ENTRIES #}
          {% set cache =
            dict(
              dict(
                cache,
                **{
                  trigger.event.context.id: object_as_dict(trigger.event, type='event') | from_json
                }
              ).items()
              | selectattr(
                  '1.time_fired',
                  '>=',
                  ( utcnow()
                  - this.attributes.get('timeout', '0:01:00')
                    | as_timedelta
                  ).isoformat()
                )
            )
          %}
          {{ cache }}
        {% endif %}

recorder:
  exclude:
    entities:
    - sensor.event_cache

The sensor can then be used to trace the event that actually triggered something.

YAML code
variables:
  cause: >-
    {%- set cache = state_attr('sensor.event_cache', 'cache') or {} %}


    {%- set ctx = trigger.to_state.context %}

    {%- set ctx = cache
                . get(ctx.parent_id or ctx.id, {})
                . get('context', ctx) %}
    {%- set ctx = cache
                . get(ctx.parent_id or ctx.id, {})
                . get('context', ctx) %}
    {%- set ctx = cache
                . get(ctx.parent_id or ctx.id, {})
                . get('context', ctx) %}
    {%- set evt = cache.get(ctx.id, trigger) %}


    {%- if evt.event_type == 'state_changed' -%}
      {%- set state = evt.data.new_state -%}
      {{ state.attributes.friendly_name }} wechselte zu {{ state.state }}
    {%- elif evt.event_type == 'call_service' and evt.context.user_id is not
    none -%}
      Manuelle Änderung
    {%- else -%}
      unbekannt ({{ evt | string }})
    {%- endif -%}

It’s not perfect yet and anyways far from ideal (as it’s a template), but it seems to work so far. I also opened a PR to put the information in the docs, that the context is not accessible by the user. :person_shrugging:

Then you should do that in your second step because it is monitoring the device_tracker’s state.

  1. This state change triggers an automation to disarm the alarm.

Your third step is monitoring the alarm state, not the device_tracker.

  1. The changed alarm state triggers an automation to notify me that “the alarm was disarmed”.

Therefore that automation’s context object will be for the alarm state, not the device_tracker.

That hasn’t been my experience, provided you understand how it works and why.
context

What I see here is that you submitted changes to the documentation despite only having a limited understanding of the context object’s purpose and how to use it (as it was intended) to determine who or what triggered the automation.


This may help to shed more light on how one can use the context object.

As to the constructive hint about in which automation to put the notification: I understand that the information is readily available in the first automation. However, I also notify for all other reasons. So, if I were to put the notifications in all places where the reason is readily available, that would cause a lot of duplication. Also, at least the manual disarm from the UI changes the alarm state directly. So, I would still have the notification in the second automation, together with some filtering based on the context. It might be opinionated, but I don’t like that.

Thanks for pointing me to your previous post. I had read this earlier and found it unfortunate that such information is only in some unauthoritative forum post and not in the documentation itself. That’s why I try to clarify the official documentation.

I am completely open to improvements. So, please, feel free to review the PR. I have even considered to include your thruth table. But, to my experience, your table is not complete. Since some triggers do not contain a context (e.g. time triggers), the parent_id will be None in the automation actions.

Finally, the link to the developers documentation was somewhat insightful in that it made me check for the original intentions. A few things are falling into place now (e.g. that context was indeed originally introduced to trace the cause of a change and that automations generate a child-context such that their actions are not run with user access). I’ll add that to the PR.

Use a script; pass information to it.

I’m not a member of the team that has privileges to accept/reject a documentation PR.

If I did, I would reject it based on the concern I shared earlier (and repeated below).

Yet more changing of the docs without a full understanding of the subject matter.