ESPHome: Получение заряда батареи BLE метки

5 ноября, 2023Изменена: 14 ноября, 2023 в 10:27

Некоторое время назад попались мне на глаза достаточно интересные BLE метки от братьев-китайцев производства Holyiot. Я приобрел некоторое их количество и успешно пристроил для целей отслеживания ключей и пропусков. Но по прошествии некоторого времени у меня встал вопрос о мониторинге заряда их батарей. В итоге получилось достаточно интересно.

Практически все BLE устройства в моем хозяйстве интегрированы в HA с помощью ESPHome Bluetooth Proxy. Это действительно очень удобный механизм (хоть и не без недостатков конечно), позволяющий не думать о покрытии Bluetooth, просто добавляя в нужных местах и нужном количестве платы ESP32 с прошивкой ESPHome (Например M5Stack Atom Lite). Бонусом получая возможность некоторого Room Presence, например.

Не исключение и BLE метки Holyiot. В их магазине на Aliexpress большой выбор меток в разных форм-факторах, а так же с разным набором датчиков (кнопки, вибрация, температура и т.д.).
Вживую выглядят они как то так:

С помощью соответствующего приложения есть возможность изменить ряд настроек, например режим работы (beacon, ibeacon, eddystone), всевозможные ID или мощность и периодичность отправки пакетов (advertisement). Что ценно, особенно мощность сигнала и его периодичность. Уменьшив мощность и увеличив интервал можно существенно продлить срок службы батарейки.
Моя самая старая метка работает на одной батарейке уже больше года с остатком заряда ~50%.

На всякий случай – пароль по умолчанию для подключения к меткам Holyiot – aa14061112

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

Дело в том, что компонент esp32_ble_tracker может работать как в пассивном (просто слушая эфир), так и в активном режиме (взаимодействуя с устройствами, самостоятельно отправляя запросы на которые устройства должны отвечать). При этом активный режим потребляет несколько больше энергии с т.з. конечных устройств, утилизируя заряд батареи более активно, что нам совершенно не нужно. Выдержка из документации:

active (Optional, boolean): Whether to actively send scan requests to request more data after having received an advertising packet. With some devices this is necessary to receive all data, but also drains those devices’ power a bit more. Some devices don’t need this, in that case you can save power and RF pollution by setting it to false. Defaults to true.

Соответственно, для экономии заряда в идеале нужно все BT Proxy держать в пассивном режиме, но к сожалению это не позволяет получить данные заряда батареи. Метка отдает заряд только при активном сканировании.

Отсюда основная идея заключается в том, чтобы включать активный режим только в определенных ситуациях. Например, при получении первого advertise пакета от метки (срабатывает бинарный сенсор присутствия метки), или по расписанию. При таком подходе большую часть времени ESP слушает эфир в пассивном режиме, переключаясь в активный только по определенным событиям.

Далее будет представлен пример кода с комментариями. Все тайминги в свое время были рекомендованы авторами ESPHome Bluetooth Proxy, но с тех пор они поменяли свое мнение. Тем не менее, у меня данные параметры прекрасно себя зарекомендовали, поэтому оставлю их здесь без изменений. При необходимости можете самостоятельно попробовать менять значения interval/window и duration, желательно при этом опираясь на какую-либо документацию =)

esp32_ble_tracker:
  id: ble_tracker_id
  # На старте компонента мы указываем следующие параметры сканирования:
  scan_parameters:
    interval: 450ms
    window: 160ms
    duration: 10s
    active: true
    continuous: true
  # При получении пакета от определенного mac-адреса (метки) с нужным нам service_uuid (5242),
  # отправляем второй по счету (от 0) бит в нужный сенсор заряда батареи.
  on_ble_service_data_advertise:
  - mac_address: F9:9D:1D:40:23:40
    service_uuid: '5242'
    # id(black1_battery) = id нужного нам template сенсора, x[1] = бит из сообщения, содержащий заряд батареи.
    # Найдено здесь - https://github.com/custom-components/ble_monitor/issues/1183
    then:
    - lambda: !lambda |-
        id(black1_battery).publish_state(x[1]);
  # Все то же самое для второй метки
  - mac_address: E6:9F:37:5B:1D:DA
    service_uuid: '5242'
    then:
    - lambda: !lambda |-
        id(white1_battery).publish_state(x[1]);

sensor:
  # Два template сенсора, в которые будут отправлены показания заряда батареек
  - platform: template
    name: Holy-IoT Black-1 Battery
    id: black1_battery
    device_class: battery
    unit_of_measurement: '%'
    entity_category: diagnostic
    accuracy_decimals: 0
    disabled_by_default: false
    force_update: false
    update_interval: 60s
  - platform: template
    name: Holy-IoT White-1 Battery
    id: white1_battery
    device_class: battery
    unit_of_measurement: '%'
    entity_category: diagnostic
    accuracy_decimals: 0
    disabled_by_default: false
    force_update: false
    update_interval: 60s

binary_sensor:
  # binary_sensor присутствия метки, переключается в on при получении пакета с нужными ibeacon данными (uuid\major\minor)
  # для того, что бы сгладить возможный "дребезг" датчика из за потерянных пакетов, установлен delayed_off в 30 сек.
  # После перехода сенсора в on, запускается скрипт ble_active_scan, который и отвечает за изменение режимов.
  - platform: ble_presence
    name: Hallway Holy IoT Black-1 Presence
    ibeacon_uuid: fda50693-a4e2-4fb1-afcf-c6eb07647825
    ibeacon_major: 10011
    ibeacon_minor: 1
    device_class: presence
    icon: mdi:key-chain-variant
    filters:
    - delayed_off: 30s
    on_press:
    - then:
      - script.execute:
          id: ble_active_scan
    disabled_by_default: false
  # То же самое для второй метки
  - platform: ble_presence
    name: Hallway Holy IoT White-1 Presence
    ibeacon_uuid: fda50693-a4e2-4fb1-afcf-c6eb07647826
    ibeacon_major: 10011
    ibeacon_minor: 3
    device_class: presence
    icon: mdi:car-key
    filters:
    - delayed_off: 30s
    on_press:
    - then:
      - script.execute:
          id: ble_active_scan

script:
  # Скрипт "на ходу" меняет режим (active\passive) и прочие настройки сканирования.
  # Краткое описание параметров:
  # set_scan_active = bool (true\false)
  # set_scan_duration = milliseconds
  # set_scan_interval = milliseconds / 0.625 (example: 450ms / 0.625 = 720)
  # set_scan_window = milliseconds / 0.625 (example: 160ms / 0.625 = 256)
  # set_scan_continuous = bool (true\false)
  - id: ble_active_scan
    # Режим запуска скрипта - single
    mode: single
    then:
    # Остановка текущего сканирования.
    - lambda: !lambda |-
        id(ble_tracker_id).stop_scan();
    # Задержка 2 сек
    - delay: 2s
    # Настройка параметров и старт сканирования (Active).
    - lambda: !lambda |-
        id(ble_tracker_id).set_scan_active(true);
        id(ble_tracker_id).set_scan_duration(10);
        id(ble_tracker_id).set_scan_interval(720);
        id(ble_tracker_id).set_scan_window(256);
        id(ble_tracker_id).set_scan_continuous(true);
        id(ble_tracker_id).start_scan();
    # Запись в лог.
    - logger.log:
        format: Active scan started
        level: INFO
        args: []
        tag: main
    # Продолжительность активного сканирования 30 секунд.
    - delay: 30s
    # Запись в лог.
    - logger.log:
        format: Active scan stopped
        level: INFO
        args: []
        tag: main
    # Остановка текущего (активного) сканирования.
    - lambda: !lambda |-
        id(ble_tracker_id).stop_scan();
    # Задержка 2 сек.
    - delay: 2s
    # Настройка параметров и старт сканирования (Passive).
    - lambda: !lambda |-
        id(ble_tracker_id).set_scan_active(false);
        id(ble_tracker_id).set_scan_duration(10);
        id(ble_tracker_id).set_scan_interval(720);
        id(ble_tracker_id).set_scan_window(256);
        id(ble_tracker_id).set_scan_continuous(true);
        id(ble_tracker_id).start_scan();

interval:
  # Запуск активного сканирования по расписанию, раз в 30 минут.
  # Вполне можно изменить расписание на раз в 24 часа или вообще убрать.
  - interval: 30min
    then:
    - script.execute:
        id: ble_active_scan

У меня датчики батареек для всех меток созданы на одной единственной ESP, которая расположена в коридоре, возле входной двери. А т.к. все мои метки висят либо на ключах, либо на пропусках, данные батареек будут гарантированно получены как минимум при входе или выходе из квартиры.

А для оповещения о низком заряде батарейных устройств у меня есть соответствующая статья.