Одинаково ли тепло в разных концах теплицы?
У двери одни условия, в середине другие — проверим разницу тремя датчиками Zigbee. Raspberry Pi примет сообщения, Telegraf сохранит их в InfluxDB, а Grafana покажет историю. Это лабораторная сборка: скрипт вентиляции сначала будет только показывать расчётную команду. Исполнительный узел подключают после проверки схемы и испытаний.
Готовые датчики позволяют сосредоточиться на размещении и наблюдениях. Проверьте интервалы отчётов, батарейки и связь именно в своей теплице. Металл, вода и соседние радиосети могут влиять на приём — сравнение разных мест станет частью опыта.
- Координатор SONOFF ZBDongle-E: модель на EFR32MG21 использует адаптер
emberв Zigbee2MQTT. Это не ESP32-C6. У ZBDongle-P чип CC2652P и адаптерzstack; настройки и прошивки этих моделей различаются. - Три SONOFF SNZB-02D: датчики температуры и влажности с экраном. Перед покупкой сверяют точную модель и доступные свойства в каталоге Zigbee2MQTT. Совместимость бренда целиком не гарантируется.
- Raspberry Pi с Linux, питание и накопитель: на нём работают брокер Mosquitto, Zigbee2MQTT и Telegraf. InfluxDB и Grafana в этом примере уже установлены. В смету также входят корпус, кабели и резервное питание, если оно нужно.
- Исполнительный узел: устройство с подтверждённой поддержкой команд
state: ON/OFF, подходящее для выбранной нагрузки. Для двигателя проверяют пусковой ток, категорию нагрузки и необходимость контактора, защиты двигателя и ручного управления.
SONOFF ZBMINIL2 — конечное устройство, а не Zigbee-роутер. Его нельзя использовать как обещанный ретранслятор; нельзя и выбирать для произвольного вентилятора или насоса только по максимальному току в карточке. Исполнительный узел в примере ниже обозначен именем fan_relay, без утверждения о пригодности ZBMINIL2 для двигателя.
Другие варианты из каталога устройств: Aqara WSDCGQ11LM измеряет температуру, влажность и давление; SJCGQ11LM сообщает о протечке; SONOFF SNZB-03 — о движении. Срабатывание PIR не равно подсчёту людей. IKEA VINDSTYRKA измеряет, в частности, PM2.5, но бытовой датчик не заменяет приборы контроля опасной пыли на производстве.
Если хочется построить шлюз на ESP32-C6
ESP32-C6 поддерживает IEEE 802.15.4, а Espressif предоставляет ESP Zigbee SDK. На этой основе можно разрабатывать собственный координатор с обработкой устройств и передачей по Wi-Fi. Это отдельный проект прошивки; для первого знакомства ниже используем готовый USB-координатор.
Приведённая ранее команда с файлом tasmota32c6-zigbee.factory.bin не подтверждена официальным списком сборок. Документация Zigbee в Tasmota описывает, в частности, внешние Zigbee-сопроцессоры. В этой сборке за приём отвечает USB-координатор с Zigbee2MQTT.
Подключаем координатор и принимаем первое сообщение
Ниже — пример новой установки на Linux. На существующем сервере сначала сохраните конфигурации и проверьте занятые порты: не заменяйте общие настройки брокера вслепую. MQTT и веб-интерфейс в этом примере доступны только с самого Raspberry Pi; для удалённого подключения используйте защищённый канал.
Установите Mosquitto и создайте три разных пароля. Ключ -c создаёт новый файл и нужен только в первой команде, когда файла ещё нет. В действующем файле он удалит прежние учётные записи.
sudo apt install mosquitto mosquitto-clients
sudo mosquitto_passwd -c /etc/mosquitto/passwd z2m
sudo mosquitto_passwd /etc/mosquitto/passwd telegraf
sudo mosquitto_passwd /etc/mosquitto/passwd fan
sudo chown root:mosquitto /etc/mosquitto/passwd
sudo chmod 640 /etc/mosquitto/passwdВ /etc/mosquitto/conf.d/zigbee.conf добавьте настройки слушателя. Убедитесь, что другие файлы не открывают дополнительный порт без защиты.
listener 1883 127.0.0.1
allow_anonymous false
password_file /etc/mosquitto/passwd
acl_file /etc/mosquitto/zigbee.aclВ /etc/mosquitto/zigbee.acl задайте права. Брокер должен иметь право чтения файла; для него также подходят владелец root:mosquitto и режим 640.
user z2m
topic readwrite zigbee2mqtt/#
user telegraf
topic read zigbee2mqtt/greenhouse_begin
topic read zigbee2mqtt/greenhouse_mid
topic read zigbee2mqtt/greenhouse_end
user fan
topic read zigbee2mqtt/greenhouse_begin
topic read zigbee2mqtt/greenhouse_mid
topic read zigbee2mqtt/greenhouse_end
topic write zigbee2mqtt/fan_relay/setПерезапустите Mosquitto и проверьте журнал: sudo systemctl restart mosquitto, затем journalctl -u mosquitto -n 30. Настройка пароля в брокере и в каждом клиенте должна совпадать; анонимное подключение должно отклоняться.
Установите Node.js и pnpm по официальной инструкции Linux, сверяя требования выбранного выпуска Zigbee2MQTT. Далее настроим отдельную службу запуска. Команды выполняются обычным пользователем, который затем будет указан в сервисе.
sudo apt install git make g++ gcc libsystemd-dev
sudo mkdir /opt/zigbee2mqtt
sudo chown "$USER" /opt/zigbee2mqtt
git clone --depth 1 https://github.com/Koenkk/zigbee2mqtt.git /opt/zigbee2mqtt
cd /opt/zigbee2mqtt
pnpm install --frozen-lockfile
ls -l /dev/serial/by-id/Сохраните следующий файл как /opt/zigbee2mqtt/data/configuration.yaml, заменив все значения REPLACE_…. Путь USB-адаптера берут из вывода команды, а не угадывают по названию модели. Дайте пользователю доступ к последовательному порту через соответствующую группу системы; на Raspberry Pi OS это обычно dialout. После изменения групп нужен новый вход в сеанс.
homeassistant:
enabled: false
mqtt:
server: mqtt://127.0.0.1:1883
base_topic: zigbee2mqtt
user: z2m
password: REPLACE_Z2M_PASSWORD
serial:
port: /dev/serial/by-id/REPLACE_WITH_REAL_ADAPTER_ID
adapter: ember
frontend:
enabled: true
host: 127.0.0.1
port: 8080
auth_token: REPLACE_FRONTEND_SECRET
advanced:
network_key: GENERATE
cache_state: false
last_seen: ISO_8601
log_output: [console]
availability:
enabled: true
device_options:
retain: false
optimistic: falsenetwork_key: GENERATE предназначен для создания новой сети. В существующей установке сохраняют сетевой ключ и резервную копию каталога data. Прошивка координатора должна соответствовать точной модели и требованиям драйвера ember; старое имя ezsp больше не следует копировать в новый пример.
cache_state: false принципиален для автоматики ниже: сообщение о батарейке не должно повторно публиковать старую температуру. last_seen добавляет время сообщения Zigbee. В этой конфигурации интеграция Home Assistant отключена; для её включения, требующего кэширования, логику свежести нужно проектировать иначе. optimistic: false отключает предположительный ответ о состоянии после команды, но само по себе не доказывает работу вентилятора.
Защитите файл с паролями правами 600 для пользователя сервиса. После подготовки конфигурации запустите pnpm start из каталога проекта. Для доступа с другого компьютера откройте SSH-туннель и перейдите в браузере на http://127.0.0.1:8080:
ssh -L 8080:127.0.0.1:8080 YOUR_USER@PI_ADDRESSВключайте разрешение присоединения только на время сопряжения. У SNZB-02D удерживают кнопку сопряжения до входа в соответствующий режим; точная процедура приведена в карточке модели. Назовите датчики greenhouse_begin, greenhouse_mid и greenhouse_end, затем закройте присоединение. Положите датчики рядом, сравните сообщения и интервалы температуры, затем разнесите их по выбранным точкам теплицы.
Смотрим три линии на одном графике
Установите Telegraf из официального пакета для архитектуры своей системы. В /etc/telegraf/telegraf.d/zigbee.conf замените пароль и токен InfluxDB. Токену достаточно права записи в выбранный bucket; файл должен читать пользователь сервиса Telegraf, а не все пользователи системы.
[[inputs.mqtt_consumer]]
servers = ["tcp://127.0.0.1:1883"]
topics = ["zigbee2mqtt/greenhouse_begin", "zigbee2mqtt/greenhouse_mid", "zigbee2mqtt/greenhouse_end"]
username = "telegraf"
password = "REPLACE_TELEGRAF_PASSWORD"
qos = 1
persistent_session = false
client_id = "telegraf-zigbee"
data_format = "json_v2"
[[inputs.mqtt_consumer.topic_parsing]]
topic = "zigbee2mqtt/+"
tags = "_/sensor"
[[inputs.mqtt_consumer.json_v2]]
measurement_name = "zigbee"
timestamp_path = "last_seen"
timestamp_timezone = "UTC"
timestamp_format = "2006-01-02T15:04:05Z07:00"
[[inputs.mqtt_consumer.json_v2.field]]
path = "temperature"
type = "float"
optional = true
[[inputs.mqtt_consumer.json_v2.field]]
path = "humidity"
type = "float"
optional = true
[[inputs.mqtt_consumer.json_v2.field]]
path = "battery"
type = "float"
optional = true
[[inputs.mqtt_consumer.json_v2.field]]
path = "linkquality"
type = "int"
optional = true
[[outputs.influxdb_v2]]
urls = ["http://127.0.0.1:8086"]
token = "REPLACE_BUCKET_WRITE_TOKEN"
organization = "farm"
bucket = "sensors"Этот пример проверен с Telegraf 1.36.3. Подписка ограничена тремя датчиками, поэтому служебные сообщения bridge не смешиваются с измерениями. Имя датчика извлекается из темы MQTT в тег sensor; необязательные поля допускают отдельные отчёты о температуре, влажности или батарейке. Время точки берётся из last_seen. При отсутствии этого поля парсер может использовать текущее время, поэтому после настройки проверьте его наличие в сообщениях и синхронизацию часов.
Перезапустите Telegraf и найдите в InfluxDB measurement zigbee с тремя значениями sensor. В Grafana постройте температуру и влажность отдельно для каждой точки. Сравните, как меняются линии в течение дня. При разрыве связи или перезапуске в этой схеме возможен пробел: постоянной дисковой очереди пока нет.
Уведомления настройте отдельными правилами Grafana: низкая температура и отсутствие свежей температуры для каждого ожидаемого датчика. Для фиксированного состава сети надёжнее три явных запроса, чем один запрос, из которого пропавший датчик просто исчезнет. В политике обработки No Data и ошибок задайте оповещение, подключите Telegram как контакт и проверьте тестовое сообщение, отключение датчика и остановку Telegraf. Пример порога 3 °C требует согласования с культурой и местом измерения.
Какую команду предложит вентиляции средняя температура
Запустим fan.py и посмотрим на расчёт. Он берёт среднее только по трём свежим показаниям. В примере при 30 °C появляется ON, при 26 °C — OFF, а между ними сохраняется прежнее намерение. Предел свежести 900 секунд подберите по реальным отчётам и нужному времени реакции.
Неверные данные или просроченная температура блокируют управление до ручного перезапуска. При доступном MQTT скрипт посылает OFF, но при обрыве связи не может гарантировать доставку. Выключение вентилятора не является универсальным безопасным состоянием теплицы: независимый термостат, защита двигателя, ручной режим и контроль фактической работы должны действовать независимо от Raspberry Pi и радиосети.
Сохраните код и установите зависимость в отдельное окружение:
python3 -m venv ~/zigbee-fan-venv
~/zigbee-fan-venv/bin/pip install paho-mqtt==2.1.0#!/usr/bin/env python3
"""Учебное управление: по умолчанию только журнал, без команд реле."""
import argparse
import json
import math
import signal
import time
from datetime import datetime
from pathlib import Path
import paho.mqtt.client as mqtt
SENSORS = ("greenhouse_begin", "greenhouse_mid", "greenhouse_end")
BASE = "zigbee2mqtt/"
MAX_AGE = 900.0 # Пример: 15 минут; согласуйте с отчётами датчиков.
ON, OFF = 30.0, 26.0 # Пример настройки, не агрономическая норма.
class Policy:
def __init__(self, now):
self.started = now
self.samples = {}
self.state = "OFF"
self.ready = False
self.fault = ""
def fail(self, reason):
self.fault = self.fault or reason
self.state = "OFF"
def receive(self, name, payload, retained, wall, mono):
if name not in SENSORS or retained or self.fault:
return
try:
if len(payload) > 4096:
raise ValueError("large payload")
data = json.loads(payload)
if not isinstance(data, dict):
raise ValueError("not object")
if "temperature" not in data:
return # Батарейка/влажность не освежает температуру.
value = data["temperature"]
if type(value) not in (int, float) or not math.isfinite(value):
raise ValueError("not finite number")
if not -40 <= value <= 80:
raise ValueError("outside example range")
stamp = datetime.fromisoformat(data["last_seen"].replace("Z", "+00:00"))
if stamp.tzinfo is None:
raise ValueError("missing timezone")
timestamp = stamp.timestamp()
age = wall - timestamp
if not -5 <= age < MAX_AGE:
raise ValueError("stale or future timestamp")
old = self.samples.get(name)
if old and timestamp <= old[2]:
return # Повтор MQTT не продлевает срок годности.
self.samples[name] = (float(value), mono - max(age, 0), timestamp)
except (ValueError, TypeError, KeyError, AttributeError, OverflowError):
self.fail("invalid temperature: " + name)
def tick(self, now):
if self.fault:
return "OFF"
if any(now - row[1] >= MAX_AGE for row in self.samples.values()):
self.fail("stale temperature")
elif len(self.samples) != len(SENSORS):
if self.ready or now - self.started >= MAX_AGE:
self.fail("missing sensor")
else:
self.ready = True
average = sum(row[0] for row in self.samples.values()) / len(SENSORS)
if average >= ON:
self.state = "ON"
elif average <= OFF:
self.state = "OFF"
return self.state
class Controller:
def __init__(self, client, armed):
self.client, self.armed = client, armed
self.policy = Policy(time.monotonic())
self.online = False
self.pending = {}
self.sent = None
self.last_log = None
def connected(self, client, userdata, flags, reason, properties):
if reason.is_failure:
raise RuntimeError("MQTT authentication/connection rejected")
self.online = True
topics = [(BASE + name, 1) for name in SENSORS]
rc, _ = client.subscribe(topics)
if rc != mqtt.MQTT_ERR_SUCCESS:
raise RuntimeError("subscribe failed")
self.send("OFF") # Начальное намерение, не доказательство выключения.
def subscribed(self, client, userdata, mid, reasons, properties):
if any(reason.is_failure for reason in reasons):
raise RuntimeError("subscription denied")
def received(self, client, userdata, message):
name = message.topic.removeprefix(BASE)
self.policy.receive(name, message.payload, message.retain,
time.time(), time.monotonic())
def published(self, client, userdata, mid, reason, properties):
self.pending.pop(mid, None) # PUBACK подтверждает только приём брокером.
if reason.is_failure:
raise RuntimeError("publish rejected")
def send(self, state):
if self.armed:
info = self.client.publish(BASE + "fan_relay/set",
json.dumps({"state": state}), qos=1, retain=False)
if info.rc != mqtt.MQTT_ERR_SUCCESS:
raise RuntimeError("publish failed")
self.pending[info.mid] = time.monotonic() + 10
self.sent = state
def tick(self):
now = time.monotonic()
state = self.policy.tick(now)
if self.online and state != self.sent:
self.send(state)
status = (state, self.policy.ready, self.policy.fault)
if status != self.last_log:
print("COMMAND" if self.armed else "MONITOR", status, flush=True)
self.last_log = status
if any(now >= deadline for deadline in self.pending.values()):
raise RuntimeError("PUBACK timeout")
if not self.online and now - self.policy.started > 10:
raise RuntimeError("CONNACK timeout")
def stop(self):
self.policy.fail("stopped; inspect equipment before restart")
if self.armed and self.client.is_connected():
try:
self.send("OFF")
until = time.monotonic() + 2
while self.pending and time.monotonic() < until:
if self.client.loop(timeout=0.1) != mqtt.MQTT_ERR_SUCCESS:
break
except Exception:
pass # При обрыве сети доставка OFF не гарантируется.
self.client.disconnect()
def main():
parser = argparse.ArgumentParser()
parser.add_argument("config", help="JSON с MQTT-паролем; права 0600")
parser.add_argument("--armed", action="store_true")
args = parser.parse_args()
config = json.loads(Path(args.config).read_text())
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2,
client_id="greenhouse-fan", clean_session=True,
reconnect_on_failure=False)
client.username_pw_set("fan", config["password"])
client.connect_timeout = 5
control = Controller(client, args.armed)
client.on_connect = control.connected
client.on_subscribe = control.subscribed
client.on_message = control.received
client.on_publish = control.published
def stop_signal(*unused):
raise KeyboardInterrupt
signal.signal(signal.SIGTERM, stop_signal)
try:
client.connect("127.0.0.1", 1883, keepalive=15)
while True:
if client.loop(timeout=1) != mqtt.MQTT_ERR_SUCCESS:
raise RuntimeError("MQTT lost; manual restart required")
control.tick()
except KeyboardInterrupt:
pass
finally:
control.stop()
if __name__ == "__main__":
main()Создайте рядом fan.json с содержимым {"password":"REPLACE_FAN_PASSWORD"} и правами 600. Начнём с наблюдения за расчётом:
~/zigbee-fan-venv/bin/python fan.py fan.jsonРежим MONITOR только выводит расчёт в журнал. После проверки нагрузочной схемы и испытаний на тестовом исполнительном узле параметр --armed разрешает отправку команд. Запускайте только один экземпляр контроллера; не добавляйте автоматический перезапуск для режима управления. Если ранее использовалась другая автоматика, проверьте и удалите оставленные ею сохранённые команды MQTT в теме fan_relay/set перед подключением исполнительного узла.
Подтверждение MQTT QoS 1 означает получение команды брокером, а не вращение вентилятора. Код не использует обратную связь от двигателя и не является автономной защитой теплицы. Он также намеренно не переподключается автоматически: после обрыва связи оператор проверяет оборудование и запускает новый процесс, чтобы старое ON не было повторно отправлено при восстановлении соединения.
Оставляем наблюдение работать после закрытия терминала
После успешного ручного запуска остановите Zigbee2MQTT и создайте /etc/systemd/system/zigbee2mqtt.service. Замените YOUR_USER и, если нужно, путь к Node.js по выводу command -v node.
[Unit]
Description=Zigbee2MQTT
After=network-online.target mosquitto.service
Wants=network-online.target
[Service]
User=YOUR_USER
WorkingDirectory=/opt/zigbee2mqtt
Environment=NODE_ENV=production
Type=notify
ExecStart=/usr/bin/node index.js
WatchdogSec=10s
Restart=always
RestartSec=10s
[Install]
WantedBy=multi-user.targetsudo systemctl daemon-reload
sudo systemctl enable --now mosquitto zigbee2mqtt telegraf
journalctl -u zigbee2mqtt -u telegraf -n 50Перезагрузите сервер и дождитесь новых точек от всех трёх датчиков. Проверьте время и уведомления. Если подключён исполнительный узел, отдельно посмотрите на фактическое действие — так проверяется вся цепочка после запуска.
Если нужно расширить сеть, ищите явную роль Router у выбранных устройств: питание от сети само по себе её не определяет. Например, ZBMINIL2 остаётся конечным устройством. Проверьте приём на рабочих местах датчиков и используйте карту сети для поиска проблем. USB-удлинитель поможет отнести координатор от источников помех.
При проблемах с сопряжением начните с модели, процедуры сброса, разрешения присоединения и журнала. При пропавших сообщениях проверьте батарейку, интервал, питание роутеров и координатор. Сравнивайте availability с возрастом самой температуры: это разные наблюдения.
OTA доступно только для поддерживаемых моделей и прошивок. Проверку и обновление выполняют через интерфейс Zigbee2MQTT по официальной инструкции OTA, в отдельное время обслуживания с отключённой автоматикой. Перед обновлением сохраняют резервную копию, а после — заново проверяют отчёты датчиков и поведение исполнительного узла.
Первый день наблюдений в теплице
- Все три датчика передают температуру с подходящим интервалом; отдельное сообщение о батарейке не обновляет её возраст.
- Сохранённые MQTT-сообщения, неверный JSON, потеря одного датчика и разрыв связи не приводят к повторному автоматическому включению.
- Ручное управление и независимая защита работают при остановленном Raspberry Pi; фактическое состояние оборудования проверяется отдельно от журнала команд.
- Графики и уведомления выявляют отсутствие данных, а не скрывают его за старым последним значением.
Отметьте в журнале открытие двери или проветривание и найдите этот момент на трёх графиках. Затем отключите один датчик и посмотрите, как это отразится на свежести и расчёте вентиляции. Такие опыты помогут выбрать места установки и понять поведение всей сборки.
Для полевых узлов с другим радиоканалом см. передатчик LoRa на ESP32 и шлюз LoRa с сохранением данных на Raspberry Pi. Сравните эти варианты по покрытию, питанию и нужному способу управления на вашем участке.





