Home Assistant: Оповещения о заряде батареек

12 марта, 2021Изменена: 21 марта, 2021 в 16:49

Один из самых популярных беспроводных стандартов для устройств умного дома – ZigBee. Это достаточно энергоэффективный стандарт, позволивший создать огромное количество устройств, питающихся от батареек. И т.к. таких устройств в использовании со временем становится все больше и больше, рано или поздно встает вопрос мониторинга их заряда.

У себя я очень долгое время обходился максимально простой автоматизацией отслеживания заряда с заранее определенным списком датчиков, добавляя в него новые устройства по мере их появления. Но, как-то раз в одном из чатов Телеграма посвященном Home Assistant’у мне попался на глаза очень любопытный пример автоматизации, создающей и (самое главное!) поддерживающей в актуальном состоянии группу объектов по заранее определенным признакам. Мне подумалось что было бы неплохо применить это как раз в разрезе оповещения о низком заряде батареек ZigBee-устройств, а после того как я увидел живую реализацию моих мыслей в конфиге @to4ko я окончательно решил отбросить лень. =)

Для начала стоит разобраться в получившейся автоматизации, собирающей в одну группу все сенсоры батареек и периодически ее обновляющую:

- alias: "Create Group of Battery Devices"
  trigger:
    - platform: homeassistant
      event: start
    - platform: time_pattern
      hours: "/2"
  action:
    - service: group.set
      data:
        object_id: battery_devices
        entities: >-
          {%- 
            for state in states.sensor 
              if is_state_attr(state.entity_id, 'device_class', 'battery') and
              (state.entity_id.endswith("_battery") or state.entity_id.endswith("_power"))
          %}
          {{ state.entity_id }}{%- if not loop.last -%}, {%- endif -%}
          {%- endfor %}

Здесь интересен сервис group.set, который позволяет создать группу (если ее не существует), а так же актуализировать ее состояние при последующих вызовах. Помимо group.set, существуют так-же сервисы group.reload и group.remove, о предназначении которых можно судить исходя из их названия.
Параметр object_id задает имя группы, в приведенном примере объект группы получит имя group.battery_devices.
Список (массив) объектов для включения в группу формируется с помощью шаблона, в цикле for отбирая объекты state из домена sensor по признакам определенным в условии if:
is_state_attr(state.entity_id, 'device_class', 'battery')device_class у объекта должен быть battery,
и
(state.entity_id.endswith("_battery") or state.entity_id.endswith("_power")) – имена объектов должны заканчиваться на _battery или _power.

Т.к. у меня ZigBee устройства подключены к HA с помощью двух разных интеграций – XiaomiGateway3 и ZHA то и сенсоры батареек для устройств немного отличаются, отсюда необходимость отбирать объекты сразу по нескольким признакам.

Эта автоматизация будет выполнятся при каждой загрузке HA (т.к. группы нет в конфиге, и она создается только при вызове сервиса group.set) и далее каждые два часа по триггеру time_pattern.
Результат ее работы будет выглядеть как-то так:

После этого, можно уже подумать над автоматизацией которая будет оперировать созданной группой и своевременно оповещать о необходимости замены батареек в устройствах. Здесь есть разные варианты, как то – обход группы скриптами по расписанию, сортировка объектов по уровню заряда и отправка ежедневных (например) отчетов и т.д.
Мне же хотелось получать оповещения именно по факту падения заряда ниже определенного значения и у меня получился вот такой вариант:

- alias: "Low Battery Alert"
  trigger:
    - platform: event
      event_type: state_changed
  condition:
    - condition: template
      value_template: >-
        {{ trigger.event.data.entity_id in (expand('group.battery_devices') | map(attribute='entity_id')) }}
    - condition: template
      value_template: >-
        {{ not trigger.event.data.new_state.state in ['unknown', 'unavailable'] }}
    - condition: template
      value_template: >-
        {{ (trigger.event.data.new_state.state | int) < 20 }}
  action:
    - service: notify.telegram
      data:
        message: |
          *Внимание, низкий заряд батареи!*
          {{ trigger.event.data.new_state.attributes.friendly_name }}: {{ trigger.event.data.new_state.state }}%!

Автоматизация срабатывает по событию (event) state_changed, т.е. по изменению состояния любого объекта в системе, в т.ч. и сенсоров батареек. Далее, с помощью condition отбираются интересующие нас объекты, а именно те, что входят в группу battery_devices:
{{ trigger.event.data.entity_id in (expand('group.battery_devices') | map(attribute='entity_id')) }}
После этого отбрасываются не интересующие нас состояния – unknown, например:
{{ not trigger.event.data.new_state.state in ['unknown', 'unavailable'] }}
И наконец, события фильтруются по новому заряду батареи – меньше 20%:
{{ (trigger.event.data.new_state.state | int) < 20 }}

В результате, оповещение будет отправлено при изменении стейта (state) у любого из объектов в группе battery_devices, если новое значение будет ниже 20.
Важный момент здесь – оповещение будет отправляться при каждом изменении стейта объекта, если новое значение удовлетворяет условиям. Например, для любого из датчиков входящих в группу group.battery_devices оповещения будут отправляться при изменении его значения на 19, 18, 17, 16 и т.д.
С одной стороны это кажется излишним спамом, но с учетом того, что обычно заряд батареек не падает слишком быстро, это лишнее напоминание о том, что уже давно пора поменять батарейку.

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

На просторах интернета гуляет огромное кол-во вариантов решения задачи по мониторингу заряда батарей, от совсем простых, до огромных packages на 800+ строк. Но в этом то и прелесть ХА, что одну и ту же задачу можно решить множеством разных способов.

Бонусом приведу пару вариантов для наглядного отображения всех батареек с сортировкой по заряду:

Вариант с одной кастомной карточкой flex-table-card (отбор сенсоров по маскам, не очень удобно):

title: Battery Info
path: battery
icon: mdi:battery
cards:
  - type: vertical-stack
    cards:
      - type: markdown
        content: "### <center> Device Battery Info </center>"
      - type: "custom:flex-table-card"
        clickable: false
        entities:
          include:
            - sensor.*_battery
            - sensor.battery_status_*
            - sensor.lumi_*_power
          exclude:
            - sensor.mirobot_1s_battery
        sort_by: state+
        columns:
          - data: friendly_name
            name: Friendly Name
            modify: '(x+"").replace(/ Battery$/,"").replace(/ power$/,"")'
          - data: state
            name: Remaining %
            modify: "isNaN(parseInt(x, 10)) ? 0 : parseInt(x, 10)"
        css:
          table+: "padding-top: 2px;"
          "tbody tr:nth-child(even)": "background-color: #a2542f6;"
          td.left: "padding: 2px 2px 2px 2px"
          th.left: "padding: 0px 0px 2px 2px"

Вариант с комбинацией из flex-table-card и auto-entities (используется ранее созданная группа group.battery_devices):

title: Battery Info
path: battery
icon: mdi:battery
cards:
  - type: vertical-stack
    cards:
      - type: markdown
        content: "### <center> Device Battery Info </center>"
      - type: "custom:auto-entities"
        filter:
          include:
            - group: group.battery_devices
          exclude:
            - entity_id: sensor.mirobot_1s_battery
        sort:
          method: state
          numeric: true
        card:
          type: "custom:flex-table-card"
          clickable: false
          columns:
            - data: friendly_name
              name: Friendly Name
              modify: '(x+"").replace(/ Battery$/,"").replace(/ power$/,"")'
            - data: state
              name: Remaining
              #modify: "isNaN(parseInt(x, 10)) ? 0 : parseInt(x, 10)"
              modify: x+' %'
          css:
            table+: "padding-top: 2px;"
            "tbody tr:nth-child(even)": "background-color: #a2542f6;"
            td.left: "padding: 2px 2px 2px 2px"
            th.left: "padding: 0px 0px 2px 2px"