Ok, so there is technically a way you can do this with only creating one single helper, and you can use that same helper for every bulb and for anything else you want to have a persistent “last changed” recorded for. But it’s going to require templates. Lots of templates.
As a bit of a challenge and because I was interested myself, I created a single template sensor that can record the “last changed” timestamp of any entity that you apply a label of “State Tracker” to. Here is what that template sensor is. Since this is a trigger-based template sensor, you have to place this in your configuration.yaml, or your templates.yaml or however you have your config split up:
Click to expand template sensor YAML
templates:
- triggers:
- trigger: template
value_template: >
{% set entity_list_new = label_entities('state_tracker') %}
{% set entity_state_list_new = entity_list_new | map('states') | list %}
{% set states_dict_new = dict(zip(entity_list_new, entity_state_list_new)|list) %}
{% set states_dict_new_filtered = dict(states_dict_new.items() | rejectattr(1, 'in', ['unavailable', 'unknown'])) %}
{% set entity_list_old = state_attr('sensor.state_tracker', 'history').keys() | list %}
{% set entity_state_list_old = state_attr('sensor.state_tracker', 'history').values() | map(attribute='state') | list %}
{% set states_dict_old = dict(zip(entity_list_old, entity_state_list_old)|list) %}
{% set states_dict_old_filtered = dict(states_dict_old.items() | selectattr(0, 'in', states_dict_new_filtered.keys()) | list) %}
{% set entity_mismatch = (entity_list_new|sort != entity_list_old|sort) %}
{% set states_dict_filtered_mismatch = (states_dict_new_filtered != states_dict_old_filtered) %}
{{ entity_mismatch or states_dict_filtered_mismatch }}
- trigger: homeassistant
event: start
- trigger: event
event_type: "state_tracker_refresh"
sensor:
- name: State Tracker
device_class: timestamp
unique_id: 019c839f-3c7f-74fe-8589-b84436d254c2
state: "{{ now() }}"
attributes:
history: >
{% set keep_list = label_entities('state_tracker') %}
{% set t = this.attributes.get('history', {}) %}
{# remove entities no longer labelled #}
{% set t = dict(t.items() | selectattr('0', 'in', keep_list)) %}
{% set ns = namespace(entity_dict={}) %}
{% for entity in keep_list | expand %}
{% set state = t.get(entity.entity_id, {}).get('state') %}
{% set timestamp = t.get(entity.entity_id, {}).get('timestamp') %}
{% if state != entity.state and entity.state not in ['unavailable', 'unknown'] %}
{% set state = entity.state %}
{% set timestamp = entity.last_changed | as_local | string %}
{% endif %}
{% set updated_item = {entity.entity_id: {'state': state, 'timestamp': timestamp}} %}
{% set ns.entity_dict = dict(ns.entity_dict.items(), **updated_item) %}
{% endfor %}
{{ dict(ns.entity_dict) }}
What you end up with is a single sensor that has a history of the last time any labeled entity changed, and it ignores unavailable and unknown states and won’t update the timestamp if the same state happens again later. So restarting HA doesn’t affect the history. Here’s what it shows:
This works out really nice, and it is pretty easy to manage since you just have to label things. After adding or removing a label on an entity, you have to either restart, fire the custom event (state_tracker_refresh), or change the state of an already-tracked entity.
But the automation to use that data becomes a bit of a bear.
Here’s an automation example that uses some variables to attempt to make it easier, but it’s still kind of a mess.
Click to expand automation YAML
description: "Entity on Too Long"
mode: single
trigger_variables:
entity: light.living_room_dimmer
undesired_state: 'on'
duration: "00:05:00"
triggers:
- trigger: template
value_template: |-
{{ state_attr('sensor.state_tracker', 'history')[entity]['timestamp'] | as_datetime + as_timedelta(duration) < now()
and
state_attr('sensor.state_tracker', 'history')[entity]['state'] == undesired_state
}}
- trigger: homeassistant
event: start
conditions:
- condition: template
value_template: |-
{{ state_attr('sensor.state_tracker', 'history')[entity]['timestamp'] | as_datetime + as_timedelta(duration) < now()
and
state_attr('sensor.state_tracker', 'history')[entity]['state'] == undesired_state
}}
actions:
- action: persistent_notification.create
metadata: {}
data:
message: "{{ entity ~ ' has been ' ~ undesired_state ~ ' for ' ~ duration }}"
Whether doing all this is really easier than creating a per-entity helper is left for the user to decide…