Identifying and alerting on "Missing Devices"

I have been looking for a way to be able to select a few devices, a group of devices, something like that, then alert when they are not seen over a given period of time. Overall goal is to be able to tell when my zigbee2mqtt door sensor batteries die, for instance. I’ve been looking for a way to do this for QUITE a while now and found one blueprint that came close but it seems to basically just say “hey…devices are missing”. I couldn’t find a way to tell it which devices I wanted it to pay attention to, change the check in duration, etc.

Does anyone know a way to do this? Any ideas would be GREATLY appreciated.

Thank you!!

There are loads of blueprints that warn you when battery entities are running low.

Why would you wait until a device is unavailable when you can be warned before it becomes a problem?

Because a low battery isn’t the only reason a device may be unavailable.

Device_id’s don’t do templates.
You need to use entity_id’s.

I have built a Blueprint exactly for this purpose and other use cases (DS18B20 sensors vanishing, seasonal sensors exceptions, etc.)
Some battery powered devices just go from OK to zero in a matter of minutes so tracking battery level is not bulletproof.
The concept here is simply to assign label(s) to device entities that typically change through time (Temperature, Humidity, Signal, RSSI, Uptime, Memory, etc.) or manually identify entities that could vanish entirely (DS18B20/Tasmota sensors); the Blue print will notify you once the entity stops updating, goes offline or vanishes after a defined period of time.
I am using Pushover notifications but you can use another notify service.
I have been testing it for a few months and really like it so far.
I don’t have time to document my blueprint so haven’t posted it officially but feel free to try it, it could help with your use case. You’ll have to do manual installation.
Let me know if you have any questions or feedback.

blueprint:
  name: Sensor Integrity Monitor (Stale, Offline or Vanished)
  description: Alerts if sensors vanish, go offline, or stop updating. Supports separate thresholds for Stale vs Offline, perpetual seasonal windows, and robust Sun-based monitoring.
  domain: automation
  input:
    check_interval:
      name: Check Interval (Hours)
      description: How often to run the check automatically.
      default: 3
      selector:
        number:
          min: 1
          max: 24
          unit_of_measurement: h
    threshold_stale_mins:
      name: Stale Threshold (Minutes)
      description: Max time allowed since last state change for "Healthy" sensors.
      default: 180
      selector:
        number:
          min: 5
          max: 1440
          unit_of_measurement: min
    threshold_offline_mins:
      name: Offline Threshold (Minutes)
      description: Grace period for sensors in "unavailable" or "unknown" state before alerting.
      default: 30
      selector:
        number:
          min: 0
          max: 1440
          unit_of_measurement: min
    target_labels:
      name: Labeled Entities
      description: Entities with these labels will be monitored 24/7.
      default: []
      selector:
        label:
          multiple: true
    vanishing_entities:
      name: Vanishing Entities (Manual)
      description: "Entities that might vanish entirely (e.g., Tasmota + DS18B20 sensors)."
      default: []
      selector:
        entity:
          multiple: true
    
    # --- SUN-BASED DAYTIME MONITORING ---
    daytime_entities:
      name: "Daylight Only Entities"
      description: Entities to monitor only between Sunrise and Sunset.
      default: []
      selector:
        entity:
          multiple: true
    sunrise_offset:
      name: "Sunrise Offset (Minutes)"
      default: 30
      selector:
        number: { min: -120, max: 120, unit_of_measurement: min }
    sunset_offset:
      name: "Sunset Offset (Minutes)"
      default: -30
      selector:
        number: { min: -120, max: 120, unit_of_measurement: min }

    # --- PERPETUAL SEASONAL GROUPS ---
    exclude_entities_a:
      name: "Group A: Seasonal Entities"
      default: []
      selector:
        entity: { multiple: true }
    start_date_a:
      name: "Group A: Start Monitoring On"
      default: "2024-05-01"
      selector: { date: {} }
    end_date_a:
      name: "Group A: Stop Monitoring On"
      default: "2024-10-15"
      selector: { date: {} }

    exclude_entities_b:
      name: "Group B: Seasonal Entities"
      default: []
      selector:
        entity: { multiple: true }
    start_date_b:
      name: "Group B: Start Monitoring On"
      default: "2024-11-01"
      selector: { date: {} }
    end_date_b:
      name: "Group B: Stop Monitoring On"
      default: "2024-03-15"
      selector: { date: {} }

    notify_service:
      name: Notification Service
      default: notify.pushover
      selector:
        text: { multiline: false }

mode: single

trigger:
  - trigger: time_pattern
    hours: "/1"

action:
  - variables:
      conf_interval: !input check_interval
      input_start_a: !input start_date_a
      input_end_a: !input end_date_a
      input_start_b: !input start_date_b
      input_end_b: !input end_date_b
      is_manual: "{{ trigger.platform is none or trigger.platform != 'time_pattern' }}"
      
  - condition: template
    value_template: "{{ (now().hour % conf_interval | int) == 0 or is_manual }}"
  
  - variables:
      today: "{{ now().strftime('%j') | int }}"
      
      # Seasonal Logic
      start_a: "{{ as_timestamp(now().strftime('%Y-') ~ input_start_a[5:10], 0) | timestamp_custom('%j') | int }}"
      end_a: "{{ as_timestamp(now().strftime('%Y-') ~ input_end_a[5:10], 0) | timestamp_custom('%j') | int }}"
      should_monitor_a: "{% if start_a <= end_a %}{{ start_a <= today <= end_a }}{% else %}{{ today >= start_a or today <= end_a }}{% endif %}"
      
      start_b: "{{ as_timestamp(now().strftime('%Y-') ~ input_start_b[5:10], 0) | timestamp_custom('%j') | int }}"
      end_b: "{{ as_timestamp(now().strftime('%Y-') ~ input_end_b[5:10], 0) | timestamp_custom('%j') | int }}"
      should_monitor_b: "{% if start_b <= end_b %}{{ start_b <= today <= end_b }}{% else %}{{ today >= start_b or today <= end_b }}{% endif %}"

      # Sun-Based Logic
      is_daylight: >
        {% set sun = states.sun.sun %}
        {% if sun %}
          {{ (state_attr('sun.sun', 'elevation') | default(-10) > -5) }} 
        {% else %}
          false
        {% endif %}

      # Assemble final list
      entities_a: !input exclude_entities_a
      entities_b: !input exclude_entities_b
      ignored_seasonal: "{{ (entities_a if not should_monitor_a else []) + (entities_b if not should_monitor_b else []) }}"
      
      day_entities: !input daytime_entities
      ignored_daylight: "{{ day_entities if not is_daylight else [] }}"
      
      all_exclusions: "{{ (ignored_seasonal + ignored_daylight) | unique | list }}"

      # Master Monitor Queue
      target_labels: !input target_labels
      vanishing_entities: !input vanishing_entities
      all_labeled: >
        {% set ns = namespace(entities=[]) %}
        {% for l in target_labels %}
          {% set ns.entities = ns.entities + label_entities(l) %}
        {% endfor %}
        {{ ns.entities }}
      
      all_to_monitor: >
        {% set combined = (all_labeled + vanishing_entities + day_entities) | unique | list %}
        {{ combined | reject('in', all_exclusions) | list }}

      # Processing with Dual Thresholds
      thresh_stale: !input threshold_stale_mins
      thresh_offline: !input threshold_offline_mins

      stale_entities: >
        {% set ns = namespace(stale=[]) %}
        {% for eid in all_to_monitor %}
          {% set s = states[eid] %}
          {% if s is none %}
            {% set ns.stale = ns.stale + [ eid ~ " (VANISHED)" ] %}
          {% else %}
            {% set last_ts = as_timestamp(s.last_changed, 0) %}
            {% set diff = (as_timestamp(now()) - last_ts) / 60 %}
            {% set a = area_name(eid) %}
            {% set name = state_attr(eid, 'friendly_name') or ( (a ~ ": " if a else "") ~ eid) %}
            
            {% if s.state in ['unavailable', 'unknown'] %}
               {% if diff >= thresh_offline %}
                  {% set ns.stale = ns.stale + [ name ~ " (OFFLINE)" ] %}
               {% endif %}
            {% else %}
               {% if diff >= thresh_stale %}
                  {% set last_seen = s.last_changed | as_local %}
                  {% set timestamp = last_seen.strftime('%b %d, %I:%M %p') %}
                  {% set ns.stale = ns.stale + [ name ~ " (STALE @ " ~ timestamp ~ ")" ] %}
               {% endif %}
            {% endif %}
          {% endif %}
        {% endfor %}
        {{ ns.stale }}

  - if:
      - condition: template
        value_template: "{{ stale_entities | count > 0 }}"
    then:
      - service: !input notify_service
        data:
          title: "Sensor Integrity Alert"
          message: "The following sensors require attention:\n{{ stale_entities | join('\n') }}"
          data:
            priority: 2
            sound: siren
            retry: 3600
            expire: 86400