Template optimisation question for last_seen datetime sensor

@123 I wonder if you could help me with a template question please? I’ve been trying to follow a lot of your excellent template examples and experimenting with them in the template editor, but I’m stuck trying to count the number of entities that have a datetime attribute longer than x hours.

I have the following working template using for loops at the moment (along with associated automation for completeness)

template:
  - sensor:
      - unique_id: z2m_last_seen_entities
        name: "Z2M Last Seen Entities"
        state: >
          {% set lapsed_hours = 24 %}
          {% set ns = namespace(count=0) %}
          {% for state in states.sensor | selectattr('entity_id', 'search', '.*_last_seen$')  %}
            {% if states(state.entity_id) == 'unavailable' or ((as_timestamp(now()) - as_timestamp(states(state.entity_id))) > ((lapsed_hours | int) * 60 * 60)) %}
              {% set ns.count = ns.count + 1 %}
            {% endif %}
          {% endfor %}
          {{ ns.count }}     
        attributes:
          devices: >
            {% set lapsed_hours = 24 %}
            {% set result = namespace(sensors=[]) %}
            {% for state in states.sensor | selectattr('entity_id', 'search', '.*_last_seen$') %}
              {% if states(state.entity_id) == 'unavailable' or ((as_timestamp(now()) - as_timestamp(states(state.entity_id))) > ((lapsed_hours | int) * 60 * 60)) %}
                {% set result.sensors = result.sensors + [state.name | regex_replace(find='_last_seen', replace='') ~ ' (' ~ relative_time(strptime(states(state.entity_id), '%Y-%m-%dT%H:%M:%S%z', 'unavailable')) ~ ')'] %}
              {% endif %}
            {% endfor %}
            {{ result.sensors | join(', ') }}

automation:
  - alias: Offline Zigbee Devices
    id: offline_zigbee_devices
    description: Sends notification for offline Z2m devices
    trigger:
      - platform: time
        at: '20:00'
    condition:
      - condition: template
        value_template: '{{states(''sensor.z2m_last_seen_entities'')|int > 0}}'
    action:
      - service: notify.signal_justme
        data:
          title: Low battery alert
          message: '{% set phrase = ''s are '' if states(''sensor.z2m_last_seen_entities'')|int > 1 else '' is '' %} 
                    The following sensor{{ phrase }}missing: {{ state_attr(''sensor.z2m_last_seen_entities'', ''devices'') }}'

The problem though is that this gives me the following errors when rebooting HA, which I assume is because the data isn’t ready straight away:

2022-04-27 11:42:27 WARNING (MainThread) [homeassistant.helpers.template] Template warning: 'as_timestamp' got invalid input 'unknown' when rendering template '{% set lapsed_hours = 24 %} {% set result = namespace(sensors=[]) %} {% for state in states.sensor | selectattr('entity_id', 'search', '.*_last_seen$') %}
  {% if states(state.entity_id) == 'unavailable' or ((as_timestamp(now()) - as_timestamp(states(state.entity_id))) > ((lapsed_hours | int) * 60 * 60)) %}
    {% set result.sensors = result.sensors + [state.name | regex_replace(find='_last_seen', replace='') ~ ' (' ~ relative_time(strptime(states(state.entity_id), '%Y-%m-%dT%H:%M:%S%z', 'unavailable')) ~ ')'] %}
  {% endif %}
{% endfor %} {{ result.sensors | join(', ') }}' but no default was specified. Currently 'as_timestamp' will return 'None', however this template will fail to render in Home Assistant core 2022.1
2022-04-27 11:42:27 ERROR (MainThread) [homeassistant.helpers.event] Error while processing template: Template("{% set lapsed_hours = 24 %} {% set result = namespace(sensors=[]) %} {% for state in states.sensor | selectattr('entity_id', 'search', '.*_last_seen$') %}
  {% if states(state.entity_id) == 'unavailable' or ((as_timestamp(now()) - as_timestamp(states(state.entity_id))) > ((lapsed_hours | int) * 60 * 60)) %}
    {% set result.sensors = result.sensors + [state.name | regex_replace(find='_last_seen', replace='') ~ ' (' ~ relative_time(strptime(states(state.entity_id), '%Y-%m-%dT%H:%M:%S%z', 'unavailable')) ~ ')'] %}
  {% endif %}
{% endfor %} {{ result.sensors | join(', ') }}")
Traceback (most recent call last):
  File "/usr/lib/python3.9/site-packages/homeassistant/helpers/template.py", line 407, in async_render
    render_result = _render_with_context(self.template, compiled, **kwargs)
  File "/usr/lib/python3.9/site-packages/homeassistant/helpers/template.py", line 1820, in _render_with_context
    return template.render(**kwargs)
  File "/usr/lib/python3.9/site-packages/jinja2/environment.py", line 1291, in render
    self.environment.handle_exception()
  File "/usr/lib/python3.9/site-packages/jinja2/environment.py", line 926, in handle_exception
    raise rewrite_traceback_stack(source=source)
  File "<template>", line 2, in top-level template code
TypeError: unsupported operand type(s) for -: 'float' and 'NoneType'

What I would like to do is optimise the code and avoid these errors with something like the following examples, but I can’t figure out what the code should be to manipulate the datetime state from the Zigbee2MQTT *_last_seen entity (see screenshot below)

        https://community.home-assistant.io/t/turn-off-all-light-on-from-3hours-not-using-group/345013/6?u=jarrah
        This template lists all lights that have been on for more than 3 hours.
        {{ states.light | selectattr('state', 'eq', 'on')
           | selectattr('last_changed', 'lt', now() - timedelta(hours=3)) 
           | map(attribute='entity_id') | join(', ') }}

        {%- set threshold = 40 -%}
        {{ states.sensor | selectattr('entity_id', 'search', '^(?!.*energy).*_power$')
                  | map(attribute='state') | map('int', 0) | select('<', threshold) | list | count }}

        https://community.home-assistant.io/t/improvement-to-sensor-template-with-for-loop/360550/12?u=jarrah
        {{ state_attr('weather.kmrb_hourly', 'forecast') 
          | selectattr('datetime', '<=', today_at('21:00').isoformat())
          | map(attribute='temperature')
          | max }}

I can count the total number of entities found like so:

{{ states.sensor | selectattr('entity_id', 'search', '.*_last_seen$')  | map(attribute='entity_id') | list | count }}

However, the following gives an error that says TypeError: '<' not supported between instances of 'str' and 'datetime.datetime', so I assume it’s because the state value is a string rather than datetime object?

        {{ states.sensor | selectattr('entity_id', 'search', '.*_last_seen$') 
            | selectattr('state', 'lt', now() - timedelta(hours=3)) 
            | map(attribute='entity_id') | list | count }}

Thanks in advance.

There’s no need to tag me directly. This community has nearly 131 thousand members so someone is likely to come to your assistance.

Your template is using functions/filters that require a default value.

I do apologise, I guess it was because I was trying to use some of the examples you provided in other threads, but you are right there was no need for me to tag you specifically. :slightly_frowning_face:

Ah of course! This seems to have done the trick:

as_timestamp(states(state.entity_id),0))
template:
  - sensor:
      - unique_id: z2m_last_seen_entities
        name: "Z2M Last Seen Entities"
        state: >
          {% set lapsed_hours = 24 %}
          {% set ns = namespace(count=0) %}
          {% for state in states.sensor | selectattr('entity_id', 'search', '.*_last_seen$')  %}
            {% if states(state.entity_id) == 'unavailable' or ((as_timestamp(now()) - as_timestamp(states(state.entity_id),0)) > ((lapsed_hours | int) * 60 * 60)) %}
              {% set ns.count = ns.count + 1 %}
            {% endif %}
          {% endfor %}
          {{ ns.count }}     
        attributes:
          devices: >
            {% set lapsed_hours = 24 %}
            {% set result = namespace(sensors=[]) %}
            {% for state in states.sensor | selectattr('entity_id', 'search', '.*_last_seen$') %}
              {% if states(state.entity_id) == 'unavailable' or ((as_timestamp(now()) - as_timestamp(states(state.entity_id),0)) > ((lapsed_hours | int) * 60 * 60)) %}
                {% set result.sensors = result.sensors + [state.name | regex_replace(find='_last_seen', replace='') ~ ' (' ~ relative_time(strptime(states(state.entity_id), '%Y-%m-%dT%H:%M:%S%z', 'unavailable')) ~ ')'] %}
              {% endif %}
            {% endfor %}
            {{ result.sensors | join(', ') }}

Thank you @ 123