↩️ Назад

Категории

Умный дом на Raspberry Pi результат прожарки гермесом

26.09.2026

Хост: 192.168.88.237 (raspberrypi, Debian 12 bookworm, Pi 5, 8 ГБ RAM, NVMe 117 ГБ) · Документ составлен 26.09.2026 по факту чтения кода и живого стенда.

Коротко о том, что здесь есть. Пять независимых кусков, работающих на одной машине:
  • zigbee2mqtt — общается с Zigbee-устройствами (датчики движения, протечки, реле, кран, свет, качество воздуха, влажность почвы) и публикует их состояние в MQTT.
  • Mosquitto — брокер MQTT. Через него всё общается, шина данных.
  • mqtt_to_mysql.py — слушает MQTT и складывает текущие значения в MariaDB (таблица sensor_data), историю — в sensor_history.
  • mqtt_listener.php — «мозг»: раз в секунду читает последние значения из БД, проверяет правила из таблицы automations и отправляет команды обратно в MQTT.
  • PHP-панель на nginx — веб-интерфейс: графики, список датчиков, редактор правил.
Важная деталь: «мозг» не слушает MQTT. Он читает состояние из базы раз в секунду. Скрипт на Python — единственный, кто слушает брокер, и он пишет в ту же базу. Отсюда все странности с задержками и дублями, о которых ниже.

1. Из чего состоит и как связано

Zigbee-устройствадатчики, реле, кран, свет
→
zigbee2mqtt:8080 (node)
Wi-Fi/Zigbee-мост
→
Mosquitto:1883 +1884 local
брокер, топики zigbee2mqtt/#
mqtt_to_mysql.pyслушает MQTT (paho)
→
MariaDB iot_db:3306
sensor_data, sensor_history
←
mqtt_listener.phpчитает БД раз в 1 сек
mqtt_listener.php
решение о действии
→
mosquitto_pub → брокер
.../set
→
устройство включается→ состояние возвращается в MQTT → в БД
Пользователь браузер /admin/
→
nginx + php8.2-fpm:80
панель, API, графики
→
MariaDB чтение/запись правил и настроек

2. Как идёт обработка одного события (сквозной путь)

  1. Датчик движения сработал → zigbee2mqtt публикует zigbee2mqtt/motion_sensor/presence = {"presence":1,"linkquality":120}.
  2. mqtt_to_mysql.py получает сообщение и раскладывает его по «плоским» топикам: .../presence = 1, плюс весь JSON целиком в топик zigbee2mqtt/motion_sensor. Служебные поля (linkquality, countdown_l*, last_seen) отбрасываются. Каждое значение пишется в sensor_data через INSERT ... ON DUPLICATE KEY UPDATE — в таблице хранится только последнее значение по каждому топику.
  3. Если для топика в sensor_info стоит save_history=1 и значение приводится к числу — дополнительно пишется строка в sensor_history и тут же удаляются старые (оставляем 10 000 последних по топику). Для bool true/false история, кстати, не пишется — они сохраняются как «1»/«0» (число), этот случай работает.
  4. mqtt_listener.php раз в секунду одним запросом вытягивает последнее значение каждого топика из sensor_data в массив $sensorData.
  5. По каждому правилу из automations (перечитываются раз в 60 сек) проверяется условие: trigger_topic + оператор + condition_value. Значение {"state":"ON"} разворачивается до ON.
  6. Если условие выполнилось и состояние изменилось (или истёк cooldown) — правило срабатывает: пишется строка в automation_log, затем выполняется действие.
  7. Действие = вызов mosquitto_pub с топиком и payload из правила (например zigbee2mqtt/fish_usb_reley/set = {"state_l3":"ON"}). Перед отправкой проверяется shouldSkipAction() — если устройство уже в нужном состоянии, команда не отправляется.
  8. Через MQTT состояние возвращается в базу, и на следующем круге движок видит результат. Если у правила задан confirmation_topic, движок ставит правило в «ожидание подтверждения» и ищет в sensor_data совпадение ожидаемого состояния.

3. Сервисы и порты

СлужбаЧто делаетПорт / сокетОт кого запущена
nginxвеб-сервер панели0.0.0.0:80root
php8.2-fpmисполняет PHP панели и APIunix /run/php/php8.2-fpm.sockroot
mariadbбаза iot_db127.0.0.1:3306root
mosquittoMQTT-брокер0.0.0.0:1883 (логин/пароль) + 127.0.0.1:1884 (анонимно)root
zigbee2mqttмост Zigbee ↔ MQTT:8080 (веб-интерфейс)mazzick
iot-automationдвижок правил: mqtt_listener.php—www-data, автозапуск, Restart=always
mqtt-to-mysqlлоггер: mqtt_to_mysql.py—mazzick, автозапуск, Restart=always
cron (root)cron_fish_heater.php каждые 20 мин, cron_fish_feeder.php в 10:00, cron_push.php и cron_automation_emails.php каждую минуту, export_cron.sh в 10:00—root
cron (mazzick)rpi_monitor.sh каждые 5 мин, log_analyzer.sh каждый час, check_battery.py в 09:00, OPTIMIZE TABLE по воскресеньям—mazzick
Двойной путь управления. Часть автоматики живёт в БД и обрабатывается PHP-движком, часть — отдельными cron-скриптами (cron_fish_heater.php, cron_fish_feeder.php, cron_push.php), которые пишут в email_triggers/push_notifications. Это две разные логики про одни и те же устройства — при разборе полётов надо помнить, что событие могло прийти не из движка, а из cron.

4. Файлы и структура (состояние после чистки 26.09.2026)

/var/www/html/iot/
├── public_html/                 ← корень сайта (nginx root)
│   ├── index.php                ← главная панель (плитка датчиков, PWA)
│   ├── index.html               ← статичная старая версия
│   ├── manifest.json, sw.js     ← PWA-манифест и service worker
│   ├── graf.php                 ← графики истории
│   ├── datchiki.php, datchiki2.php, datchiki_min.php   ← страницы/фрагменты датчиков
│   ├── all_charts.php, all_charts2.php, all_charts_2.php, chart_min.php,
│   │   chart_only.php, charts_by_button.php, charts_only.php   ← варианты графиков
│   ├── crontab_all.txt / crontab_mazzick.txt / crontab_root.txt ← выгрузка кронов (для показа в панели)
│   ├── admin/
│   │   ├── sensors.php              ← ВХОД В ПАНЕЛЬ (логин/пароль) + редактор списка датчиков
│   │   ├── logic.php                ← редактор правил автоматизации (то, что лежит в automations)
│   │   ├── automation_log.php       ← просмотр журнала срабатываний
│   │   ├── mqtt_listener.php        ← ДВИЖОК правил (запускается systemd, не через браузер)
│   │   ├── mqtt_to_mysql.py         ← ЛОГГЕР MQTT→БД (запускается systemd; перенесён сюда 26.09.2026)
│   │   ├── mqtt_listener_logs.php, log_viewer.php, cron_logs.php, read_log.php, logs_proxy.php
│   │   │                            ← разные просмотрщики логов (дублируют друг друга)
│   │   ├── service_status.php       ← статус служб для панели
│   │   ├── send_mqtt.php, set_channel.php, clear_log.php  ← мелкие утилиты
│   │   ├── cottage_panel.php        ← демо-панель «коттедж» на выдуманных данных
│   │   └── logout.php               ← выход
│   ├── api/
│   │   ├── live_data.php            ← JSON последних значений датчиков (для главной)
│   │   ├── live_system_data.php     ← JSON состояния самой малины (темп/память/диск)
│   │   ├── history.php              ← JSON истории для графиков
│   │   ├── control.php              ← команда устройству: ?topic=...&state=ON  (без авторизации!)
│   │   └── clear_history.php        ← очистка истории
│   ├── include/links_panel.php      ← общее меню ссылок панели
│   ├── css/                         ← style.css, index*.css, logic_admin.css
│   ├── icons/                       ← иконки PWA (ВСЕ файлы по 0 байт — битые)
│   ├── db/sensors.db                ← SQLite-остаток от старой версии, не используется
│   └── mqtt/                        ← php-mqtt (composer) — НЕ используется ни одной страницей
└── cron_*.php                       ← скрипты, которые запускает cron (см. таблицу служб)
    cron_automation_emails.php, cron_fish_feeder.php, cron_fish_filter.php,
    cron_fish_heater.php, cron_fish_light.php, cron_motion_hourly.php,
    cron_polivayka.php, cron_push.php, cron_soil_moisture.php, generate_sensor_data.py

Что перенесено в карантин (не удалено): номерные версии index2..index40, тестовые test1..test2222.php, все *.phparc / *.phpgood / *.bak / .swp, дубли-копии движка и логгера в домашней папке, старые SQL-дампы, каталоги-копии сайта. Всё лежит в /home/mazzick/cleanup_20260926/, полный список — manifest.txt (25 179 файлов, 1,5 ГБ).

5. Логика движка mqtt_listener.php

Один бесконечный цикл while(true) с паузой sleep(1). Что происходит на каждой итерации:

раз в 60 сек  → reloadRules(): SELECT * FROM automations WHERE enabled=1
раз в 1 сек   → SELECT последних значений по каждому topic из sensor_data → $sensorData
раз в 1 сек   → проверка РАСПИСАНИЯ (schedule_type = daily/weekly):
                  если текущее время >= schedule_time И правило сегодня ещё не срабатывало
                  → evaluateRuleCondition() → срабатывание
каждую итерацию → проверка ТРИГГЕРНЫХ правил (schedule_type = none):
                  trigger_topic + оператор + condition_value
                  → cooldown проверка → срабатывание при СМЕНЕ значения
всегда        → checkPendingConfirmations(): ищем подтверждение действий
всегда        → отложенные действия (delay_seconds): выполняем, когда время пришло

Механики, которые реально работают

МеханикаКак работаетГде настраивается в панели
Условиеtrigger_topic + оператор (> < == != >= <=) + condition_value; если значение — JSON с полем state, берётся оноlogic.php → правило → «Условие»
condition_durationусловие должно держаться N секунд, иначе таймер сбрасываетсяlogic.php → «Длительность условия»
cooldown_secondsпосле действия правило молчит N секундlogic.php → «Пауза»
Смена состояниясрабатывает только при изменении значения (защита от повторов), lastTriggerStateавтоматически
shouldSkipActionесли устройство уже в нужном состоянии — команда не отправляетсяавтоматически
delay_secondsдействие через N секунд (в памяти процесса, не в БД)logic.php → «Задержка»
confirmation_topicждём, что устройство подтвердит состояние; при подтверждении правило удаляется (если не стоит «Повторять»)logic.php → «Подтверждение»
persistent«Повторять»: правило не удаляется после срабатыванияlogic.php → галочка
dependency_topic/valueдоп. условие-разрешение (например «окно закрыто»)logic.php → «Зависимость»
schedule_typenone / daily / weekly + время + дни неделиlogic.php → «Расписание»
Мёртвый код в движке. В таблице есть колонки, которые движок не использует вовсе: hysteresis (гистерезис), max_retries/retry_delay/failure_count (повторы), time_restriction_start/end (окно времени), in_progress, push_notify/push_topic/push_priority. Также есть режим condition_type='script' — исполнение PHP-выражения из БД (файл /tmp/eval_*.php + include): ни одно правило его не использует, а фильтрация там слабая. Рекомендую выключить эту возможность (см. раздел 8).

6. База данных iot_db

ТаблицаЧто хранитРазмер / строки
sensor_dataпоследнее значение по каждому топику (137 топиков). Ключ уникальности — topic, поэтому строка одна на топик85 строк
sensor_historyистория чисел для графиков; пишется логгером, чистится до 10 000 последних на топик≈101 тыс. строк, 11 МБ
sensor_infoсправочник топиков: название, комната, группа, иконка, save_history (что писать в историю)25 строк
automationsправила автоматизации — то, что редактируется в logic.php17 строк
automation_logжурнал срабатываний: правило, топик, payload, skipped, confirmed, expected/actual state16 271 строка (после чистки)
automation_email_sent, email_triggersучёт отправленных писем (чтобы не спамить), пишут cron-скрипты56 / 855
rpi_monitorтелеметрия самой малины (темп, нагрузка, память, диск) — cron каждые 5 мин≈73 тыс. строк, 9,5 МБ
log_statsсчётчики ключевых слов из journald — cron каждый час1 695
daily_resetsотметки ежедневного сброса правил95
push_notifications, rs485_commands, rs485_sensor_data, sensorsпустые таблицы — остатки нереализованных функций (push, RS-485, старая схема датчиков)0 строк

7. Что исправлено 26.09.2026 (с проверкой)

#ПроблемаЧто сделаноКак проверено
1Логи дублировались (раньше троились)В MariaDB жил триггер after_automation_update на таблице automations: при каждом обновлении last_triggered он вставлял ещё одну строку в automation_log. Движок пишет в лог сам и затем обновляет last_triggered → каждое событие превращалось в 2-3 строки. Триггер удалён (определение сохранено в /home/mazzick/przorka_backup/trigger_after_automation_update.sql)Ручное UPDATE last_triggered больше не создаёт строку (до/после = 27 321). Сквозной тест: одно событие → ровно 1 строка в логе
2«Фантомные» строки в журналеУдалены 11 060 строк без rule_id (следы триггера) — журнал 27 331 → 16 271, таблица пересобрана (OPTIMIZE)Повторный подсчёт по базе
3Расписание молотило одно правило каждую секунду (169 и 170 дали по 50 срабатываний за 50 секунд, лог рос без остановки, пока время суток больше заданного)В mqtt_listener.php: правило расписания проверяется не чаще одного раза в 30 секунд; если действие уже не нужно (устройство в нужном состоянии) — правило помечается выполненным на сегодня, а не перепроверяется до полуночиВременное правило расписания: за минуту ровно 1 проверка (было ~60). php -l — без ошибок. Резервная копия до правки: przorka_backup/mqtt_listener.php.orig
4mqtt_to_mysql.py лежал в домашней папке, вне сайтаСкрипт перенесён в /var/www/html/iot/public_html/admin/mqtt_to_mysql.py, юнит mqtt-to-mysql.service обновлён; запускается от пользователя mazzick тем же venv (/home/mazzick/mqtt-env). Старая копия — в przorka_backup/mqtt_to_mysql.py.origСлужба active, значения приходят в БД, лог чистый
5Лог логгера вырос до 6,25 ГБ: на каждое сообщение писалось INFO, ≈16 млн строк за 17 днейУровень сообщения «📥 Получено» понижен до debug, лог обрезан, добавлен /etc/logrotate.d/mqtt-logger (50 МБ, 5 файлов, сжатие, copytruncate)Лог остаётся 228 байт, данные идут, 0 ошибок в журнале службы
6По HTTP отдавались исходники и резервные копии: /admin/mqtt_to_mysql.py, sensors.bak, .cron_logs.php.swp, crontab_root.txt, test_fish_power.sh, копии движка, logic.phparcsecВ nginx добавлены правила: файлы *.bak/*.py/*.sh/*.sql/*.log/*.txt/*.swp и *.phpgood* не отдаются (403)Все перечисленные адреса → 403; панель, /manifest.json, /sw.js, иконки и .php-страницы работают как раньше
7Мусор: 44 файла-версии в корне сайта, 26 архивных копий в admin, дубли движка и логгера в домашней папке, 3 каталога-копии сайта (1,3 ГБ), старые дампы БДВсё перенесено в карантин /home/mazzick/cleanup_20260926/ — ничего не удалено, есть manifest.txt со списком. Освобождено ≈5 ГБ на диске (35 ГБ → 30 ГБ) и 1,5 ГБ в карантинеГлавные страницы отдают 200, новых 404 в логах nginx нет, движок и логгер без ошибок

8. Что осталось сделать (по приоритету)

Сначала — безопасность (сейчас дом открыт)

  • Пароль панели ходит в открытом виде. В admin/sensors.php логин и пароль приходят методом GET — в журнале nginx видно ?username=...&password=09540056. Сама панель паролем 09540056 и закрыта, но пароль совпадает с паролем SSH и с паролем MQTT.
  • Главная панель и API открыты. index.php, graf.php, api/live_data.php отдают данные без авторизации, а api/control.php?topic=...&state=ON вообще позволяет любому в сети включать и выключать устройства.
  • nginx слушает всю сеть (0.0.0.0:80), как и MQTT (1883). Кто в сети — тот в доме.
  • Пароли в коде открытым текстом — в ~30 PHP-файлах, в mqtt_to_mysql.py, в двух python-скриптах, в cron-заданиях и в скриптах бэкапа. Учётка iot_user/Abudfv09540056 вообще лежала в файле, доступном по HTTP → считай её скомпрометированной.
  • Пароль SMTP от it@logoakademia.ru лежит в ~/.msmtprc и попадал в ~/.msmtp.log (файл 3,8 МБ — стоит почистить).
Что предлагаю сделать: свести все учётки в /etc/iot/credentials.ini (права 640, владелец www-data) и читать их из кода; сменить пароли (отдельный для панели, отдельный для MQTT, отдельный для БД iot_ro/iot_rw); закрыть панель basic-auth или разрешить доступ только с домашней подсети в nginx; MQTT с 1883 слушать только на 127.0.0.1 и локальной сети; на api/control.php повесить токен.

Потом — логика и лишний код

  • Отключить исполнение PHP из базы (condition_type='script'): сейчас движок собирает файл в /tmp и делает include — это фактически запуск произвольного кода из таблицы. Ни одно правило так не работает.
  • Две логики управления: часть скриптов в cron (cron_fish_heater.php, cron_fish_feeder.php, cron_polivayka.php, cron_push.php) дублирует то, что умеет движок. Их стоит перевести в правила automations и оставить один путь — иначе при разборе сбоя непонятно, кто включил свет.
  • Проверить правила 158/159 (включены, но без действия) и 163 (без условия). Настроить имя 169 под фактическое время (09:00, а не 8:00).
  • Убрать неиспользуемое: каталог public_html/mqtt/ (php-mqtt), db/sensors.db, пустые таблицы (push_notifications, rs485_*, sensors), колонки hysteresis/max_retries/retry_delay/time_restriction_*/push_*, страницы-дубли (datchiki.php vs index.php, четыре варианта all_charts*, четыре просмотрщика логов).
  • Подтверждения и задержки живут только в памяти процесса: delay_seconds, ожидание подтверждения, cooldown после перезапуска теряются. Для «выключить свет через 60 секунд» это значит, что systemctl restart отменяет отложенное действие.
  • Задержка реакции до 1 секунды (цикл с sleep(1)) и полное чтение 137 топиков каждую секунду — на этом железе терпимо, но по-хорошему движок должен слушать MQTT сам, а не опрашивать базу.
  • Журналы systemd не сохраняются (Storage=volatile): при перезагрузке пропадают, а log_analyzer.sh каждый час считает по ним статистику. Включить постоянное хранение и ограничить размер.
  • Ротация: /var/log/cron_automation_emails.log (7 МБ) и ~/.msmtp.log (3,8 МБ) не ротируются вообще.
  • Иконки PWA по 0 байт — приложение на телефон ставится без иконки.

Проверка после правок

# движок и логгер живы
systemctl is-active iot-automation mqtt-to-mysql mosquitto zigbee2mqtt mariadb nginx

# данные идут (timestamp должен быть «сейчас»)
mysql -u mazzick -p... iot_db -e "SELECT topic,LEFT(value,20),timestamp FROM sensor_data ORDER BY timestamp DESC LIMIT 3;"

# нет ошибок
journalctl -u iot-automation -u mqtt-to-mysql --since "10 min ago" | grep -iE "error|fatal|traceback"

# лог логгера не растёт
ls -la /home/mazzick/mqtt_logger.log       # должен оставаться 228 байт

# дублей в журнале больше нет: за одно срабатывание — одна строка
mysql -u mazzick -p... iot_db -e "SELECT triggered_at,rule_name,COUNT(*) FROM automation_log WHERE triggered_at > NOW()-INTERVAL 1 HOUR GROUP BY 1,2 HAVING COUNT(*)>1;"

# вернуть файл из карантина
mv /home/mazzick/cleanup_20260926/admin/<файл> /var/www/html/iot/public_html/admin/

9. Откат всего, что сделано сегодня

ЧтоГде резервная копияКак вернуть
Триггер БДprzorka_backup/trigger_after_automation_update.sqlвыполнить файл в mysql (вернёт дубли)
Правка расписания в движкеprzorka_backup/mqtt_listener.php.origcp ... /var/www/html/iot/public_html/admin/mqtt_listener.php && systemctl restart iot-automation
Логгер (перенос + уровень логирования)przorka_backup/mqtt_to_mysql.py.orig, mqtt-to-mysql.service.origвернуть файл в /home/mazzick/ и ExecStart в юните
Правила nginxprzorka_backup/nginx-iot.origвернуть файл в /etc/nginx/sites-available/iot, nginx -t && systemctl reload nginx
Удалённые строки журнала—только из бэкапа БД /home/mazzick/backup_smart_home_*.tar.gz (внутри полный дамп)
Файлы, убранные из сайта и дома/home/mazzick/cleanup_20260926/ + manifest.txtперенести обратно mv; удалить карантин целиком — rm -rf /home/mazzick/cleanup_20260926
Схема составлена по факту чтения кода: admin/mqtt_listener.php (750 строк, sha256 ec79ab15…389cd0e), admin/mqtt_to_mysql.py (153 строки), юниты systemd, конфиг mosquitto и nginx, структура iot_db, crontab пользователей mazzick и root. Все утверждения раздела 7 проверены на живом стенде 26.09.2026.



Категории:

Категории

← Назад к списку

Посетителей сегодня: 0
о блоге | карта блога | 📡 Подписаться на RSS

© Digital Specialist | Не являемся сотрудниками Google, Яндекса и NASA