Hi @Quaternion
Prerequisite to run this script is that you have installed the custom integration pyscript (e.g. via HACS), where you probably have to enable these two options in the config:
- hass_is_global: true
- allow_all_imports: true
The script will run autonomously by itself (the examples in the posts above runs every 30 minutes). The script does two things:
- persistent notifications that list the devices that are unavailable
- creates a sensor that you can use for e.g. dashboards and/or automations;
pyscript.device_availability_checker
. The state represents the number of unavaliable devices. An attribute lists the details
Note: In my post above I only improved the detection of the original script, no personal tailoring, in order to stay close to the original script.
Then I tailored it fully to my use case:
- I don’t want notifications as in my setup as there are always unavailable devices (ie HUE lights and Chromecasts physically turned off) → so I made notifications an option (default=off)
- Increased the execution rate of the the script to once per minute
- Changed the purpose of
ignore_entities
; When evaluating the entities of each device, the state of those entities is ignored, not the whole device!
- added a test script to evaluate one device in the dev tools
My personalized version of the script:
ignore_devices = [
# Add ids of devices to be ignored here.
# The template {{ device_id('entity id') }} can be used to find device id's
]
ignore_entities = [
# Add entities that should be ignored when evaluating their device state
]
extend_integrations = [
# Add integrations whose entities are added to devices but can remain available
"powercalc","nmap_tracker"]
CHECKER_ID = "pyscript.device_availability_checker"
from homeassistant.helpers import entity_registry as erm
from homeassistant.helpers import device_registry as drm
from homeassistant.const import STATE_UNAVAILABLE
@service(supports_response="only")
def device_availability_test(device_id: str):
"""yaml
description: Test the device_availability_checker result for a specific device
fields:
device_id:
description: device id to be tested
required: true
default: ''
example: 2e6ec7645070322e572e1e161306c2f7
selector:
text:
"""
if device_id in ignore_devices:
return {"result": "ignored device" }
er = erm.async_get(hass)
dr = drm.async_get(hass)
import json
result=[]
for e in erm.async_entries_for_device(er, device_id):
useForCheck = "Yes"
if ( # check if entity should be ignored
(e.original_device_class == "connectivity") or
(e.platform in extend_integrations) or
(e.entity_id in ignore_entities)
):
useForCheck = "Ignored"
#log.warning(f"avaliablity test entity {e.entity_id}")
result.append( {"entity": e.entity_id, "part_of_check": useForCheck, "available": "No" if hass.states.is_state(e.entity_id, STATE_UNAVAILABLE) else "Yes", "platform":e.platform, "device_class": e.original_device_class} )
return {"result": result }
@time_trigger('cron(* * * * *)')
def device_availability_checker(doNotify=False):
er = erm.async_get(hass)
dr = drm.async_get(hass)
unavailable_devices = {}
unavailable_since = {}
# Iterate over all devices and check whether they are unavailable
# A device is supposed to be unavailable if all related entities are in
# state "unavailable". Except some added entities by other integrations, and connectivity types
for d in dr.devices:
if d in ignore_devices:
continue
unavailable = False
since = None
for e in erm.async_entries_for_device(er, d):
if ( # check if entity should be ignored
(e.original_device_class == "connectivity") or
(e.platform in extend_integrations) or
(e.entity_id in ignore_entities)
):
continue
elif hass.states.is_state(e.entity_id, STATE_UNAVAILABLE):
unavailable = True
since = hass.states.get(e.entity_id).last_changed
else:
unavailable = False
break
if unavailable:
unavailable_devices[d] = dr.async_get(d)
unavailable_since[d] = since
notifs = []
devices = {}
# Iterate over all unavailable devices and construct notification text
# for a persistent_notification
for k, d in unavailable_devices.items():
name = d.name if d.name_by_user is None else d.name_by_user
manufacturer = d.manufacturer
model = d.model
area = d.area_id or ""
integration="unknown"
#ids = d.identifiers
#if ids is not None and len(ids):
# integration = str(list(ids)[0][0])
entry = hass.config_entries.async_get_entry(entry_id=d.primary_config_entry)
if entry is not None:
integration = entry.domain
if doNotify:
text = f'- {name}'
desc = ""
if model is not None:
desc += model
if manufacturer is not None:
if desc != "":
desc += f" [{manufacturer}]"
else:
desc += manufacturer
if desc != "":
text += "\n - " + desc
if area is not None:
text += f"\n - Area: {area}"
text += f"\n - Integration: {integration}"
text += f"\n - Since: {unavailable_since[k]}"
text += f"\n - ID: {d.id}"
notifs.append(text)
devices[k] = {
"integration": integration,
"area": area,
"name": name,
"manufacturer": manufacturer,
"model": model,
"since": unavailable_since[k]
}
if doNotify:
# Show a persistent notification or dismiss an old one if there is nothing
# to show
ntext = "\n".join(notifs)
if ntext != "":
hass.services.async_call("persistent_notification", "create", {
"notification_id": "device_availability_warning",
"title": f"List of {len(devices)} Unavailable Devices",
"message": ntext
}, False)
else:
hass.services.async_call("persistent_notification", "dismiss", {
"notification_id": "device_availability_warning"
}, False)
state.set(CHECKER_ID, len(devices), devices = devices)
I created a straight forward dashboard to display them. I used a combi of the mark down and card custom:auto-entities to list the devices. Further I styled the card using card_mod (you can safely remove that ) and used the custom relative_time_plus template to display how long devices are unavailable. You optionally can use the standard available relative_time
instead. All these custom items can be installed via HACS.
Note: In this card you can tap on a device to open the device page!
YAML of my card
type: custom:auto-entities
card:
type: entities
card_mod:
style: |
:host {
--ha-card-border-width: 0px;
}
.card-content {
margin-left: -50px;
}
filter:
template: >
{%- set devlist =
state_attr('pyscript.device_availability_checker','devices') or [] %}
{%- set ns=namespace(rows=[]) -%} {%- from 'relative_time_plus.jinja'
import relative_time_plus -%} {%- for k in devlist -%}
{%- set info=devlist[k] -%}
{%- set sec_info = 'Area:'~info.area~', Mdl:'~info.model -%}
{%- set entry = {
'type': 'custom:template-entity-row',
'name': info.integration~': '~info.name,
'state': relative_time_plus(info.since,abbr=true),
'secondary': sec_info,
'since': info.since.timestamp(),
'tap_action': {
'action': 'navigate',
'navigation_path': '/config/devices/device/'~k },
} -%}
{%- set ns.rows= ns.rows + [entry] -%}
{%- endfor -%}
{{ ns.rows | sort(attribute='since', reverse = True)}}
include: []
exclude: []
sort:
method: none