Умный дом на Raspberry Pi

Описание проекта и архива: что за система, из чего она состоит, как развернуть её с нуля и на какие грабли уже наступали. Документ лежит внутри архива рядом с кодом — читать перед тем, как что-то менять.

Содержание
  1. Что это такое
  2. Как идут данные
  3. Службы и порты
  4. Состав архива
  5. База данных
  6. Развёртывание с нуля
  7. Секреты и доступ
  8. Грабли
  9. Откат

1. Что это такое

Самописная домашняя автоматизация без облака и без готовых платформ. Всё крутится на одной Raspberry Pi: Zigbee-устройства (датчики движения, протечки, температуры и влажности, реле, краны, розетки) общаются с координатором, zigbee2mqtt превращает их сообщения в MQTT, Mosquitto раздаёт их, логгер складывает значения в MariaDB, движок правил сравнивает их с таблицей правил и отправляет команды обратно в брокер, а веб-панель показывает состояние и даёт править правила и справочник датчиков.

Ключевая идея архитектуры: брокер — это шина событий, база — состояние и правила. Движок не опрашивает базу по таймеру, а подписан на темы и реагирует в момент сообщения. Расписания, выдержки условия и отложенные команды остаются на такте в одну секунду — это единственное, что действительно требует таймера.

2. Как идут данные

Zigbee-устройства
      │  радио
zigbee2mqtt ──► Mosquitto (127.0.0.1:1883, с логином)
                    │
        ┌───────────┴────────────┐
 mqtt_to_mysql.py            mqtt_listener.php
 логгер: JSON → таблицы      движок: правила → команды в .../set
        │                        │
        └──────► MariaDB ◄───────┘
                    │
            nginx + PHP 8.2 (панель)

Сквозной путь одного события: датчик протечки прислал water_leak: true → zigbee2mqtt опубликовал JSON в zigbee2mqtt/leak_bath → логгер разложил поля в sensor_data и sensor_history → движок, подписанный на тему, разобрал JSON, проверил правила на поле water_leak, нашёл совпадение и опубликовал {"state":"ON"} в zigbee2mqtt/fish_light/set → реле включило свет → устройство доложило новое состояние → строка в automation_log.

3. Службы и порты

СлужбаЧто делаетПорт / сокетПользователь
zigbee2mqttмост Zigbee → MQTTUSB-координаторпользователь pi
mosquittoMQTT-брокер127.0.0.1:1883 (логин обязателен)mosquitto
mqtt-to-mysqlлоггер MQTT → базаисходящие к брокеру и БДпользователь pi
iot-automationдвижок правилисходящие к брокеруwww-data
iot-anomalyсбор точек для модуля аномалий (в оригинале выключен)—пользователь pi
mariadbбаза данныхunix-сокетmysql
nginx + php8.2-fpmвеб-панель:80, только разрешённые адресаwww-data
Анонимный MQTT закрыт. Раньше рядом с защищённым слушал порт без пароля, и любая программа на малине могла командовать устройствами. Теперь единственный вход — с логином. Симптом возврата: в логах скриптов «Connection Refused: not authorised» — значит где-то mosquitto_pub без учётных данных.

4. Состав архива

ПутьЧто внутри
site/public_html/корень сайта: главная с карточками и графиками, admin/, api/, css/, icons/, include/
site/public_html/admin/sensors.phpвход в панель и справочник датчиков: имя, комната, группа, иконка, признак «сохранять историю»
site/public_html/admin/logic.phpредактор правил автоматизации (таблица automations)
site/public_html/admin/mqtt_listener.phpдвижок правил: подписка на брокер, вычисление условий, публикация команд, журнал
site/public_html/admin/mqtt_client.phpсвой клиент MQTT 3.1.1 на сокетах — используется только движком
site/public_html/admin/mqtt_to_mysql.pyлоггер: раскладка JSON в sensor_data и sensor_history
site/public_html/admin/batteries.phpсостояние элементов питания; общая логика — в include/battery_lib.php
site/public_html/admin/automation_log.phpжурнал срабатываний: что, когда, отправлена ли команда, подтвердило ли устройство
site/config/iot_config.phpобщий конфиг PHP: подключение к базе и константы IOT_*; читает секреты из /etc/iot/
site/db/*.sqlтаблицы, которые создаёт только администратор базы
cron/cron_*.phpзадачи в обход движка: кормушка, нагрев, свет, письма, полив
cron/crontab_mazzick.txt, cron/crontab_root.txtрасписания пользователя и root
scripts/healthcheck, сторож с письмом, проверка батареек, сбор аномалий, отправка push, бэкап, чистка базы
bash_scripts/анализ журналов, мониторинг самой малины, рассылка тревог
systemd/четыре юнита: iot-automation, mqtt-to-mysql, iot-anomaly, zigbee2mqtt
nginx/iot.confконфиг сайта: доступ по подсети и запрет отдачи не-веб файлов (.py, .sql, .log, бэкапы)
mosquitto/mosquitto.conf и conf.d/mqtt.conf — слушатель только на localhost, логин обязателен
zigbee2mqtt/configuration.yamlконфиг Z2M: последовательный порт, сетевой ключ Zigbee, MQTT, список устройств
secrets/credentials.iniвсе секреты приложения (права 640, владелец root:www-data)
secrets/credentials.envте же секреты для shell и cron (640, root:пользователь pi)
db/schema_iot_db.sqlструктура базы: CREATE TABLE, триггеры, процедуры. Без данных
logrotate-*.confротация журналов (логгер умеет писать гигабайты, если забыть)
README.md, docs/*.htmlэто описание, статья об обновлении, таблица новых паролей
Чего в архиве нет. Данных датчиков (история — сотни тысяч строк, при развёртывании собирается заново), node_modules и vendor (ставятся пакетным менеджером), журналов, бэкапов и карантина мусора — это следы эксплуатации, а не проект.

5. База данных

22 таблицы. Данные не выгружались — только структура, чтобы было видно устройство.

ГруппаТаблицыЗачем
Состояние и историяsensor_data, sensor_historyпоследнее значение по топику и все значения для графиков
Справочникsensor_info, battery_specимена, комнаты, группы датчиков; типы элементов питания и пороги
Автоматизацияautomations, automation_log, pending_actions, automation_email_sent, email_triggersправила, журнал срабатываний, отложенные команды и ожидания подтверждений, защита от повторных писем
Уведомленияpush_notifications, daily_resetsочередь push и суточные счётчики
Аномалииanomaly_watchers, anomaly_samples, anomaly_events, anomaly_requests, anomaly_reportsкарточки наблюдения, точки, события, запросы и отчёты (модуль в оригинале выключен)
Прочееrpi_monitor, log_stats, rs485_commands, rs485_sensor_data, sensors, zz_test_permметрики самой малины, статистика журналов, задел под RS-485, служебные
Две роли в базе. Прикладная учётка умеет только SELECT/INSERT/UPDATE/DELETE. Создание и изменение таблиц — руками администратора. Это не строгость ради строгости: если приложение само себе меняет схему, откат превращается в археологию.

6. Развёртывание с нуля

  1. Пакеты: nginx php8.2-fpm php8.2-mysql mariadb-server mosquitto mosquitto-clients python3-venv nodejs npm.
  2. База: создать iot_db в utf8mb4, залить db/schema_iot_db.sql, завести прикладного и административного пользователей.
  3. Секреты: /etc/iot/credentials.ini (640, root:www-data) и credentials.env (640, root:пользователь pi). Значения — как в README.
  4. Сайт: разложить site/ в /var/www/html/iot/, поправить пути в nginx/iot.conf, включить сайт, nginx -t && systemctl reload nginx.
  5. Службы: юниты из systemd/ в /etc/systemd/system/, daemon-reload, enable --now iot-automation mqtt-to-mysql.
  6. MQTT: mosquitto/ в /etc/mosquitto/, пользователя завести через mosquitto_passwd -c /etc/mosquitto/passwd iot.
  7. Zigbee2mqtt: конфиг в каталог Z2M, вписать сетевой ключ и порт координатора, systemctl enable --now zigbee2mqtt.
  8. Cron и сторож: задания из cron/crontab_*.txt, healthcheck.sh в домашний каталог, ежечасный iot_watchdog.sh.
  9. Проверка: все службы active, в sensor_data свежие метки времени, в журнале движка «Загружено N активных правил» и ни одной ошибки PHP.

7. Секреты и доступ

Если положить проект в публичный репозиторий: пароли из README станут публичными. Правильный порядок — держать репозиторий приватным либо заменить значения в secrets/* на заглушки, а настоящие хранить только на самой малине.

8. Грабли

Милливольты против вольт

У батарейных Zigbee-устройств поле voltage — это милливольты (2500…3000), а у розетки с измерением мощности то же поле — вольты сети (207…253). Если собирать «батарейки» поиском по имени топика, розетка попадёт в список и будет выглядеть заряженной на тысячи процентов. Список берётся из battery_spec, а пороги в ней записаны в вольтах — сравнивать надо с милливольтами.

Темы «устройство/поле» существуют только в базе

При output: json в брокер приходит одно сообщение на устройство. Движок обязан разбирать JSON и проверять правила по каждому полю — иначе правила на датчики молча не срабатывают, при этом расписания продолжают работать. Симптом: «по времени работает, по топику нет».

Payload обязан соответствовать топику

Для многоканальных реле возможны две формы: .../set/state_l3 + {"state":"ON"} или .../set + {"state_l3":"ON"}. Перепутал — в журнале «успешно отправлено», а реле молчит.

Выдержка условия у простых правил

«Условие держится N секунд» должно работать и для триггерных правил, а не только для расписаний. Иначе кран закрывается в ту же секунду, когда открылся.

Состояние в памяти процесса

Отложенные команды и ожидания подтверждений переживают перезапуск только потому, что пишутся в таблицу pending_actions. Иначе systemctl restart тихо отменяет всё, что было запланировано.

Топик живёт вечно

Строка последнего значения остаётся в базе, даже если устройство выключили полгода назад. Поэтому у каждой карточки в панели есть возраст значения: «18 дн назад» — это не «всё хорошо», это повод проверить связь.

Приложение не должно создавать таблицы

Наличие таблицы проверяется чтением; если её нет — в журнал идёт понятное сообщение, а создаёт её администратор. Иначе каждая страница и каждый cron сыплют «CREATE command denied».

9. Откат