How I "fixed" the real last changed status of sensors (restart/reload resistant) on my cards

Disclaimer: There’s nothing new that does not really exist in this post. It brings together ideas and recipes from other contributors. I’m just sharing them together. Attributions at the end.

In this post I explain how I get the real last open/close status of my doors and windows, and the real last detected for PIR. I share the code for dashboard that shows their real last changed and status, and they are also ordered by last changed (optional).

Result:

You can either use one group and one template trigger OR use different groups per sensors. See explanation in step 2) for choosing what’s best for you.

Steps:

  1. Create 2 or 3 groups (One or two for windows and doors), one for presence detectors (PIR and others)
  2. Create template triggers for each group. You will need to manually list of the sensors of this group in the template sensor. That’s my main remaining “problem” (see “Bonus” below).

I use different template triggers because I may have different triggers for each group. For instance, I use “to: on” for Presence Sensors. I consider that a door being open or closed is a sign of presence. However, for detectors, I consider it applies only when it goes to “on”. You may want to keep the “any change state” behavior if you consider that it’s important for you to know when a presence sensor goes to “off”. I think it may be relevant for millimeter-wave sensors like Aqara FP1/2.

Here’s the code in configuration.yaml for 3 groups:

template:     
  - trigger:
      - platform: state
        entity_id:
            - binary_sensor.buanderie_presence
            - binary_sensor.cellier_motion_sensor
            - binary_sensor.couloir_motion_sensor
        to:
          - "on"
        not_from:
          - unavailable
          - unknown
    sensor:
      unique_id: dbf841e2-ae6f-4fc1-9534-a10462040001
      name: Presence Sensors Change History
      state: "{{ trigger.entity_id }}"
      attributes:
        changes: >
          {% set current = this.attributes.get('changes', {}) %}
          {% set new = {trigger.entity_id: trigger.to_state.last_changed.isoformat()} %}
          {{ dict(current, **new) }}
  - trigger:
      - platform: state
        entity_id:
            - binary_sensor.bureau_julien_fenetre
            - binary_sensor.bureau_morgane_fenetre
            - binary_sensor.wc_fenetre
        not_to:
          - unavailable
          - unknown
        not_from:
          - unavailable
          - unknown
    sensor:
      unique_id: dbf841e2-ae6f-4fc1-9534-a10462040000
      name: Window Sensors Change History
      state: "{{ trigger.entity_id }}"
      attributes:
        changes: >
          {% set current = this.attributes.get('changes', {}) %}
          {% set new = {trigger.entity_id: trigger.to_state.last_changed.isoformat()} %}
          {{ dict(current, **new) }}
  - trigger:
      - platform: state
        entity_id:
            - binary_sensor.buanderie_porte
            - binary_sensor.cuisine_porte
            - binary_sensor.exterieur_portail
        not_to:
          - unavailable
          - unknown
        not_from:
          - unavailable
          - unknown
    sensor:
      unique_id: dbf841e2-ae6f-4fc1-9534-a10462040002
      name: Door Sensors Change History
      state: "{{ trigger.entity_id }}"
      attributes:
        changes: >
          {% set current = this.attributes.get('changes', {}) %}
          {% set new = {trigger.entity_id: trigger.to_state.last_changed.isoformat()} %}
          {{ dict(current, **new) }}   

This is the type of sensor it automatically creates:

Bonus (not required for the cards): An alternative for the latest activated binary sensor of a group) using a template trigger

template:
  # https://community.home-assistant.io/t/automation-trigger-on-group-of-sensors-and-sent-specific-triggering-sensor-in-notification-so-close/375213/15?u=mincka 
  - sensor:
      - name: Latest Opening
        state: >
          {% set x = expand('binary_sensor.ouvertures') | selectattr('state', 'eq', 'on') | sort(attribute='last_changed', reverse=true) | list %}
          {{ (x[0].entity_id if now() - x[0].last_changed < timedelta(seconds=3) else '') if x | count > 0 else '' }}

Sadly, I have issues to leverage this “Latest Opening” sensor ,which benefits from reading groups, with my trigger sensor. This is because there are conflicts when multiple sensors are updated simultaneously. Ultimately, it would be perfect to get TheFes trick work with a group.

  1. Use custom:auto-entities for the Lovelace card.

This part, that is the real “new” thing of this thread, was a little bit tricky because I wanted the sorting by last change and we must use a template filter to achieve this. There are also tricks to show the relative time as secondary attribute (the main purpose of this post), and to show the active color. If you don’t want the sorting by last changed, it’s easier to configure the card.

To get the proper labels for “on”/“off”, udpate the last part of the templates. For instance, I use iif("Detected","Clear") for the presence detectors card, instead of iif("Open","Closed").

type: custom:auto-entities
card:
  type: entities
  title: Portes
  state_color: true
filter:
  template: >
    {% set entities = state_attr('sensor.door_sensors_change_history',
    'changes') | dictsort(false, 'value', reverse = True) %}

    [{% for entity in entities %}

    {"entity": "{{entity[0]}}", "name": "{{state_attr(entity[0],
    'friendly_name')}}", "type": "custom:template-entity-row", "secondary": "{{
    relative_time(state_attr("sensor.door_sensors_change_history",
    "changes")[entity[0]]| as_datetime)}} ago", "color": "{{ is_state(entity[0],
    "on") | iif("var(--state-active-color)") }}", "state": "{{
    is_state(entity[0], "on") | iif("Open","Closed") }}"},

    {% endfor %}]
sort:
  method: none
  1. A markdown card for the latest status of each history sensor:

image

{% set entities = state_attr('sensor.door_sensors_change_history', 'changes') | dictsort(false, 'value', reverse = True) %}
{% set entity_id = entities[0][0] %}
{% set entity_name = state_attr(entity_id, 'friendly_name') %}
{% set entity_last_update = entities[0][1] %}
{% set datetime = (entity_last_update| as_timestamp | timestamp_custom('%Y-%m-%d %H:%M:%S', True)) %}
{% set relative_datetime = relative_time(strptime(datetime,"%Y-%m-%d %H:%M:%S")) %}

<ha-icon icon="mdi:door-open"></ha-icon> &nbsp;{{ entity_name }} - {{ relative_datetime }} ago
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Le {{ entity_last_update | as_timestamp  | timestamp_custom('%d/%m/%Y')}} à {{ entity_last_update | as_timestamp | timestamp_custom('%H:%M:%S') }}


{% set entities = state_attr('sensor.presence_sensors_change_history', 'changes') | dictsort(false, 'value', reverse = True) %}
{% set entity_id = entities[0][0] %}
{% set entity_name = state_attr(entity_id, 'friendly_name') %}
{% set entity_last_update = entities[0][1] %}
{% set datetime = (entity_last_update| as_timestamp | timestamp_custom('%Y-%m-%d %H:%M:%S', True)) %}
{% set relative_datetime = relative_time(strptime(datetime,"%Y-%m-%d %H:%M:%S")) %}

<ha-icon icon="mdi:motion-sensor"></ha-icon> &nbsp;{{ entity_name }} - {{ relative_datetime }} ago
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Le {{ entity_last_update | as_timestamp  | timestamp_custom('%d/%m/%Y')}} à {{ entity_last_update | as_timestamp | timestamp_custom('%H:%M:%S') }}

{% set entities = state_attr('sensor.window_sensors_change_history', 'changes') | dictsort(false, 'value', reverse = True) %}
{% set entity_id = entities[0][0] %}
{% set entity_name = state_attr(entity_id, 'friendly_name') %}
{% set entity_last_update = entities[0][1] %}
{% set datetime = (entity_last_update| as_timestamp | timestamp_custom('%Y-%m-%d %H:%M:%S', True)) %}
{% set relative_datetime = relative_time(strptime(datetime,"%Y-%m-%d %H:%M:%S")) %}

<ha-icon icon="mdi:window-open"></ha-icon> &nbsp;{{ entity_name }} - {{ relative_datetime }} ago
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Le {{ entity_last_update | as_timestamp  | timestamp_custom('%d/%m/%Y')}} à {{ entity_last_update | as_timestamp | timestamp_custom('%H:%M:%S') }}
  1. Open/close and enable all the sensors at least one time to initialize everything. You may get a few errors in the logs during the setup.

Attribution:
@TheFes

@123Taras

@thomasloven for Auto-Entities

Related WTH topic, please vote!

7 Likes

It’s also possible to use it with secondaryinfo-entity-row (with card-tools dependency) like this:

type: entities
entities:
  - entity: binary_sensor.porte_d_entree
    state_color: true
    type: custom:secondaryinfo-entity-row
    secondary_info: >-
      {{ relative_time(state_attr("sensor.door_sensors_change_history",
      "changes")["binary_sensor.porte_d_entree"] | as_datetime)}} - entity - ago

I did not manage to use the special “{entity}” variable for self-referencing when using Jinja templates, so you must reference the entity id of the current entity. It would have been great to use “entity” or “this” to reference the current entity.

1 Like

I am at step 2 and trying to incorporate a dynamic assignment of entities to be tracked as I have quite a few and to make it future proof.
When I process the template in the editor the visual render is like the code but it doesn’t get interpreted within the platform

  - trigger:
      - platform: state
        entity_id: >
          {%- set ns = namespace(doorsensors=[]) -%}
          {%- set ns.doorsensors = states.binary_sensor
          | selectattr('attributes.device_class', 'defined')
          | selectattr('attributes.device_class', 'in', ['door','window','garage_door'])
          | rejectattr('entity_id', 'in', 'binary_sensor.jardin_boiteauxlettres_volet_contact')
          | rejectattr('entity_id', 'in', 'binary_sensor.jardin_boitealait_contact')
          | map(attribute='entity_id') | list -%}
          {{ '- ' + ns.doorsensors | join('\n' + '- ')}}
        not_to:
          - unknown
        not_from:
          - unknown
    sensor:
      unique_id: historique_ouverture_portefenetre
      name: Historique ouverture porte et fenetre
      state: "{{ trigger.entity_id }}"
      attributes:
        changes: >
          {% set current = this.attributes.get('changes', {}) %}
          {% set new = {trigger.entity_id: trigger.to_state.last_changed.isoformat()} %}
          {{ dict(current, **new) }}

any hints to get this to go through?