Trigger based template sensor to store global variables

Hi, i think i can really make use of this instead of creating helpers for each parameters of every new user.

Could I create this kind of setup per user

variables:
  username1: 
     option1: true
     option2: 30
  username2:
     option1: true
     option2: 30

Also, creating or changing a variable can only be done in an automation and script, right? It can’t be done, for example, in Developper Tools → Template?

okay, I’ve figured it out the first question of my post:

In Developper Tools → Events

event type:
set_variable

Event data (YAML, optional)
key: user1
value: 
  option1: true
  option2: 43

In Developper Tools → Template

{{ state_attr('sensor.variables', 'variables')['user1'].value['option1'] }}
{{ state_attr('sensor.variables', 'variables')['user1'].value['option2'] }}

True
43

You can fire an event in developer tools > event

Could you elaborate on how I could achieve this.

I’m looking to store user’s parameters. I might not have a huge list of parameters but might end up with many users. Both list could grow so I’m trying to plan ahead.

Note: I don’t need this tomorrow. In my current situation, this is already much better than creating a list of helpers for every new user.

Note2: I’m also considering the possibility of having each user assigned to a category. Something similar to a user level like beginner, intermediate, advanced…

Thanks for this which I’ve implemented and it works a treat. I thought I could adapt it to keep track of who is using my HA instance. It works sometimes, but periodically fails, resetting the trigger entity to unavailable and losing the stored data. I can’t work out how to debug it and the log just contains enigmatic statements which don’t tell me which part of the YAML is failing.
I’ve created 2 sensors: the first successfully keeps track of the most recent user to log on or off:

  - sensor:
    - name: browser_users_last
      state: >
         {% set lastone = integration_entities('browser_mod')|select('contains','browser_user')|reject('contains','agent')|expand|sort(attribute='last_updated')|last %}
         {{ lastone.name.split(' Browser')[-2] }}
      attributes:
        when: >
         {% set lastone = integration_entities('browser_mod')|select('contains','browser_user')|reject('contains','agent')|expand|sort(attribute='last_updated')|last %}
         {{ lastone.last_changed }}
        who: >
         {% set lastone = integration_entities('browser_mod')|select('contains','browser_user')|reject('contains','agent')|expand|sort(attribute='last_updated')|last %}
         {{ lastone.state }}
        prev: >
          {% set lastone = integration_entities('browser_mod')|select('contains','browser_user')|reject('contains','agent')|expand|sort(attribute='last_updated')|last %}
          {% set id = lastone.name.split(' Browser')[-2] %}
          {% if id in state_attr('sensor.browser_users_database', 'database') %}
            {{ state_attr('sensor.browser_users_database', 'database')[id].when }}
          {% else %}
            {{ now().isoformat }}
          {% endif %}

Then comes the adapted version from the above discussion. It’s triggered by any change of the sensor.browser_users_last defined above and the database should produce a dict of ids with the time of each id’s most recent change in status, previous change and the accumulated time (ignoring time not logged on).

  - trigger:
    - platform: state
      entity_id: sensor.browser_users_last
    sensor:
      - name: browser_users_database
        unique_id: browser_users_database
        state: "testing"
        attributes:
          when: "{{ trigger.to_state.attributes.when }}"
          id: "{{ trigger.to_state.state }}"
          olddb: "{{ this.attributes.get('database', {}) }}"
          last: >
            {% if trigger.to_state.state in this.attributes.get('database', {}) %}
              {{ this.attributes.database[trigger.to_state.state].when }}
            {% endif %}
          database: >
            {% set id = trigger.to_state.state %}
            {% set others = dict(this.attributes.get('database', {}).items() | rejectattr('0', 'eq', id)) %}
            {% set when  = trigger.to_state.attributes.when %}
            {% set last  = trigger.to_state.attributes.prev~'trig' %}
            {% if id in this.attributes.get('database', {}) %}
              {% set last = this.attributes.get('database')[trigger.to_state.state].when %}
            {% endif %}
            {% set for = this.attributes.get('database')[trigger.to_state.state].for | float(0) %}
            {% if state_attr('sensor.browser_users_last','who') == 'unavailable'%}
              {% set for = for + as_timestamp(when, as_timestamp(now())) - as_timestamp(last, as_timestamp(now())) %}
            {% endif %}
            {% set for = max( for, 0)| round(0) %}
            {% set new = {id: {'prev': last, 'when': when, 'for': for }} %}
            {{ dict(others, **new) }}

Wen it stops working it does not re-create itself like the variables one above does.
The log contains lines like:

Logger: homeassistant.helpers.sensor
Source: helpers/trigger_template_entity.py:204
First occurred: 15:07:05 (41 occurrences)
Last logged: 17:05:39

Error rendering state template for sensor.browser_users_database: UndefinedError: 'None' has no attribute 'DJI-P9_l-app'

so not telling me which bit went wrong. I haven’t been able to test things using developer tools because of the trigger. and this. parts of the YAML.
Any pointers on offer? Please?

You can create a file notification sensor and write the values of this and trigger into a file for debugging in your automation.

More options:

Write to the HA log file: System Log - Home Assistant.

Use the dev tools and listen for the event.

Thanks, I’ve never done this before but I’ll see if I can work out how to do it. I guess I’ll have to write lots of separate details to see which go wrong.

FYI the solution was to put all the processing in the sensor that triggers the database update. There are now trigger.attributes for each item stored in the list and this seem to have done the trick.
Thanks again for a great template, which has solved problems that have been bugging me for months.

1 Like

I spoke too soon. When I restarted HA this morning the database got wiped. I suspect it was due to the trigger sensor going unavailable & triggering before it’s attributed had been set.

Can you share the YAML of what you now used?

Sorry, I missed your response. I’ve fixed the “database wiped” issue by adding not_to: unavailable but I’m still getting wrong data being added. Is it possible for the trigger sensor to be triggered BEFORE all the attributes of the sensor triggering it have been rendered? I added a 1sec delay to avoid this, but 2 attributes of the trigger entity which look-up data from the triggered sensor seem to use the value from after the event not before it. This is what I’d using for the trigger entity:

  - sensor:
    - name: browser_users_last
      unique_id: browser_users_last
      state: >
         {% set lastone = integration_entities('browser_mod')|select('contains','browser_user')|reject('contains','agent')|expand|sort(attribute='last_updated')|last %}
         {{ lastone.name.split(' Browser')[-2] }}
      attributes:
        when: >
         {% set lastone = integration_entities('browser_mod')|select('contains','browser_user')|reject('contains','agent')|expand|sort(attribute='last_updated')|last %}
         {{ lastone.last_changed }}
        who: >
         {% set lastone = integration_entities('browser_mod')|select('contains','browser_user')|reject('contains','agent')|expand|sort(attribute='last_updated')|last %}
         {{ lastone.state }}
        user: >
          {% set lastone = integration_entities('browser_mod')|select('contains','browser_user')|reject('contains','agent')|expand|sort(attribute='last_updated')|last %}
          {% set id = lastone.name.split(' Browser')[-2] %}
          {% set who = lastone.state %}
          {% set usr = who %}
          {% if id in state_attr('sensor.browser_users_db', 'database') %}
            {% set entry = state_attr('sensor.browser_users_db', 'database')[id] %}
            {% if "user" in entry %} 
              {% set usr = entry.user %}
            {% elif who == 'unavailable' %}
              {% set usr = '' %}
            {% endif %}
          {% endif %}
          {{ usr }}
        prev: >
          {% set lastone = integration_entities('browser_mod')|select('contains','browser_user')|reject('contains','agent')|expand|sort(attribute='last_updated')|last %}
          {% set id = lastone.name.split(' Browser')[-2] %}
          {% if id in state_attr('sensor.browser_users_db', 'database') %}
            {% set entry = state_attr('sensor.browser_users_db', 'database')[id] %}
            {% set prev = iif( 'when' in entry, entry.when, now().isoformat) %}
          {% else %}
            {% set prev = now().isoformat() %}
          {% endif %}
          {{ prev }}
        cum: >
          {% set lastone = integration_entities('browser_mod')|select('contains','browser_user')|reject('contains','agent')|expand|sort(attribute='last_updated')|last %}
          {% set id = lastone.name.split(' Browser')[-2] %}
          {% set when = as_timestamp(as_datetime(lastone.last_changed)) %}
          {% set who = lastone.state %}
          {% set for = 0 %}
          {% if id in state_attr('sensor.browser_users_db', 'database') %}
            {% set entry = state_attr('sensor.browser_users_db', 'database')[id] %}
            {% set prev = iif('when' in entry, entry.when, when) %}
            {% set for = iif('for' in entry, entry.for, 0) %}
            {% if who == 'unavailable' %}
              {% set for = max(for + when - as_timestamp(prev), 0) | round(0) %}
            {% endif %}
          {% endif %}
          {{ for }}

And the data store:

  - trigger:
    - platform: state
      entity_id: sensor.browser_users_last
      not_to: 
        - "unavailable" 
      for: 
        seconds: 1
    sensor:
      - name: browser_users_db
        unique_id: browser_users_db
        state: "{{ trigger.to_state.state }}"
        attributes:
          database: >
            {% set id = trigger.to_state.state %}
            {% set others = dict(this.attributes.get('database', {}).items() | rejectattr('0', 'eq', id)) %}
            {% set usr  = trigger.to_state.attributes.user %}
            {% set when = trigger.to_state.attributes.when %}
            {% set last = trigger.to_state.attributes.prev %}
            {% set cum  = trigger.to_state.attributes.cum %}
            {% set new = {id: {'user': usr, 'prev': last, 'when': when, 'for': cum }} %}
            {{ dict(others, **new) }}

Is it possible to use the last value of a trigger sensor in its template trigger?
Also, I keep having to recalculate things like id - is it possible to read the new value of state in each attribute instead?

In case it helps others, I’m definitely seeing unexpected values being passed between the trigger entity and the sensor it triggers I decided to try to put all the processing in the triggered sensor. In the code below I’m using a modified trigger whose state contains both the browser id and the time it was last updated. The / between them allows the ID to be obtained. The database looks like this and seems to be doing all the right things now.

          database: >
            {% set id = trigger.to_state.state.split(' /')[-2] %}
            {% set db = dict(this.attributes.get('database', {}).items() | selectattr('0', 'eq', id)) %}
            {% set others = dict(this.attributes.get('database', {}).items() | rejectattr('0', 'eq', id)) %}
            {% set when = trigger.to_state.attributes.when %}
            {% set who = trigger.to_state.attributes.who %}
            {% set prev, dur = trigger.to_state.attributes.when, 0 %}
            {% set usr  = trigger.to_state.attributes.user %}
            {% if db[id] != undefined %}
              {% set prev =  db[id].when if db[id].when != undefined %}
              {% set dur = db[id].for | int(0) if db[id].for != undefined %}
              {% set usr  = db[id].user if db[id].user != undefined %}
            {% endif %}
            {% if who == 'unavailable' %}
              {% set dur = max(dur + as_timestamp(when, 0) - as_timestamp(prev, 0), 0) | round(0) %}
            {% endif %}
            {% set new = {id: {'user': usr, 'prev': prev, 'when': when, 'for': dur }} %}
            {{ dict(others, **new) }}
1 Like

I started using this template, and think it’s absolutely brilliant. I did just have an issue where during one of the reboots of my system after updating some integration, all the stored values in the attribute just disappeared. Anyone have an idea what may have caused that and how to prevent that from happening in the future (outside of not restarting ever :wink:)?

in case anyone comes back to this thread and their variables are gone… I added triggers for startup and shutdown of HA to serialize the contents of the sensor into an MQTT topic. the sensor checks at startup if the sensor is empty and loads them from the MQTT topic. not very elegant, but adds a bit of safety to this.


- trigger:
    - platform: event
      event_type: set_global_variable
      id: set_global_variable
    - platform: event
      event_type: remove_global_variable
      id: remove_global_variable
    - platform: event
      event_type: clear_global_variables
      id: clear_global_variables
    - platform: state
      entity_id: sensor.global_variables
      attribute: variables      
      id: update_mqtt_topic
    - platform: event
      event_type: update_mqtt
      id: update_mqtt_topic
    - platform: homeassistant
      event: start
      id: ha_start
    - platform: homeassistant
      event: shutdown
      id: ha_shutdown
  condition:
    - condition: template
      value_template: >
        {{
          trigger.id in ['ha_start', 'ha_shutdown', 'update_mqtt_topic'] or
          (
            trigger.id == 'set_global_variable'
            and trigger.event.data is defined
            and trigger.event.data.key is defined
            and trigger.event.data.value is defined
          ) or
          (
            trigger.id == 'remove_global_variable'
            and trigger.event.data is defined
            and trigger.event.data.key is defined
          ) or
          trigger.id == 'clear_global_variables'
        }}
  action:
    - choose:
        - conditions: "{{ trigger.id not in ['ha_start', 'ha_shutdown', 'update_mqtt_topic'] and trigger.event.data.get('log', state_attr('sensor.global_variables', 'log_events')) }}"
          sequence:
            - service: logbook.log
              data:
                name: "{{ trigger.id }}_global_variable:"
                message: >
                  {{ trigger.event.data.key | default('All variables removed') }}
                  {%- if trigger.id == 'set_global_variable' -%}
                    : {{ trigger.event.data.value }}.
                  {%- endif -%}
        - conditions: "{{ trigger.id not in ['ha_start'] }}"
          sequence:
            - service: mqtt.publish
              data:
                topic: "global_variables/mqtt_persistent_global_variables"
                payload: >
                  {{ {
                    "state": now().isoformat(),
                    "vars": state_attr('sensor.global_variables', 'variables')
                  } | tojson }}
                retain: true
  sensor:
    - unique_id: ui_beach_global_variables
      name: Global Variables
      state: Variables
      attributes:
        default_timestamp: true
        log_events: false
        variables: >  
          {% set current_vars = this.attributes.get('variables', {}) %}
          {# LOAD DATA FROM MQTT IF THE VALUE WAS EMPTY DURING STARTUP - id = ha_start#}
          {% if trigger.id == 'ha_start' %}
            {% set mqtt_data = state_attr('sensor.mqtt_persistent_global_variables', 'vars') %}
            {# Ensure mqtt_data is a valid string before decoding #}
              {% if mqtt_data and mqtt_data | trim | lower not in ['unknown', 'unavailable', 'none', ''] %}
                {% if mqtt_data is string %}
                  {% set mqtt_data_json = mqtt_data | from_json %}
                {% else %}
                  {% set mqtt_data_json = mqtt_data %}
                {% endif %}
              {% else %}
                {% set mqtt_data_json = {} %}
              {% endif %}
              {# Use mqtt_data_json if current_vars is empty #}
              {% if current_vars | length == 0 %}
                {{ mqtt_data_json }}
              {% else %}
                {{ current_vars }}
              {% endif %}
          {% elif trigger.id in ['set_global_variable', 'remove_global_variable', 'clear_global_variables'] %}
          {# PROCESS THE COMMANDS - id = set_global_variable, remove_global_variable, or clear_global_variables #}
            {# handle empty vars #}
            {% set key = trigger.event.data.key|default('') %}
            {% if current_vars | length == 0 %}
              {% set others = {} %}
            {% else %}
              {% set others = dict(current_vars.items() | rejectattr('0', 'eq', key)) %}
            {% endif %}
            {# SET a global variable with timestamp #}
            {% if trigger.id == 'set_global_variable'
                and trigger.event.data.get('set_timestamp', this.attributes.get('default_timestamp', false)) %}
              {% set val = trigger.event.data.value %}
              {% set val = val.isoformat() if val is datetime else val %}
              {% set new = { key: {'value': val, 'timestamp': now().isoformat()} } %}
              {{ dict(others, **new) }}
            {# SET a global variable #}
            {% elif trigger.id == 'set_global_variable' %}
              {% set new = { key: trigger.event.data.value } %}
              {{ dict(others, **new) }}
            {# REMOVE a global variable #}
            {% elif trigger.id == 'remove_global_variable' %}
              {{ others }}
            {# CLEAR all global variables #}
            {% elif trigger.id == 'clear_global_variables' %}
              {}
            {% endif %}
          {% else %}
          {# RETURN CURRENT VARIABLES - id = ha_shutdown or mqtt_update #}
            {{ current_vars }}
          {% endif %}

EDIT: modified the template so it updates MQTT with any update to the global variables.

1 Like

Another simple solution for your particular use case would be to use custom attributes for the entity that represents your users. No automations no muss no fuss. Permanently store the values right in your configuration file. I use this technique quite a bit in my config. It works very well.

homeassistant:
  customize:
    person.username1:
      option1: true
      option2: 30

    person.username2:
      option1: true
      option2: 30

Retrieve the attributes with
state_attr('person.username2', 'option2')

1 Like

Hello and thank you for this nice template, I am using it with some sensors for opening times across reboots :slight_smile:
I got one recommendation to TheFes :slight_smile: Could you please edit your first post and add the final/actual solution to it ?

I had to read the whole post to get your updated sensor and that may be a problem for some users ?

Greetings from Austria

Kilowatt

2 Likes

I’ve updated the post

1 Like