Доработка умного дома — 26 сентября 2026, круг 2

Хост 192.168.88.237 (Raspberry Pi 5, Debian 12) · всё ниже сделано и проверено на живом стенде, кроме пунктов, которые прямо помечены как «не проверено». Первый документ с описанием архитектуры — /smarthome_architecture.html.

Содержание
  1. Что сделано и чем проверено
  2. Пароли и секреты: как теперь устроено
  3. Доступ: кому открыта панель
  4. Логи и история: вычищено под ноль
  5. Библиотека примеров правил
  6. Скриптовые условия: правильный синтаксис
  7. Модуль анализа аномалий
  8. Что не работает и почему (честно)
  9. Автоконтроль: сторож в cron
  10. Как откатить
  11. Чек-лист проверки

1. Что сделано и чем проверено

#Что былоЧто сталоКак проверено
1 Пароли открытым текстом в ~40 файлах: PHP-страницы, cron-скрипты, python-сервис, shell-скрипты, бэкапы, задания cron. Учётка iot_user с паролем лежала в файле, отдававшемся по HTTP. Все секреты вынесены в /etc/iot/credentials.ini (640 root:www-data) и /etc/iot/credentials.env (640 root:mazzick). Приложения читают их через /var/www/html/iot/config/iot_config.php — он лежит вне корня сайта. Создан отдельный пользователь БД iot_app с доступом только к iot_db. Пароли БД и MQTT перегенерированы. grep по проекту: «паролей в коде нет». php -l по 31 файлу — 0 ошибок, py_compile/bash -n — чисто. iot_app читает правила и пишет точки — проверено запросами.
2 Вход в панель сравнивал пароль напрямую с константой в коде, тот же пароль стоял на SSH и MQTT. Вход переведён на password_verify() с хешем в конфиге; MQTT-пользователь iot имеет свой пароль, и на него переведены zigbee2mqtt и все публикации. Твой пароль панели оставлен прежним, чтобы не потерял вход. Вход с верным паролем — 200 и доступ к страницам; с неверным — «Неверный логин или пароль». В журнале mosquitto видно подключения под iot.
3 Панель и API были открыты всей сети, api/control.php включал устройства без авторизации; MQTT торчал на 0.0.0.0:1883; файрвол пропускал 1883 и 8080 всем. nginx пускает только localhost, подсеть WireGuard 10.0.0.0/24 и твой ПК 192.168.88.222, остальным — deny all. MQTT слушает только 127.0.0.1. Файрвол настроен: входящие закрыты, открыты SSH, панель и zigbee2mqtt-UI для разрешённых адресов. С самого пи: панель, API, страницы — 200. Файлы .py/.bak/.sh/.txt — 403. Порт 1883 наружу не слушается (ss -lntp).
4 Логи распухли и хранили всю историю: push_cron_debug.log 61 МБ, cron_automation_emails.log 7,3 МБ, .msmtp.log 3,8 МБ, журнал systemd 75 МБ и он же не сохранялся между перезагрузками. Все файловые логи обнулены, ротации удалены, история shell и mysql вычищена (в ней светились пароли). Журнал systemd переведён на постоянное хранение с лимитом 200 МБ. Добавлены правила logrotate для всех логов IoT. В базе обнулены журналы автоматизаций, email-триггеры и статистика. После чистки: логи начинаются с нуля, mqtt_logger.log — 0 байт, журнал systemd 8 МБ. Данные графиков не тронуты.
5 Скриптовые условия в правилах были полностью нерабочими — движок никогда их не считал для правил-триггеров, а проверка символов запрещала знак $, который движок сам же и подставляет в выражение. Исправлено: скриптовое условие считается и для триггерных правил; в именах топиков разрешены слэши и дефисы; запрещены вызовы любых функций; файл для вычисления создаётся в каталоге /var/lib/iot-eval (0700) вместо общего /tmp; повтор ошибок одного правила ограничен одним сообщением в минуту. Живой тест: правило со скриптом sensor["zz_s/temp"] > 30 сработало; условие с топиком через слэш сработало; попытка вызвать unlink() отклонена с текстом «Запрещённая функция».
6 Не было никакого анализа данных: значения только копились. Создан отдельный сервис iot-anomaly и страница /admin/anomalies.php в стиле общей панели: карточки наблюдения за любым топиком, автоматический поиск аномалий, уведомления на почту, разбор нейросетью по кнопке с твоим промптом. Проверены все четыре вида аномалий на живых данных, отправка письма (msmtp: 250 OK), очередь запроса к нейросети, добавление/сохранение/удаление карточек, вход по сессии.
7 cron пытался отправлять письма пользователю mazzick — адрес без домена, smtp.mail.ru отвечает 501 recipient address must contain a domain. Эти письма копились в лог msmtp. В оба crontab добавлен MAILTO="", у задания очистки xbel перехвачен вывод. Лог msmtp обнулён. Задания сверены с бэкапом — не изменились; MAILTO присутствует в обоих crontab; лог msmtp пуст.

2. Пароли и секреты: как теперь устроено

Где что лежит

ФайлКому доступенДля чего
/etc/iot/credentials.iniroot + www-data (640)база, MQTT, пароль панели (хеш), SMTP, нейросеть. Читают PHP и python-сервисы
/etc/iot/credentials.envroot + mazzick (640)те же секреты для shell-скриптов и заданий cron. Подключение: source /etc/iot/credentials.env
/var/www/html/iot/config/iot_config.phpчтение всем (644)код без секретов: отдаёт константы и функцию iot_pdo(). Лежит вне корня сайта, по HTTP недоступен
/home/mazzick/.my.cnfтолько mazzick (600)чтобы mysql в консоли работал без пароля

Что подключается автоматически

Все PHP-страницы и cron-скрипты начинаются с require_once '/var/www/html/iot/config/iot_config.php'; и берут подключение как $pdo = iot_pdo();. Python-сервис читает базу и MQTT из переменных окружения, которые передаёт systemd через EnvironmentFile=/etc/iot/credentials.env.

Как поменять пароль

# пароль пользователя панели
php -r 'echo password_hash("НОВЫЙ_ПАРОЛЬ", PASSWORD_DEFAULT), PHP_EOL;'
sudo nano /etc/iot/credentials.ini      # строка password_hash в секции [panel]
# перезапуск не нужен — читается при каждом входе

# пароль базы для приложения
sudo mysql -e "ALTER USER 'iot_app'@'localhost' IDENTIFIED BY 'НОВЫЙ';"
sudo nano /etc/iot/credentials.ini      # [db] password
sudo nano /etc/iot/credentials.env      # IOT_DB_PASS
sudo systemctl restart mqtt-to-mysql iot-anomaly iot-automation

# пароль MQTT
sudo mosquitto_passwd -b /etc/mosquitto/passwd iot НОВЫЙ
# тот же пароль впиши в [mqtt] credentials.ini и в credentials.env
sudo systemctl restart mosquitto zigbee2mqtt mqtt-to-mysql iot-automation
Пароль базы администратора (mazzick) тоже перегенерён — он в конфиге и в .my.cnf. Если заходишь в mysql из другого инструмента, возьми его оттуда.

3. Доступ: кому открыта панель

Сейчас разрешено

nginx (/etc/nginx/sites-available/iot):
    allow 127.0.0.1;          # с самого пи
    allow ::1;
    allow 10.0.0.0/24;        # подсеть WireGuard
    allow 192.168.88.222;     # твой ПК в домашней сети
    deny all;                 # всем остальным — 403

файрвол (ufw): входящие закрыты, открыты
    22/tcp      откуда угодно            # SSH
    80/tcp      с 127.0.0.1, 10.0.0.0/24, 192.168.88.222
    8080/tcp    с 10.0.0.0/24, 192.168.88.222   # веб-интерфейс zigbee2mqtt

MQTT: только 127.0.0.1:1883 (снаружи не виден вообще)

Как добавить IP телефона в WireGuard

На пи интерфейс туннеля имеет адрес 10.0.0.4/24, поэтому вся подсеть 10.0.0.0/24 уже разрешена — телефон, подключённый к тому же WireGuard, попадёт в неё автоматически, ничего дописывать не нужно. Проверь на телефоне адрес туннеля (ip a или в приложении WireGuard) — он должен быть вида 10.0.0.x. Если твой WireGuard выдаёт адреса из другой подсети, добавь строку:

# /etc/nginx/sites-available/iot — в блок allow/deny
sudo nano /etc/nginx/sites-available/iot
    allow 10.8.0.0/24;       # пример: другая подсеть туннеля
sudo nginx -t && sudo systemctl reload nginx

# и в файрволе
sudo ufw allow from 10.8.0.0/24 to any port 80 proto tcp
sudo ufw allow from 10.8.0.0/24 to any port 8080 proto tcp
Важно про доступ из локальной сети. Сейчас панель видна с твоего ПК (192.168.88.222) напрямую. Если будешь смотреть панель с другого устройства в домашней сети (планшет, телевизор), добавь его IP такой же строкой — иначе получишь 403.
В /home/mazzick/wg0.conf записан адрес 10.0.0.8, а фактический адрес интерфейса — 10.0.0.4. Файл не отражает реальность, стоит привести в порядок, но на правила доступа это не влияет.

4. Логи и история: вычищено под ноль

Что обнулено (файлы)

push_cron_debug.log (61 МБ)      cron_automation_emails.log (7,3 МБ)
.msmtp.log (3,8 МБ)             cron_export.log, battery_check.log
optimize_iot.log, cron_heater.log, cron_feeder.log, push_cron.log
mosquitto.log, nginx access/error, syslog, auth.log, kern.log, debug
mqtt_logger.log, iot_anomaly.log (создан заново)
все ротации: *.gz, *.1, *.2, старые *.journal~

Что вычищено в системе

Что вычищено в базе

ТаблицаБылоСтало
automation_log — журнал срабатываний16 285 строк0
email_triggers — очередь писем от cron8730
automation_email_sent — что уже отправлено560
log_stats — счётчики из журналов1 6950
daily_resets950
sensor_history — история графиков100 672не тронуто
rpi_monitor — телеметрия малины96 421не тронуто
Обрати внимание. Историю графиков (sensor_history) и телеметрию я не стирал сознательно: это единственные данные, по которым строятся графики за месяцы, и их восстановить не из чего. Если нужно стереть и их — скажи, это две команды TRUNCATE. Оборванная таблица automations и справочник датчиков не тронуты.

Ротация (чтобы не распухло снова)

/etc/logrotate.d/mqtt-logger   — mqtt_logger.log: 50 МБ, 5 копий, сжатие
/etc/logrotate.d/iot-home      — .msmtp.log и mqtt_logger.log: 20 МБ, 3 копии
/etc/logrotate.d/iot-logs      — все cron-логи: неделя, 4 копии, сжатие
journald: Storage=persistent, SystemMaxUse=200M

5. Библиотека примеров правил

Правила живут в таблице automations и редактируются на странице /admin/logic.php. Движок перечитывает правила раз в 60 секунд, поэтому новое правило начинает работать в течение минуты.

Как читать примеры

ПолеЧто означает
trigger_topicдатчик, за которым следим
condition_operator + condition_valueусловие срабатывания: > < == != >= <=
condition_type = script + condition_scriptсложное условие выражением (см. раздел 6)
schedule_type + schedule_time (+ schedule_days)срабатывание по времени вместо датчика
action_topic + action_payloadчто отправить в MQTT. Многоканальное реле: {"state_l3":"ON"}
cooldown_secondsпауза после срабатывания, чтобы не дёргать устройство
delay_secondsзадержка действия (живёт в памяти движка, до перезапуска)
dependency_topic + dependency_valueразрешение: правило сработает, только если другой датчик в нужном состоянии
persistent = 1«Повторять»: правило не удаляется после срабатывания (ставим всегда)
email_notify + email_recipientотправлять письмо при срабатывании (шлёт cron-скрипт раз в минуту)

Ситуация: свет по движению с выключением через минуту

Правило 1 — включить свет, когда увидели движение

Работает «по фронту»: движок реагирует на смену значения, повторов не будет.
НазваниеДвижение_коридор_ВКЛ
Триггерzigbee2mqtt/motion_sensor/presence
Условие== 1
Действиеzigbee2mqtt/fish_usb_reley/set → {"state_l3":"ON"}
Cooldown60
Повторятьда (persistent=1)

Правило 2 — выключить через минуту тишины

Cooldown 65 секунд даёт эффект «минута после последнего движения»: пока датчик видит движение, правило молчит.
НазваниеДвижение_коридор_ВЫКЛ
Триггерzigbee2mqtt/motion_sensor/presence
Условие== 0
Действиеzigbee2mqtt/fish_usb_reley/set → {"state_l3":"OFF"}
Cooldown65
Если нужна честная задержка без cooldown-трюка, поставь Действие с delay_seconds = 60. Помни: задержка хранится в памяти движка и теряется при его перезапуске.

Ситуация: протечка воды — свет и письмо

НазваниеПротечка_в_ванной_авария
Триггерzigbee2mqtt/leak_bath/water_leak
Условие== 1
Действиеzigbee2mqtt/fish_light/set → {"state":"ON"} (свет во всей зоне)
Письмогалочка «Уведомлять» + адрес, текст правила попадёт в письмо
Cooldown30
Датчик протечки питается от батареи и «просыпается» редко — у него есть смысл дополнительно следить за zigbee2mqtt/leak_bath/battery (порог < 20) отдельным правилом-уведомлением.

Ситуация: полив по сухости почвы и обязательное закрытие крана

Правило 1 — открыть кран при сухой земле

Триггерzigbee2mqtt/gorshok_smallone/soil_moisture < 40
Действиеzigbee2mqtt/PushOK_kran/set → {"state":"ON"}
Cooldown3600 (час — чтобы не открывать кран на каждое измерение)

Правило 2 — закрыть кран, когда земля напилась

Триггерzigbee2mqtt/gorshok_smallone/soil_moisture > 70
Действиеzigbee2mqtt/PushOK_kran/set → {"state":"OFF"}
Cooldown60

Правило 3 — страховка: кран не должен стоять открытым

Триггерzigbee2mqtt/PushOK_kran/state == ON
delay_seconds600 — через 10 минут закрыть
Действиеzigbee2mqtt/PushOK_kran/set → {"state":"OFF"}
Такой набор работает даже если датчик влажности завис: кран всё равно закроется через 10 минут. Это правильнее, чем нынешнее правило без условия.

Ситуация: расписание с проверкой условия

Свет аквариума по расписанию

Типdaily, время 17:00:00
Действиеzigbee2mqtt/fish_power/set → {"state_l2":"ON"}
Условие (необязательно)например zigbee2mqtt/air_box/pm25 < 150 — включать свет только когда воздух в норме
Расписание в движке защищено от повторов: если действие уже не нужно (лампа и так горит), правило закрывается до следующего дня. Не переделывай это поведение — иначе получаешь по строке в журнал каждую секунду.

Ситуация: уведомление без управления

Загазованность / качество воздуха — только предупредить

Триггерzigbee2mqtt/smart_air_sensor/voc > 1000
Действиеоставь пустым — правило ничего не переключает
Письмовключить уведомление и указать адрес
Cooldown3600 — чтобы не писать каждый раз, когда датчик «дышит»
В твоих правилах 158 и 159 сейчас включены без действия и без письма — они молча пишут строки в журнал. Добавь уведомление или выключи их.

Ситуация: правило, зависящее от другого датчика

Поливать можно только когда окно закрыто

Триггерzigbee2mqtt/gorshok_smallone/soil_moisture < 40
Зависимостьzigbee2mqtt/contact_window/contact = false
Действиеzigbee2mqtt/PushOK_kran/set → {"state":"ON"}
Зависимость работает так: если датчик зависимости есть в базе и его значение не совпало — правило пропускается. Если датчика нет в базе вообще, правило сработает (проверка не блокирует).

Ситуация: гистерезис (два порога вместо одного)

Колонка hysteresis в таблице есть, но движок её не использует. Гистерезис делается двумя правилами:

ПравилоУсловиеДействие
Нагреватель ВКЛfish_reley_temp/temperature < 23реле нагревателя ON
Нагреватель ВЫКЛfish_reley_temp/temperature > 25реле нагревателя OFF
Между 23 и 25 ничего не происходит — устройство не «дребезжит». Один порог такого не даёт.

6. Скриптовые условия: правильный синтаксис

Скриптовое условие — это выражение на PHP, которое движок вычисляет и превращает в «истина / ложь». Так можно сравнивать несколько датчиков сразу, что обычным правилом не сделать. Выбирается в редакторе правил: Тип условия → скрипт, выражение пишется в поле «Условие».

Как обращаться к датчикам

sensor["ПОЛНЫЙ_ТОПИК"]      ← правильная форма, работает с любыми топиками

Примеры:
    sensor["zigbee2mqtt/fish_light/voltage"] > 230
    sensor["zigbee2mqtt/air_box/pm25"] > 50 && sensor["zigbee2mqtt/smart_air_sensor/co2"] < 900
    sensor["zigbee2mqtt/fish_power/state_l1"] == "ON"
    sensor["zigbee2mqtt/gorshok_smallone/soil_moisture"] < 40 && sensor["zigbee2mqtt/kran/flow"] == 0
Сокращённая форма sensor.имя тоже работает, но только для имён без слэшей (движок подставляет имя как есть). Для реальных топиков этого дома всегда используй форму с квадратными скобками и кавычками.

Что можно (операторы)

ОператорСмыслПример
> < >= <=сравнение чиселsensor["…/voltage"] < 198
== !=равно / не равно (работает и со строками и с числами)sensor["…/state"] == "ON"
&&и… > 10 && … < 20
||или… == "ON" || … == "OFF"
!не!(sensor["…/state"] == "ON")
( )группировка(… > 1 || … > 2) && … < 9
+ - * /арифметикаsensor["…/current"] * sensor["…/voltage"] / 1000 > 2

Чего делать нельзя (и почему)

НельзяПричина
Вызывать функции: time(), date(), round(), любые другие с круглой скобкой после именидвижок отклоняет выражение с текстом «в скриптовом условии запрещены вызовы функций». Это защита: раньше через условие можно было запустить произвольный код
unlink(), file_put_contents(), fopen(), mail(), exec(), system() и подобныезапрещённые функции, выражение отклонится с названием функции
Проверки «сейчас ночь / сейчас будни»для этого нужна функция времени. Используй расписание (schedule_type) — это правильный инструмент для времени, а не условие
Сравнение с датой/временем датчикапо той же причине — нет функций времени

Правила поведения, о которых легко забыть

Готовые шаблоны под твоё железо

# 1. Напряжение в норме И есть нагрузка
sensor["zigbee2mqtt/fish_light/voltage"] > 207 && sensor["zigbee2mqtt/fish_light/voltage"] < 253
    && sensor["zigbee2mqtt/fish_light/power"] > 5

# 2. Просадка напряжения при заметной нагрузке (признак плохого контакта)
sensor["zigbee2mqtt/fish_light/voltage"] < 210 && sensor["zigbee2mqtt/fish_light/power"] > 50

# 3. Аквариум: свет горит, но потребления нет (перегорела лампа или реле не сработало)
sensor["zigbee2mqtt/fish_light/state"] == "ON" && sensor["zigbee2mqtt/fish_light/power"] < 1

# 4. Воздух: и пыль, и органика одновременно
sensor["zigbee2mqtt/air_box/pm25"] > 50 && sensor["zigbee2mqtt/smart_air_sensor/voc"] > 800

# 5. Земля сухая и кран закрыт — можно поливать
sensor["zigbee2mqtt/gorshok_smallone/soil_moisture"] < 40 && sensor["zigbee2mqtt/PushOK_kran/state"] == "OFF"

# 6. Температура в комнате и влажность вне комфорта
(sensor["zigbee2mqtt/bedroom_sensor/temperature"] < 19 || sensor["zigbee2mqtt/bedroom_sensor/temperature"] > 26)
    || sensor["zigbee2mqtt/bedroom_sensor/humidity"] > 65
Твой текущий пример из базы — sensor.state == "ON" (правила 163 и 164) — датчика с именем state в базе нет, поэтому условие всегда ложно, и эти правила ничего не делают. Замени на явный топик, например sensor["zigbee2mqtt/PushOK_kran/state"] == "ON".

7. Модуль анализа аномалий

Идея

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

Страница

http://192.168.88.237/admin/anomalies.php — ссылка «🔍 Аномалии» в меню панели. Оформление и меню — те же, что у остальных страниц (/css/style.css, общая навигация, тёмная и светлая тема).

Как сервис находит аномалии

ТипКак определяетсяВажность
limit_low — ниже пределазначение ушло ниже жёсткого предела карточкикритично
limit_high — выше пределазначение выше жёсткого предела карточкикритично
dip — просадканиже нормы больше чем на spike_factor устойчивых сигм. Норма — медиана окна, «сигма» считается по MAD (устойчива к выбросам)предупреждение
spike — выброс вверхвыше нормы больше чем на тот же порог. Если значения совсем не меняются (разброс нулевой), порог берётся как 2 % от нормыпредупреждение
no_data — нет данныхзначение не обновлялось дольше, чем gap_intervals × интервал съёма. Ловит зависший датчикпредупреждение
Однотипная аномалия не повторяется чаще, чем cooldown_events секунд — журнал не забивается, а письма не превращаются в спам.

Что уже работает

Две карточки по электросети, данные уже собираются:

КарточкаТопикПределыПочта для писем
Электросеть: напряжение, Вzigbee2mqtt/fish_light/voltage198…253 В (ГОСТ 32144-2013: 230 В −10 %…+10 %)it@example.com, с важности «предупреждение»
Электросеть: активная мощность, Втzigbee2mqtt/fish_light/power0…600 Втit@example.com, с важности «только критичное»

Розетка аквариума отдаёт ещё и current (ток) и energy (накопитель) — при желании добавишь карточки на них так же. Для «настоящего» контроля сети полезно добавить наблюдение за zigbee2mqtt/fish_light/voltage с меньшим окном нормы (например 6 часов): тогда просадки при включении мощной нагрузки будут видны отчётливее.

Как добавить новую аномалию (карточку)

  1. Открой /admin/anomalies.php и заполни форму «Добавить карточку наблюдения»: название, топик, единица измерения.
  2. Укажи интервал съёма (по умолчанию 60 с) и окно нормы (24 ч). Для быстрых процессов окно уменьшают.
  3. Пределы можно не задавать — тогда работают только статистические проверки (просадка, выброс, пропажа данных). Если знаешь безопасный диапазон — задай, это самая надёжная проверка.
  4. Если хочешь письмо при аномалии — впиши адрес и выбери минимальную важность.
  5. Напиши промпт для нейросети: что считать опасным, на что смотреть, в каком виде нужен ответ. Промпт можно менять в любой момент.

Как подключается нейросеть

Сейчас разбор отключён: ключа нет. Провайдер, адрес, модель и ключ задаются в одном месте — файл /etc/iot/credentials.ini, секция [ai]:

[ai]
enabled = 1
provider = openai            ; либо openai-compatible / mistral / ollama
base_url = https://api.openai.com/v1
model = gpt-4o-mini
api_key = СЮДА_КЛЮЧ

После этого перезапуск сервиса и запросы работают по-настоящему:

sudo systemctl restart iot-anomaly
# дальше — кнопка «Проанализировать нейросетью» на карточке
До подключения кнопка тоже полезна: сервис считает статистику (минимум, максимум, среднее, медиану, сигму, разбивку по суткам, список аномалий), сохраняет её в отчёт и подсказывает, что дописать в конфиг. Эти данные попадут в запрос, когда ключ появится.
Для локальной модели на другой машине подойдёт provider = ollama с адресом вида http://10.0.0.5:11434/v1 — данные не уйдут в интернет.

Таблицы модуля

ТаблицаЧто хранит
anomaly_watchersкарточки: топик, интервалы, пределы, чувствительность, промпт, адрес для писем, состояние
anomaly_samplesсобранные точки (топик → значение + время)
anomaly_eventsнайденные аномалии: тип, важность, значение, норма, отклонение, пояснение
anomaly_requestsочередь запросов «проанализировать» — страница кладёт, сервис выполняет
anomaly_reportsотчёты: отправленная статистика и ответ нейросети

Обслуживание

systemctl status iot-anomaly           # состояние сервиса
journalctl -u iot-anomaly -n 30        # что делает
tail -f /home/mazzick/iot_anomaly.log  # лог сервиса

# сколько точек собрано по карточкам
mysql iot_db -e "SELECT w.id,w.name,w.samples_count,COUNT(s.id) pts,
  ROUND(AVG(s.value),2) avg,MIN(s.value) mn,MAX(s.value) mx
  FROM anomaly_watchers w LEFT JOIN anomaly_samples s ON s.watcher_id=w.id GROUP BY w.id;"

# как часто срабатывали аномалии
mysql iot_db -e "SELECT kind,COUNT(*) n FROM anomaly_events GROUP BY kind;"

8. Что не работает и почему (честно)

Не работает / ограничениеПричина и что делать
Правила 163 и 164 («Защита от заливания» и «Анти-залипание крана»)Условие sensor.state == "ON" ссылается на несуществующий датчик state, поэтому всегда ложно — правила не срабатывают. Сейчас они включены и молчат. Замени условие на явный топик (раздел 6, шаблон для крана) или выключи правило.
Правила 158 и 159 (PM2.5 и ЛОС)Включены, но без действия и без письма — только пишут строки в журнал. Либо добавь уведомление, либо выключи.
Правило 169 «Земляника подсветка вкл 8:00»Время в базе — 09:00, а не 8:00. Название расходится с настройкой; чтобы не путаться, поправь название или время.
Колонки hysteresis, max_retries, retry_delay, time_restriction_start/end, in_progress, push_*Движок их не читает вообще. Гистерезис — двумя правилами, ограничение по времени — расписанием, повторы — не реализованы.
Задержка и подтверждения теряются при перезапуске движкаdelay_seconds и ожидание подтверждения живут в памяти процесса. Перезапуск iot-automation отменяет отложенное действие. Для критичного — расписание или страховочное правило.
Реакция движка до 1 секундыЦикл опрашивает базу раз в секунду. Для света и полива нормально; для быстрых сценариев (умный выключатель по щелчку) будет ощутимо.
Часть автоматики живёт в cron, а не в правилахcron_fish_heater.php (каждые 20 мин), cron_fish_feeder.php (10:00), cron_push.php и cron_automation_emails.php (каждую минуту). При разборе сбоя сначала смотри и сюда, а не только в движок.
Данные от розетки приходят только когда она отчитываетсяОтдельные параметры (power, voltage) обновляются с паузой, а снимок целиком — примерно раз в минуту. Интервал съёма 60 с соответствует реальности, ставить меньше смысла нет.
История графиков осталась с прежних времёнОсознанно: sensor_history и rpi_monitor не обнулялись. Если нужно «всё с нуля» — скажи.
Документ про архитектуру (первый) описывает состояние до этих правокВ нём появились неточности: он упоминает пароли в коде и открытый доступ. Актуальная картина — в этом документе.

9. Автоконтроль: сторож в cron

Чтобы «стабильно» перестало быть вопросом веры, поставлен ежечасный сторож: он сам прогоняет проверки и пишет письмо, только если что-то сломалось.

Как устроено

Файл / заданиеЧто делает
/home/mazzick/healthcheck.sh9 разделов проверок: все семь служб (включая число перезапусков), свежесть данных, сбор точек аномалий и ложные срабатывания, ошибки в журналах, дубли и следы триггера, секреты в коде, рост логов и наличие logrotate, синтаксис кода и закрытость файлов, сторож и диск. Печатает [OK]/[FAIL], код возврата = число проблем
/home/mazzick/iot_watchdog.shзапускает проверку; если проблем нет — одна строка в лог и тишина; если есть — письмо владельцу с полным отчётом
cron: 5 * * * *запуск сторожа каждый час
/home/mazzick/iot_watchdog.logлог сторожа: «ок: проблем нет» либо «ПРОБЛЕМЫ (N): письмо отправлено»
/home/mazzick/.iot_watchdog/last_problem.txtпамять сторожа: сигнатура проблем и время отправки

Защита от спама письмами

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

Проверено на живом стенде

Что проверялРезультат
Тестовое письмо (проверка канала)IOT_WATCHDOG_TEST=1 → в логе «ТЕСТ: письмо отправлено», msmtp: 250 OK
Имитация поломки: остановил сервис аномалийсторож нашёл 2 проблемы, письмо ушло, состояние записано
Повторный прогон с той же поломкойв логе «те же, что и раньше — письмо не дублируем», второго письма нет
Сервис поднят обратносторож вернул «проблем нет», состояние сброшено
Итог полного прогонаЗДОРОВ, ошибок 0

Что теперь считается «нормой» и ловится сразу

Теперь «суперстабильность» проверяется не на словах: неделя тишины в письмах означает неделю без проблем, а при первом же сбое приходит письмо с точным списком FAIL и полным отчётом. Ручной прогон в любой момент — bash /home/mazzick/healthcheck.sh.

10. Как откатить

Все правки этого круга обратимы. Бэкапы: /home/mazzick/przorka_backup/round2/ (конфиги) и /home/mazzick/przorka_backup/round2/files/ (копии каждого изменённого файла с суффиксом .orig*).

Что менялосьКопияКак вернуть
Файлы движка и страницround2/files/*.orig*sudo cp файл.orig* на_место, для PHP — php -l, затем systemctl restart iot-automation
Юниты systemdround2/*.service.orig*вернуть в /etc/systemd/system/, systemctl daemon-reload, перезапуск
nginxround2/nginx-iot.before_aclвернуть в /etc/nginx/sites-available/iot, nginx -t && systemctl reload nginx (вернёт открытый доступ всей сети)
mosquittoround2/mqtt.conf.orig, round2/mosquitto.passwd.origвернуть в /etc/mosquitto/ и перезапустить (вернёт старые пароли MQTT)
Файрвол—sudo ufw --force reset && sudo ufw allow 22/tcp && sudo ufw enable
Секретысами файлы новых секретовстарые пароли возвращать не советую: они были в открытом виде в веб-каталоге. Пароль панели не менялся
crontabround2/crontab-mazzick.txt, crontab-root.txtcrontab файл / sudo crontab -u root файл
Модуль аномалий—sudo systemctl disable --now iot-anomaly, при желании mysql iot_db -e "DROP TABLE anomaly_watchers, anomaly_samples, anomaly_events, anomaly_requests, anomaly_reports" и удалить admin/anomalies.php
Очищенные логи и журналы—не восстановить напрямую: только из архива базы /home/mazzick/backup_smart_home_20260926_143241.tar.gz (внутри полный дамп БД)

11. Чек-лист проверки

# службы (все семь должны быть active)
systemctl is-active iot-automation mqtt-to-mysql iot-anomaly mosquitto zigbee2mqtt mariadb nginx

# данные идут
mysql iot_db -e "SELECT COUNT(*) свежих_за_5мин FROM sensor_data WHERE timestamp > NOW() - INTERVAL 5 MINUTE;"

# ошибок нет
journalctl -u iot-automation -u mqtt-to-mysql -u iot-anomaly --since "30 min ago" | grep -icE "error|fatal|traceback|вызовы функций"

# секретов в коде нет
grep -rlI "********\|Abudfv********" /var/www/html/iot /home/mazzick --include=*.php --include=*.py --include=*.sh \
  | grep -v cleanup | grep -v przorka_backup || echo "чисто"

# доступ закрыт, панель жива
curl -s -o /dev/null -w "панель: %{http_code}\n" http://127.0.0.1/admin/sensors.php      # 200
curl -s -o /dev/null -w "код .py: %{http_code}\n" http://127.0.0.1/admin/mqtt_to_mysql.py # 403

# сторож и полная проверка
bash /home/mazzick/healthcheck.sh            # ожидаем: ЗДОРОВ, ошибок 0
IOT_WATCHDOG_TEST=1 /home/mazzick/iot_watchdog.sh   # проверка канала писем

# аномалии собираются
mysql iot_db -e "SELECT w.name,w.samples_count,w.status FROM anomaly_watchers w;"

# логи не растут
ls -la /home/mazzick/mqtt_logger.log /home/mazzick/iot_anomaly.log

Расширенный прогон всех проверок — скрипт healthcheck.sh из скилла smart-home-pi:

plink -batch -pw ПАРОЛЬ mazzick@192.168.88.237 'MYP=ПАРОЛЬ_БД bash /home/mazzick/healthcheck.sh'