Один из самых популярных беспроводных стандартов для устройств умного дома — 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"