На экране термометра видно, что происходит сейчас. А какая температура была ночью? Соберём историю с двух готовых 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 здесь не подходит: это был ошибочный перенос формата сторонней прошивки на штатную.
- Проверьте точную модель и аппаратную версию. Найдите датчик сканером BLE на Android и сопоставьте его физический MAC-адрес с местом установки. Идентификатор устройства, показываемый iOS, нельзя автоматически считать таким MAC-адресом.
- Получите ключ своих датчиков способом, совместимым с их прошивкой. В документации ESPHome для Xiaomi описаны требования к ключам и варианты получения. Активация через Telink и повторная привязка в Mi Home могут изменить состояние привязки: сохраните ключ и проверьте его после изменений.
- Сначала проверьте один датчик рядом с хабом. Отсутствие расшифрованных показаний — ошибка настройки или связи, а не температура 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 нельзя считать обычным открытым датчиком для такого хаба.
Данные микроклимата можно использовать рядом с учётными инструментами хозяйства, например ТерраКвант, если заранее согласованы формат и способ обмена. Для подключения понадобится отдельно реализовать и проверить этот обмен.
Начните с одного термометра, проверьте потерю связи, отсутствие данных и перезапуск хаба, затем добавьте второй. Сравните их рядом и в разных точках. Первый день истории уже даст повод разобраться, где условия меняются быстрее и какой вопрос задать следующему наблюдению.





