Своими рукамипрактическая инструкция источник: Фермозавр

Термометры Xiaomi ведут дневник: собираем BLE-хаб для теплицы

Редакция Фермозавра·9 апреля 2026 г.·13 мин чтения

Расставим два готовых термометра, примем их сообщения на ESP32 и увидим историю в Grafana. Сравним места установки и научимся замечать, когда показания перестали приходить.

Термометры Xiaomi ведут дневник: собираем BLE-хаб для теплицы

На экране термометра видно, что происходит сейчас. А какая температура была ночью? Соберём историю с двух готовых BLE-термогигрометров: они останутся на батарейках, а ESP32 будет слушать сообщения и отправлять их на сервер. Для выбранной USB-платы и готовых датчиков пайка не понадобится. Это лабораторный проект; на месте подберём защиту от воды и конденсата и проверим связь.

Два термометра и один постоянно включённый слушатель

Возьмём два Xiaomi LYWSD03MMC со штатной прошивкой, классический ESP32 DevKit с Wi-Fi и BLE и настроенный сервер. Путь показаний такой: датчик → BLE → ESP32 → MQTT с TLS → Telegraf → InfluxDB 2 → Grafana. Хаб постоянно питается от USB, чтобы принимать объявления датчиков.

У готового термогигрометра уже есть экран и корпус — удобно перенести его в другую точку и сразу посмотреть число. Разрешение 0,1 °C уточняйте отдельно от погрешности конкретной версии. Срок батарейки оцените на своей прошивке, интервале и температуре; для сравнения с другим устройством пригодится измерение расхода.

Понадобятся два совместимых датчика, ESP32 DevKit, USB-кабель с передачей данных, подходящий блок питания, Wi-Fi и компьютер для первоначальной прошивки хаба. ESP32 без BLE для этого примера не подходит. Сервер, резервное питание и сеть в стоимость датчиков не входят.

Находим адрес и ключ каждого датчика

Для LYWSD03MMC со штатной прошивкой используем компонент xiaomi_lywsd03mmc. Он принимает зашифрованные объявления MiBeacon; для каждого датчика нужен собственный ключ bindkey. Универсальный разбор байтов сервиса 0x181A здесь не подходит: это был ошибочный перенос формата сторонней прошивки на штатную.

  1. Проверьте точную модель и аппаратную версию. Найдите датчик сканером BLE на Android и сопоставьте его физический MAC-адрес с местом установки. Идентификатор устройства, показываемый iOS, нельзя автоматически считать таким MAC-адресом.
  2. Получите ключ своих датчиков способом, совместимым с их прошивкой. В документации ESPHome для Xiaomi описаны требования к ключам и варианты получения. Активация через Telink и повторная привязка в Mi Home могут изменить состояние привязки: сохраните ключ и проверьте его после изменений.
  3. Сначала проверьте один датчик рядом с хабом. Отсутствие расшифрованных показаний — ошибка настройки или связи, а не температура 0 °C. ESPHome не угадывает и не создаёт правильный bindkey вместо владельца.

Хаб пассивно слушает объявления. Понаблюдайте, как появляются температура, влажность и батарея: у каждого поля может быть свой период. Постоянного соединения с термометрами для этого приёма не требуется.

Готовим ESP32 к первому приёму

Пример проверен сборкой в ESPHome 2026.9.0 с ESP-IDF 5.5.5 для esp32dev. Сохраните следующий файл как ble-hub.yaml. Имена sensor1, sensor2 и hub1 здесь постоянные идентификаторы; подписи помещений задайте в Grafana. При добавлении датчиков нужно расширить не только список компонентов, но и допустимые идентификаторы в проверке сообщения.

esphome:
  name: farm-ble-hub
  on_boot:
    priority: 800
    then:
      - lambda: |-
          char text[17];
          snprintf(text, sizeof(text), "%08x%08x", random_uint32(), random_uint32());
          id(boot_id) = text;

esp32:
  board: esp32dev
  framework:
    type: esp-idf

logger:
  level: WARN

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  reboot_timeout: 0s

mqtt:
  id: mqtt_link
  broker: !secret mqtt_host
  port: 8883
  username: !secret mqtt_username
  password: !secret mqtt_password
  certificate_authority: !secret mqtt_ca
  skip_cert_cn_check: false
  idf_send_async: true
  discovery: false
  log_topic: null
  reboot_timeout: 0s
  birth_message:
    topic: ble/hub1/status
    payload: online
    retain: false
  will_message:
    topic: ble/hub1/status
    payload: offline
    retain: false

# OTA is enabled explicitly; use a private management network.
ota:
  - platform: esphome
    password: !secret ota_password

time:
  - platform: sntp
    id: hub_clock
    timezone: UTC

esp32_ble_tracker:
  scan_parameters:
    active: false

globals:
  - id: boot_id
    type: std::string
    restore_value: false
    initial_value: '""'
  - id: sequence
    type: uint32_t
    restore_value: false
    initial_value: '0'

script:
  - id: send_observation
    mode: queued
    max_runs: 16
    parameters:
      device: string
      metric: string
      value: float
      observed_at: int
    then:
      - if:
          condition:
            lambda: |-
              if (!id(mqtt_link).is_connected() || !id(hub_clock).now().is_valid()) return false;
              if (id(boot_id).size() != 16 || id(sequence) == UINT32_MAX || observed_at < 1700000000) return false;
              if (!std::isfinite(value)) return false;
              if (device == "hub1") return metric == "heartbeat" && value == 1;
              if (device != "sensor1" && device != "sensor2") return false;
              if (metric == "temperature") return value >= -40 && value <= 85;
              if (metric == "humidity" || metric == "battery") return value >= 0 && value <= 100;
              return false;
          then:
            - mqtt.publish_json:
                topic: ble/hub1/observations
                qos: 0
                retain: false
                payload: |-
                  root["hub"] = "hub1";
                  root["boot"] = id(boot_id);
                  root["seq"] = ++id(sequence);
                  root["device"] = device;
                  root["metric"] = metric;
                  root["value"] = value;
                  root["observed_at"] = observed_at;

interval:
  - interval: 5min
    then:
      - script.execute:
          id: send_observation
          device: "hub1"
          metric: "heartbeat"
          value: 1
          observed_at: !lambda return id(hub_clock).now().timestamp;

sensor:
  - platform: xiaomi_lywsd03mmc
    mac_address: !secret sensor1_mac
    bindkey: !secret sensor1_bindkey
    temperature:
      id: s1_temperature
      internal: true
      on_value:
        then:
          - script.execute:
              id: send_observation
              device: "sensor1"
              metric: "temperature"
              value: !lambda return x;
              observed_at: !lambda return id(hub_clock).now().timestamp;
    humidity:
      id: s1_humidity
      internal: true
      on_value:
        then:
          - script.execute:
              id: send_observation
              device: "sensor1"
              metric: "humidity"
              value: !lambda return x;
              observed_at: !lambda return id(hub_clock).now().timestamp;
    battery_level:
      id: s1_battery
      internal: true
      on_value:
        then:
          - script.execute:
              id: send_observation
              device: "sensor1"
              metric: "battery"
              value: !lambda return x;
              observed_at: !lambda return id(hub_clock).now().timestamp;
  - platform: xiaomi_lywsd03mmc
    mac_address: !secret sensor2_mac
    bindkey: !secret sensor2_bindkey
    temperature:
      id: s2_temperature
      internal: true
      on_value:
        then:
          - script.execute:
              id: send_observation
              device: "sensor2"
              metric: "temperature"
              value: !lambda return x;
              observed_at: !lambda return id(hub_clock).now().timestamp;
    humidity:
      id: s2_humidity
      internal: true
      on_value:
        then:
          - script.execute:
              id: send_observation
              device: "sensor2"
              metric: "humidity"
              value: !lambda return x;
              observed_at: !lambda return id(hub_clock).now().timestamp;
    battery_level:
      id: s2_battery
      internal: true
      on_value:
        then:
          - script.execute:
              id: send_observation
              device: "sensor2"
              metric: "battery"
              value: !lambda return x;
              observed_at: !lambda return id(hub_clock).now().timestamp;

В соседний файл secrets.yaml внесите свои параметры по шаблону ниже. Нулевой и единичный ключи, адреса и сертификат-заглушка показаны только для структуры: с ними реальные датчики и брокер не заработают. Ключи bindkey — по 32 шестнадцатеричных символа. Вставьте полный PEM-сертификат центра, которому доверяет ваш MQTT-клиент. Имя брокера должно соответствовать его сертификату; проверку имени и доверия не отключайте.

wifi_ssid: "REPLACE_WIFI_SSID"
wifi_password: "REPLACE_WIFI_PASSWORD"
mqtt_host: "mqtt.example.com"
mqtt_username: "ble-hub1"
mqtt_password: "REPLACE_MQTT_PASSWORD"
ota_password: "REPLACE_UNIQUE_OTA_PASSWORD"
sensor1_mac: "A4:C1:38:AA:BB:01"
sensor1_bindkey: "00000000000000000000000000000000"
sensor2_mac: "A4:C1:38:AA:BB:02"
sensor2_bindkey: "11111111111111111111111111111111"
mqtt_ca: |-
  -----BEGIN CERTIFICATE-----
  REPLACE_WITH_YOUR_BROKER_CA_PEM
  -----END CERTIFICATE-----

Установите ESPHome в отдельное окружение с совместимой версией Python. Затем проверьте конфигурацию, соберите её и выполните первоначальную запись через USB. Команда run уже прошивает выбранное устройство; убедитесь, что выбрали нужную плату. Сохраните прежнюю прошивку и настройки, если плата использовалась раньше.

python3 -m venv .venv
. .venv/bin/activate
python -m pip install 'esphome==2026.9.0'
chmod 600 secrets.yaml
esphome config ble-hub.yaml
esphome compile ble-hub.yaml
# Следующая команда записывает прошивку в выбранную вами плату:
esphome run ble-hub.yaml

OTA включено явно и защищено отдельным паролем. Управляйте им из доверенной локальной сети. Не публикуйте secrets.yaml, собранную прошивку и журналы с секретами. Уровень журнала WARN уменьшает диагностический вывод; для поиска неисправности можно временно включить подробные сообщения, но перед передачей журнала другому человеку проверьте, нет ли там ключей.

После синхронизации времени и подключения MQTT каждое поле отправляется из своего обработчика. Попробуйте сравнить время температуры и батареи: сообщение батареи сохраняет прежний возраст температуры. observed_at отмечает приём хабом. До первой синхронизации данные пропускаются.

Для опыта сообщения идут с QoS 0, без retain и дисковой очереди. При отключениях возможны пропуски. seq и случайный boot помогут читать журнал после перезапуска; при исчерпании 32-битного seq публикация остановится до нового запуска. Время задано в секундах, поэтому обновления одного поля внутри секунды могут объединиться в базе.

Проводим сообщение до базы

Сначала подготовьте брокер с TLS, парольной аутентификацией и отдельными правами: пользователь ble-hub1 публикует только в ble/hub1/observations и ble/hub1/status, а ble-reader читает только observations. Не разрешайте посторонним публиковать данные от имени хаба: конфигурация Telegraf ниже разбирает JSON, но не заменяет полноценную проверку недоверенного отправителя.

В InfluxDB 2 создайте организацию farm, bucket sensors и токен с правом записи только в этот bucket. Grafana выдайте другой токен, только для чтения. Порядок установки сервера разобран в инструкции по InfluxDB и Grafana. Здесь используется именно Flux для InfluxDB 2; перенос конфигурации на другую основную версию потребует изменений.

Сохраните конфигурацию Telegraf в отдельный файл. Укажите адреса, пароль читателя, токен записи и путь к CA брокера. HTTPS-сертификат InfluxDB должен быть доверенным для сервера Telegraf; при частном центре сертификации настройте доверие отдельно, а не отключайте проверку TLS. Ограничьте чтение файла с паролем и токеном учётной записью службы.

[agent]
  interval = "30s"
  flush_interval = "10s"
  metric_batch_size = 100
  metric_buffer_limit = 1000
  omit_hostname = true

[[inputs.mqtt_consumer]]
  servers = ["ssl://mqtt.example.com:8883"]
  topics = ["ble/hub1/observations"]
  username = "ble-reader"
  password = "REPLACE_MQTT_PASSWORD"
  client_id = "ble-telegraf"
  qos = 0
  persistent_session = false
  tls_ca = "/etc/telegraf/mqtt-ca.crt"
  insecure_skip_verify = false
  data_format = "json_v2"

  [[inputs.mqtt_consumer.json_v2]]
    measurement_name = "xiaomi_ble"
    timestamp_path = "observed_at"
    timestamp_format = "unix"
    [[inputs.mqtt_consumer.json_v2.tag]]
      path = "hub"
    [[inputs.mqtt_consumer.json_v2.tag]]
      path = "device"
    [[inputs.mqtt_consumer.json_v2.tag]]
      path = "metric"
    [[inputs.mqtt_consumer.json_v2.field]]
      path = "value"
      type = "float"
    [[inputs.mqtt_consumer.json_v2.field]]
      path = "seq"
      type = "int"

[[outputs.influxdb_v2]]
  urls = ["https://influx.example.com"]
  token = "REPLACE_INFLUX_TOKEN"
  organization = "farm"
  bucket = "sensors"
  timeout = "10s"

Найдите measurement xiaomi_ble: hub, device и metric задают источник и поле, value содержит значение, seq — номер. MQTT-плагин добавляет topic. Время берётся из observed_at. boot оставлен вне тегов, чтобы каждый перезапуск не создавал новую серию.

Сначала запустите проверку конфигурации. В режиме --test Telegraf покажет разобранные метрики; запись в InfluxDB начинается при обычной работе. Дождитесь реального сообщения, затем найдите его и в журнале, и в bucket.

telegraf --config ble-telegraf.conf --test --test-wait 60
# После проверки: обычный процесс с записью в InfluxDB.
# Не запускайте второй экземпляр поверх уже настроенной службы.
telegraf --config ble-telegraf.conf

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

Сравниваем два места и возраст показаний

Постройте температуру sensor1 за сутки запросом ниже. Для второй точки поменяйте device, для влажности и батареи — metric; значение остаётся в value. Пятиминутное среднее удобно для обзора, а краткие отклонения рассматривайте по исходным точкам. Отключите соединение пропусков линией.

from(bucket: "sensors")
  |> range(start: -24h)
  |> filter(fn: (r) => r._measurement == "xiaomi_ble" and r._field == "value")
  |> filter(fn: (r) => r.hub == "hub1" and r.device == "sensor1" and r.metric == "temperature")
  |> aggregateWindow(every: 5m, fn: mean, createEmpty: true)
  |> yield(name: "temperature_mean")

Добавьте рядом возраст последней температуры. Запрос ищет её за семь суток и возвращает секунды от приёма хабом. Для пустого результата задайте «нет данных». Так рядом с красивым графиком будет видна свежесть наблюдения.

from(bucket: "sensors")
  |> range(start: -7d)
  |> filter(fn: (r) => r._measurement == "xiaomi_ble" and r._field == "value")
  |> filter(fn: (r) => r.hub == "hub1" and r.device == "sensor1" and r.metric == "temperature")
  |> last()
  |> map(fn: (r) => ({r with _value: float(v: int(v: now()) - int(v: r._time)) / 1000000000.0}))
  |> yield(name: "age_seconds")

Создайте явную проверку каждого ожидаемого датчика и поля. Попробуйте включить в список ещё не передававший датчик — его отсутствие должно быть заметно. Пропуски оставляйте видимыми, без fill(usePrevious:true). Отрицательный возраст подскажет проверить часы.

Условия выхода температуры за допустимые границы выбирают для конкретной культуры, стадии и режима хранения. Пороги из демонстрации не являются агрономическими рекомендациями. Отдельно задайте допустимый возраст данных, No Data и ошибку запроса. Период проверки правила не равен времени доставки уведомления: добавляются задержка измерения, передачи, вычисления и канала связи. Настройку шлюза и оповещений смотрите в связанном руководстве; сам BLE-хаб сообщения в Telegram не отправляет.

Если термометр уже работает на другой прошивке

Проект pvvx/ATC_MiThermometer поддерживает несколько устройств и аппаратных ревизий. До записи сверяют таблицу совместимости и способ восстановления. Обновление через браузер доступно не для любого состояния устройства; иногда нужен проводной доступ. Подготовьте способ восстановления до начала изменений.

Для новой конфигурации ориентируйтесь на BTHome v2: в ветке PVVX 6.0 старые форматы объявлены устаревшими. Пример ниже — точная замена двух строк в основном файле, когда оба датчика уже настроены на зашифрованный BTHome и известны их ключи. Это не инструкция по перепрошивке самих Xiaomi. Ключи BTHome должны совпадать с настройками датчиков; прежние MiBeacon-ключи автоматически подходящими не становятся.

В обоих блоках sensor замените:
  - platform: xiaomi_lywsd03mmc
на:
  - platform: bthome_mithermometer
Остальные строки сохраняются. Ключи sensor1_bindkey и sensor2_bindkey
должны соответствовать включённому шифрованию BTHome на самих датчиках.

После замены проверьте выбранный формат и ключи. Возможности и ограничения компонента приведены в документации BTHome MiThermometer. Для каждого формата используйте соответствующий компонент.

Кто замолчал: термометр или хаб?

Раз в пять минут хаб отправляет heartbeat от device=hub1. Подставьте в запрос возраста hub1 и heartbeat вместо sensor1 и temperature. Для первого опыта задайте тревогу после десяти минут тишины и отдельно No Data. Затем подберите интервал по своей сети.

Сравните возраст heartbeat и температуры. Свежий heartbeat показывает, что сообщение хаба дошло; отдельные датчики проверяются по своим полям. Если пропал и он, пройдите цепочку от питания и Wi-Fi до MQTT, базы и часов — так легче найти разрыв.

  • Нет показаний. Проверьте модель, MAC, ключ и формат прошивки, батарейку, расстояние и синхронизацию времени. Отключите приложение, которое держит соединение с датчиком, и повторите проверку рядом с хабом. Не заменяйте отсутствие данных нулём.
  • Пропуски при работе Wi-Fi. Wi-Fi и BLE используют общие радиоресурсы ESP32. Проверьте питание и размещение, нагрузку и параметры сканирования. Безусловно выключать и заново включать BLE со старым указателем сканера — неверное исправление. Сначала измерьте потери в выбранной конфигурации.
  • Исчезает одна точка. Сопоставьте её возраст с остальными, перенесите хаб и испытайте связь при закрытых дверях, влажной растительности и заполненном складе. Число стен или метров не заменяет измерение покрытия.
  • Хаб перезапускается. Проверьте источник питания и причину сброса, затем версии и нагрузку. Watchdog не исправляет плохое питание или ошибочный код; добавлять универсальный таймер перезапуска вместо диагностики не стоит.

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

Если теплица оказалась за пределами Wi-Fi

Если возле датчиков нет Wi-Fi, можно спроектировать BLE-хаб с отдельным LoRa-радиомодулем и принимающим шлюзом. Общая схема: Xiaomi → BLE → ESP32 с радиомодулем → LoRa → шлюз → база. Приведённый выше YAML реализует путь через Wi-Fi и сам по себе SX1278 не обслуживает.

Для этой ветки понадобятся отдельная прошивка передатчика, согласованный протокол и изменения приёмника. Нельзя просто склеить произвольное число строк и считать, что они поместятся в радиопакет. Передавайте идентификатор датчика, признаки наличия каждого поля, возраст наблюдений, номер сообщения и средство проверки подлинности; проверяйте длину до отправки. Повтор старой температуры после пробуждения не должен становиться новой точкой с текущим временем.

Размер сообщения, время в эфире, допустимые параметры радио и дальность проверяют для выбранного режима и места эксплуатации. Глубокий сон экономит энергию, но оставляет интервалы без приёма BLE. Оценку срока работы делают по измеренному току всего узла, включая плату питания. Согласованные примеры радиоканала и шлюза приведены в статьях об ESP32 с SX1278 и о принимающем шлюзе. Перенос нескольких BLE-датчиков в этот протокол остаётся отдельной работой, а не готовой функцией данной конфигурации.

Считаем комплект и выбираем следующий опыт

В смету включите датчики, хабы, питание, защищённое размещение, сервер, сеть и запасные батарейки. Количество хабов определите проверкой покрытия. Если сравниваете с готовой системой, учтите также требования к поверке, гарантии и поддержке.

Другие BLE-устройства требуют своих поддерживаемых протоколов. Mi Flora может дать дополнительные данные о среде, но электрическая проводимость не является прямым измерением «плодородия». Бытовые весы не становятся средством коммерческого учёта урожая только благодаря передаче массы по BLE. Для пищевых щупов важны назначение, материалы и конкретный протокол; совместимость одного бренда нельзя переносить на все модели. BLE-маячки требуют отдельной логики определения зоны, а AirTag нельзя считать обычным открытым датчиком для такого хаба.

Данные микроклимата можно использовать рядом с учётными инструментами хозяйства, например ТерраКвант, если заранее согласованы формат и способ обмена. Для подключения понадобится отдельно реализовать и проверить этот обмен.

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

Источник: Фермозавр

💬 Комментарии

Чтобы оставить комментарий, войдите или зарегистрируйтесь

Загрузка комментариев...

Похожие статьи