Proof of concept: Real last_change Saver

Many of us struggle with automations that they use the last_changed attribute.
You know, a restart, a disconnection breaks them.
In search of a solution I came to the idea of building a blueprint to let me (and potentially other users) get the real datetime of the last change of an entity, with the help of a helper.

Prerequisites
An input_text helper to be used as our “saver” of the entities.

How it works
When setting up an automation, the blueprint asks for the entities (not more than 4 because of the 255 characters limit of the input_text state) and the helper from above.
On a state change of these entities the automation runs, saving the entity_id, state before change, last_changed timestamp before change, state after change and last_changed timestamp after change.
It automatically ignores the states: “unknown”, “unavailable”, None.

The format is as follow:
<entity_id1>,<from_state.state>=<from_state.last_changed>,<to_state.state>=<to_state.last_changed>&<entity_id2>,<from_state.state>=<from_state.last_changed>,<to_state.state>=<to_state.last_changed>

Open your Home Assistant instance and show the blueprint import dialog with a specific blueprint pre-filled.

blueprint:
  name: Real "last_changed" saver
  description: Saves the from and to states, with the timestamps, from entities given in an input_text helper, surviving restarts and disconnections.
  domain: automation
  input:
    source_entities:
      name: Entities (max. 4, limit to 255 charactes)
      description: The watched entities to pursue their real last_changed state & timestamps
      selector:
        entity:
          multiple: true
    target_list:
      name: Input_text helper
      description:  In this list helper will be saved all the required info
      selector:
        target:
          entity:
            domain: input_text
variables:
  inlist: !input 'target_list'
  input_list: '{{ inlist.entity_id }}'
  inent: !input 'source_entities'
trigger:
  - platform: state
    entity_id: !input 'source_entities'
condition:
  - condition: template
    value_template: >
      {{ states(input_list) != None 
            and trigger.platform == "state"
            and trigger.from_state.state != trigger.to_state.state }}
action:
  - choose:
      - conditions:
          - condition: template
            value_template: '{{ trigger is defined and trigger.platform == "state" }}'
        sequence:
          - service: input_text.set_value
            data:
              value: >
                {% set list =
                namespace(l=states(input_list).split('&'),n=[]) %} {% for i in
                list.l %}
                  {% if "," in i
                        and i.split(",")[0] in inent %}
                    {% set list.n = list.n + [i] %}
                  {% endif %}
                {% endfor %} {% set list.l = list.n %} {% if not
                trigger.entity_id in list.l | join()
                           and not trigger.from_state.state in ["unavailable","unknown",None] 
                           and not trigger.to_state.state in ["unavailable","unknown",None] %}
                     {% set list.l = list.l + [trigger.entity_id ~ "," ~ trigger.from_state.state ~ "=" ~ trigger.from_state.last_changed.replace(microsecond=0) | as_timestamp | int ~ "," ~ trigger.to_state.state ~"="~ trigger.to_state.last_changed.replace(microsecond=0) | as_timestamp | int] %}
                {% else %}
                  {% set list.n = [] %}
                  {% for n in list.l %}
                    {% set en = n.split(',')[0] %}
                    {% set fs = n.split(',')[2].split('=')[0] %}
                    {% set fst = n.split(',')[2].split('=')[1] %}
                    {% set ist =  iif( (trigger.from_state.state != fs 
                                        and trigger.to_state.state != fs
                                        and not (trigger.to_state.state in ["unavailable","unknown",None]) ), true, false) %}
                    {% if en == trigger.entity_id
                               and ( ( not trigger.from_state.state in ["unavailable","unknown",None]  
                                        and not trigger.to_state.state in ["unavailable","unknown",None] )
                                    or ist ) %}
                      {% set list.n = list.n + [trigger.entity_id ~ "," ~ iif(not ist, trigger.from_state.state, fs) ~ "=" ~ iif(not ist,n.split(',')[2].split('=')[1],fst) ~ "," ~ trigger.to_state.state ~"="~ trigger.to_state.last_changed.replace(microsecond=0) | as_timestamp | int] %}
                    {% else %}
                      {% set list.n = list.n + [n] %}
                    {% endif %}
                  {% endfor %}
                {% set list.l = list.n %} {% endif %} {{ list.l | join("&") }}
            target: !input 'target_list'
    default: []
mode: queued
max: 10
6 Likes

The following template is needed to get the values from the helper:

{% set helper = "<helper name>" %}
{% set name = "<entity name>" %}

{% set list = states(helper).split('&') %}
{% set date = namespace(fl= states | selectattr('entity_id','eq',name) | map(attribute='last_changed') | first ,
                        fs= "",
                        tl= states | selectattr('entity_id','eq',name) | map(attribute='last_changed') | first,
                        ts= "") %}
{% for n in list %}
  {% set date.fl = iif( n.split(',')[0] == name, n.split(',')[1].split('=')[1] | as_datetime, date.fl ) %}
  {% set date.fs = iif( n.split(',')[0] == name, n.split(',')[1].split('=')[0], date.fs ) %}
  {% set date.tl = iif( n.split(',')[0] == name, n.split(',')[2].split('=')[1] | as_datetime, date.tl ) %}
  {% set date.ts = iif( n.split(',')[0] == name, n.split(',')[2].split('=')[0], date.ts ) %}
{% endfor %}

Change the <helper name> and <entity name> with your needs and you got the following variables:
date.fl datetime from the last_changed before the change
date.fs state before the change
date.tl datetime from the last_changed after the change
date.ts state after the change

Now you got the idea…
… can you build a custom component with it?

Features:
Surving HA restarts and disconnections.
If a change made during restart/disconnection it catches it and sets the from_state attribute to the real last known last_changed (the saved one…)

Doesn’t seem to work for me…

What’s the issue you have?

Your blueprint is intended for entities, as I understand it. I experimented with this, though, and tried to store the state of “person.name,” which resulted in an error. I’ll import the blueprint again whenever I have time to get the log message.

Alternative approach:

1 Like