Статья

Отображение недоступных устройств в Home Assistant

Эта статья является логическим продолжением моей предыдущей статьи, в которой изложены базовые сведения о сущностях и их состояниях, а также основы шаблонизации, на базе разработки сенсора состояний заряда батарей датчиков.

Как известно, в Home Assistant все устройства "дробятся" на сущности (объекты), с которыми в дальнейшем и работает система. Их "object_id", как правило, одинаково начинается с упоминания родительского устройства.

Типовые сущности разных устройств

При недоступности родительского устройства, становятся недоступны и все дочерние объекты. А их колличество, порой, может переваливать за десяток, и при выводе их всех в интерфейс, присутствует некая избыточность информации. Мне подумалось, что было бы удобным отображать просто названия устройств.

Кроме того, некоторые устройства сезонные и это надо учесть. Для автоматизаций и сценариев у меня в системе созданы объекты input_boolean для каждого такого устройства, с соответствующими условиями (conditions) в автоматизациях. Увлажнитель убрал, "input_boolean.humidifier_connected" перевел в "false" - автоматизация не сработает. На таком же принципе датчик будет отслеживать недоступность устройств.

Ещё в процессе изучения вопроса выяснилось, что удобно создать постоянную группу с объектами, которые будут игнорироваться этим датчиком.

Техзадание:

  • вывести в интерфейс удобочитаемую информацию о недоступных устройствах (не сущностях);
  • исключить из списка отключенные мною устройства (я на лето убираю увлажнитель);
  • исключить объекты, которые вводят в заблуждение при формировании списка устройств;
  • при необходимости, полная информация о недоступных сущностях;
  • не использовать сторонние интеграции для интерфейса (используем Markdown).

Устройства  ( DEVICES )

В разделе "Templating" инструкции к HA есть раздел "DEVICES". За него и зацепимся.

В системе каждому устройству (и службе) присваивается идентификатор "device_id" (32-значное HEX-число). Его, кстати, можно увидеть  в конце адресной строки браузера, когда находишься на странице устройства. Дальше приведу несколько примеров работы с device_id на примере бесперебойника фирмы APC с соответствующей интеграцией:

yaml
Копировать
  {# Вывод идентификатора устройства (ID) на основе одной из сущностей этого устройства: #}

{{ device_id('sensor.ups_transfer_to_battery') }}

-> e46086d4e8bb0c2eb3ebcc0e67d68da5

  {# Все сущности устройства на основе его ID #}

{{ device_entities('e46086d4e8bb0c2eb3ebcc0e67d68da5') }}

-> ['binary_sensor.ups_online_status', 'sensor.ups_input_voltage', 'sensor.ups_battery_timeout',
'sensor.ups_battery_shutdown', 'sensor.ups_time_left', 'sensor.ups_transfer_from_battery',
'sensor.ups_daemon_info', 'sensor.ups_status_data', 'sensor.ups_status', 'sensor.ups_time_on_battery',
'sensor.ups_name', 'sensor.ups_hostname', 'sensor.ups_model', 'sensor.ups_status_date', 'sensor.ups_load',
'sensor.ups_cable_type', 'sensor.ups_shutdown_time', 'sensor.ups_battery', 'sensor.ups_mode',
'sensor.ups_startup_time', 'sensor.ups_driver', 'sensor.ups_transfer_to_battery'] {# Производитель устройства на основе ID: #} {{ device_attr('e46086d4e8bb0c2eb3ebcc0e67d68da5', 'manufacturer') }} -> APC {# Проверка на соответствие устройства производителю: #} {{ is_device_attr('sensor.ups_transfer_to_battery', 'manufacturer', 'APC') }} -> True {# Вывод системного имени (имя, которое присваивает интеграция, кстати редкий случай, когда его нет): #} {{ device_attr('sensor.ups_transfer_to_battery', 'name') }} -> None {# Вывод имени устройства, которое дал пользователь: #} {{ device_attr('sensor.ups_transfer_to_battery', 'name_by_user') }} -> Back-UPS ES 525

Кроме того, у многих устройств можно узнать: hw_version - версия железа;
sw_version - версия программного обеспечения;
model - ну вы поняли;
и другие, иногда индивидуальные, аттрибуты.

Хочу уточнить, что названия устройств будут считываться из аттрибута "name_by_user", а если он пустует , то из аттрибута "name", то есть, если шаблон не найдет имени устройства, заданного пользователем, то будет использоваться имя системное. Поэтому стоит назвать свои устройства так, чтобы удобно было это потом воспринимать. Делается это как раз на странице устройства. В верхнем правом углу нажимаем на карандаш и переименовываем. Название устройства здесь же, но в левом углу.

При этом не надо соглашаться на переименовывание идентификаторов объектов, если только вы не хотите переписать все изменившиеся сущности в автоматизациях и пр. !

Жмём "Нет" !

Прежде чем начать описывать датчик, уточню:

  1.  У меня для каждого сезонного устройства есть свой input_bulean , которым я блокирую срабатывание автоматизаций, если устройство отключено. Этот логический переключатель будет учитываться в сенсоре.
  2. Я заранее создал ещё один  input_bulean "input_boolean.visible_unav_ent", который будет отвечать за отображение списка недоступных сущностей в карточке Markdown.
  3. В ходе экспериментов выяснилось, что датчик бесперебойника "sensor.ups_transfer_to_battery" (время последнего перехода на работу от батареи), после перезагрузки HA становится недоступным, что не влияет на работу самого бесперебойника и HA, поэтому для подобных проблемных сенсоров я создал группу игнорируемых недоступных объектов "group.ignored_unavailable_entities", которая просматривается во время формирования списка недоступных устройств.

Датчик

Решено было сделать датчик на основе интеграции "Template", в состоянии (state) которого будет количество недоступных сущностей, и, дополнительно, в нем будет словарь с тремя аттрибутами: кол-во недоступных устройств, список недоступных устройств и список недоступных сущностей.

Сенсор (колличество недоступных сущностей):

python
Копировать
    {# Присваиваем переменной nm ссылку на глобальную функцию NAMESPACE, а в качестве ее аттрибута #}
    {# переменную list_unav_ent , представляющую собой пустой список [] #}

{% set nm = namespace(list_unav_ent = [] ) %}   

    {# Присваиваем переменной domains ссылку на список [], #}
    {# в который входят нужные нам домены, которые дальше будут просматриваться #}

{% set domains = ['light', 'switch', 'sensor', 'binary_sensor'] %}

    {# Новая переменная domain, которая ссылается поочереди на каждый объект в списке domains #}
    {# пока не кончатся все объекты #}

{% for domain in domains -%}     
  
    {# Очередная новая переменная state1 которая будет ссылаться на сущности домена, #}
    {# которые недоступны, но при соблюдении еще одного, причем двойного условия: #}
    {# еcли input_boolean.humidifier_connected сейчас вЫключен, #}
    {# то сущности, у которых object_id начинаестя со слова humidifier игнорируются #}

{% for state1 in states[domain] if ((state1.state | lower == "unavailable") 
and not (( is_state('input_boolean.humidifier_connected', 'off')            
and  (state1.object_id.startswith("humidifier"))))) %}                     
         
    {# Пополняем наш список nm.list_unav_ent #}
    {# В него добавляется очередная сущность, прошедшая тесты #}
                                                                
{% set nm.list_unav_ent = nm.list_unav_ent + [state1.entity_id] %}

    {# Заканчиваем тесты этой сущности и переходим к следующей #}
      
{% endfor %} 

    {# Переходим к следующему домену #}
    
{% endfor %} 

    {# Вывод колличества элементов, получившегося списка #}
  
{{ nm.list_unav_ent | list | length }}

Аттрибуты:

Аттрибут - список недоступных сущностей ( то же самое, но без подсчёта элементов списка, а сам список ): 

python
Копировать
{% set nm = namespace(list_unav_ent = [] ) %}
{% set domains = ['light', 'switch', 'sensor', 'binary_sensor'] %}
  {% for domain in domains -%}
    {% for state1 in states[domain] if ((state1.state | lower == "unavailable")
    and not (( is_state('input_boolean.humidifier_connected', 'off') 
    and  (state1.object_id.startswith("humidifier"))))) %}
      {% set nm.list_unav_ent = nm.list_unav_ent + [state1.entity_id] %}
    {% endfor %} 
  {% endfor %} 
{{ nm.list_unav_ent }}

Аттрибут - список недоступных устройств:

python
Копировать
    {# Как и в предыдущем случае, создаем глоб. переменные NAMESPACE #}
    {# но в этот раз одну для сущностий и одну для устройств #}

{% set nm = namespace(list_unav_ent = [] ) %}
{% set nm1 = namespace( list_unav_dev = []) %}

    {# Как и в коде выше, формируем список недоступных сущностей #}

{% set domains = ['light', 'switch', 'sensor', 'binary_sensor'] %}
  {% for domain in domains -%}
    {% for state1 in states[domain] if ((state1.state | lower == "unavailable")) %}
    
    {# В этот раз будем проверять наличие объекта в группе игнорируемых #}
    {# Переменной "ignored" присваивается состояние аттрибута "entity_id" группы #}
    {# Значение этого аттрибута - список внесенных в него игн. сущностей #}
    
      {% set ignored = state_attr('group.ignored_unavailable_entities','entity_id') %}
      
    {# Проверяем, находится ли наш очередной объект в этом списке #}
      
      {% if state1.entity_id not in ignored %}
      
    {# Если нет, добавляем его в список недоступных сущностей #}
      
        {% set nm.list_unav_ent = nm.list_unav_ent + [state1.entity_id] %}
      {% endif %}
    {% endfor %} 
  {% endfor %} 
  
    {# Список сущностей, с отсеиванием игнорируемых, составили #} 
    {# Теперь, на основе списка формируем список устройсв #}  
    {# Переменной unav_ent будет присваиваться каждый объект списка #}    
  
{% for unav_ent in nm.list_unav_ent  -%}
  
    {# Тест: Если аттрибут "name_by_user" родительского устройства = "ничего" #}
  
{% if (device_attr(unav_ent, "name_by_user") is none) %}
    
    {# Вписываем в переменную "nav_dev" имя из аттрибута "name" #}
    
{% set unav_dev = device_attr(unav_ent, "name") %}
      
    {# Иначе #}
      
{% else %}
    
    {# Вписываем в переменную "nav_dev" имя из аттрибута "name_by_user" #}
    
{% set unav_dev = device_attr(unav_ent, "name_by_user") %}
      
    {# Конец проверки аттрибутов и выбора имени устройству #}
      
{% endif %}
    
    {# Наполняем наш список недоступных устройств, добавив один объект #}
    
{% set nm1.list_unav_dev = nm1.list_unav_dev + [unav_dev] %}
    
    {# Переходим к следующему элементу списка "nm.list_unav_ent" #}
    
{% endfor %}
  
    {# формируем фильтр "Увлажнитель". #}
    {# Присваиваем переменной "ignore_1" пустоту #}
  
{% set ignore_1 = "" %}

    {# Если "input_boolean.humidifier_connected"  выключен (увлажнитель убран) #}

{% if is_state("input_boolean.humidifier_connected", "off") %}

    {# Переприсваиваем переменной "ignore_1" значение "Увлажнитель" #}

{% set ignore_1 = "Увлажнитель" %}
  
    {# Конец проверки #}
  
{% endif %}

    {# Вывод списка недоступных устройств. При этом используем фильтры: #}
    {# "| unique" отрежет все повторяющиеся названия, оставив одно #}
    {# "| reject("==", ignore_1)" удалит из списка слово "Увлажнитель", при условии, что он убран #}
    {# "| list"  собственно, список #}

{{ nm1.list_unav_dev
| unique
| reject("==", ignore_1)
| list
}}

Аттрибут - колличество недоступных устройств

python
Копировать
    {# Кроме альтернативно расположенных проверок сущностей на наличие в группе #}
    {# и принадлежности к увлжнителю, код повторяет предыдущие #}

{% set nm = namespace(list_unav_ent = [] ) %}
{% set nm1 = namespace( list_unav_dev = []) %}
{% set domains = ['light', 'switch', 'sensor', 'binary_sensor'] %}
  {% for domain in domains -%}
    {% for state1 in states[domain] if ((state1.state | lower == "unavailable")
    and not (( is_state('input_boolean.humidifier_connected', 'off') 
    and  (state1.object_id.startswith("humidifier"))))) %}
      {% set ignored = state_attr('group.ignored_unavailable_entities','entity_id') %}
      {% if state1.entity_id not in ignored %}
        {% set nm.list_unav_ent = nm.list_unav_ent + [state1.entity_id] %}
      {% endif %}
    {% endfor %} 
  {% endfor %} 
  {% for unav_ent in nm.list_unav_ent  -%}
    {% if (device_attr(unav_ent, "name_by_user") is none) %}
      {% set unav_dev = device_attr(unav_ent, "name") %}
    {% else %}
      {% set unav_dev = device_attr(unav_ent, "name_by_user") %}
    {% endif %}
    {% set nm1.list_unav_dev = nm1.list_unav_dev + [unav_dev] %}
  {% endfor -%}
{{ nm1.list_unav_dev
| unique
| list
| length 
}} 

Полный код датчика

yaml
Копировать
- trigger:
    - platform: homeassistant
      event: start
    - platform: time_pattern
      minutes: "/20"
    - platform: state
      entity_id: input_boolean.visible_unav_ent
      to:
    - platform: state
      entity_id: input_boolean.humidifier_connected
      to:
  sensor:
    - name: unavailable entities
      unit_of_measurement: entities
      state: >
            {% set nm = namespace(list_unav_ent = [] ) %}
            {% set domains = ['light', 'switch', 'sensor', 'binary_sensor'] %}
              {% for domain in domains -%}
                {% for state1 in states[domain] if ((state1.state | lower == "unavailable")
                and not (( is_state('input_boolean.humidifier_connected', 'off') 
                and  (state1.object_id.startswith("humidifier"))))) %}
                  {% set nm.list_unav_ent = nm.list_unav_ent + [state1.entity_id] %}
                {% endfor %} 
              {% endfor %} 
            {{ nm.list_unav_ent | list | length }}
            
      attributes:
      
        list_of_unavailable_entities: >
            {% set nm = namespace(list_unav_ent = [] ) %}
            {% set domains = ['light', 'switch', 'sensor', 'binary_sensor'] %}
              {% for domain in domains -%}
                {% for state1 in states[domain] if ((state1.state | lower == "unavailable")
                and not (( is_state('input_boolean.humidifier_connected', 'off') 
                and  (state1.object_id.startswith("humidifier"))))) %}
                  {% set nm.list_unav_ent = nm.list_unav_ent + [state1.entity_id] %}
                {% endfor %} 
              {% endfor %} 
            {{ nm.list_unav_ent }}
            
        list_of_unavailable_devices: >
            {% set nm = namespace(list_unav_ent = [] ) %}
            {% set nm1 = namespace( list_unav_dev = []) %}
            {% set domains = ['light', 'switch', 'sensor', 'binary_sensor'] %}
              {% for domain in domains -%}
                {% for state1 in states[domain] if ((state1.state | lower == "unavailable")) %}
                  {% set ignored = state_attr('group.ignored_unavailable_entities','entity_id') %}
                  {% if state1.entity_id not in ignored %}
                    {% set nm.list_unav_ent = nm.list_unav_ent + [state1.entity_id] %}
                  {% endif %}
                {% endfor %} 
              {% endfor %} 
              {% for unav_ent in nm.list_unav_ent  -%}
                {% if (device_attr(unav_ent, "name_by_user") is none) %}
                  {% set unav_dev = device_attr(unav_ent, "name") %}
                {% else %}
                  {% set unav_dev = device_attr(unav_ent, "name_by_user") %}
                {% endif %}
                {% set nm1.list_unav_dev = nm1.list_unav_dev + [unav_dev] %}
              {% endfor %}
            {% set ignore_1 = "" %}
            {% if is_state("input_boolean.humidifier_connected", "off") %}
              {% set ignore_1 = "Увлажнитель" %}
            {% endif %}
            {{ nm1.list_unav_dev
            | unique
            | reject("==", ignore_1)
            | list
            }}
            
        count_unavailable_devices: >
            {% set nm = namespace(list_unav_ent = [] ) %}
            {% set nm1 = namespace( list_unav_dev = []) %}
            {% set domains = ['light', 'switch', 'sensor', 'binary_sensor'] %}
              {% for domain in domains -%}
                {% for state1 in states[domain] if ((state1.state | lower == "unavailable")
                and not (( is_state('input_boolean.humidifier_connected', 'off') 
                and  (state1.object_id.startswith("humidifier"))))) %}
                  {% set ignored = state_attr('group.ignored_unavailable_entities','entity_id') %}
                  {% if state1.entity_id not in ignored %}
                    {% set nm.list_unav_ent = nm.list_unav_ent + [state1.entity_id] %}
                  {% endif %}
                {% endfor %} 
              {% endfor %} 
              {% for unav_ent in nm.list_unav_ent  -%}
                {% if (device_attr(unav_ent, "name_by_user") is none) %}
                  {% set unav_dev = device_attr(unav_ent, "name") %}
                {% else %}
                  {% set unav_dev = device_attr(unav_ent, "name_by_user") %}
                {% endif %}
                {% set nm1.list_unav_dev = nm1.list_unav_dev + [unav_dev] %}
              {% endfor -%}
            {{ nm1.list_unav_dev
            | unique
            | list
            | length 
            }}    

Вывод датчика в интерфейс. Markdown

Здесь расположены:

  1. Переключатель "input_boolean.visible_unav_ent", который отвечает за видимость списка недоступных сущностей.
  2. Карта Mrcdown с этим самым списком, видимость которой зависит от переключателя.
    Каждая сущность списка проверяется на принадлежность к группе со списком игнорируемых, и, положительно проходящие тест, для удобства, помечаются "игнор."
  3. Неубираемая карта Markdown, которая, в случае отсутствия недоступных устройств, выводит надпись: "Список пуст".
markdown
Копировать
cards:

   - type: entities
     entities:
       - entity: input_boolean.visible_unav_ent
  
   - type: conditional
     conditions:
       - entity: input_boolean.visible_unav_ent
         state: "on"
     card:
       type: markdown
       content: >-
         <font color=Black size=2><center>  <h3>Недоступные объекты
         </center>
         
         {% for entity in state_attr('sensor.unavailable_entities','list_of_unavailable_entities') -%}
         {% set ignored = state_attr('group.ignored_unavailable_entities','entity_id') %}
         {% if entity in ignored %}
         * {{ entity ~ '&emsp; (игнор.) \n'}}
         {% else %}
         * {{ entity ~ '\n'}}
         {% endif -%} 
         {% endfor %}
         
         </font>
       
   - type: markdown
     content: >-
       <font color=Black size=2><center>  <h3>Недоступные устройства
       </center><center>
       
       {% if state_attr('sensor.unavailable_entities','count_unavailable_devices') > 0 %}
       
       {% for device in state_attr('sensor.unavailable_entities','list_of_unavailable_devices') %}
       {{ device ~ '\n'}}
       {% endfor %}
       
       {% else %}
       Список пуст
       
       {% endif %}
       
       </center></font>

Как это выглядит?

Список скрыт, увлажнитель и его input_boolean выключены
Список раскрыт, увлажнитель убран, но его input_boolean не отключен

Заключение

У меня получилась очередная огромная простыня. Хотел укоротить, прошелся по ней и немного добавил. Разумеется, здесь представлен, наверняка, не лучший вариант, но я ещё учусь. Если подобного рода идея кого-нибудь заинтересует, буду рад.

1



С недавних пор, после перезагрузки ХА, стал наблюдать единичные ошибки следующего вида:
"Error while processing template: Template template= (много текста про Traceback и т.п.) TypeError: 'NoneType' object is not iterable"

Это связано с тем, что шаблон в Маркдаун срабатывает быстрее шаблона датчика, и не может обработать пустое (None) значение датчика.

Решил проблему, дополнив шаблон Маркдауна, проверкой на отсутствие значения None (без кавычек)

Вернуться назад
Вернуться назад