Хост 192.168.88.237 (Raspberry Pi 5, Debian 12) · всё ниже сделано и проверено на живом стенде,
кроме пунктов, которые прямо помечены как «не проверено». Первый документ с описанием архитектуры — /smarthome_architecture.html.
Пароли открытым текстом в ~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 пуст.
те же секреты для 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. Файл не отражает реальность, стоит привести в порядок, но на правила доступа это не влияет.
Журнал systemd: сжат и переведён на хранение в /var/log/journal с лимитом 200 МБ (было 75 МБ в volatile-режиме и терялось при перезагрузке).
История команд shell и mysql (~/.bash_history, ~/.mysql_history) — там встречались пароли в открытом виде.
~/.xsession-errors и его старая копия.
Что вычищено в базе
Таблица
Было
Стало
automation_log — журнал срабатываний
16 285 строк
0
email_triggers — очередь писем от cron
873
0
automation_email_sent — что уже отправлено
56
0
log_stats — счётчики из журналов
1 695
0
daily_resets
95
0
sensor_history — история графиков
100 672
не тронуто
rpi_monitor — телеметрия малины
96 421
не тронуто
Обрати внимание. Историю графиков (sensor_history) и телеметрию я не стирал сознательно: это единственные данные, по которым строятся графики за месяцы, и их восстановить не из чего. Если нужно стереть и их — скажи, это две команды TRUNCATE. Оборванная таблица automations и справочник датчиков не тронуты.
Правила живут в таблице 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 — включить свет, когда увидели движение
Работает «по фронту»: движок реагирует на смену значения, повторов не будет.
Если нужна честная задержка без cooldown-трюка, поставь Действие с delay_seconds = 60. Помни: задержка хранится в памяти движка и теряется при его перезапуске.
Ситуация: протечка воды — свет и письмо
Название
Протечка_в_ванной_авария
Триггер
zigbee2mqtt/leak_bath/water_leak
Условие
==1
Действие
zigbee2mqtt/fish_light/set → {"state":"ON"} (свет во всей зоне)
Письмо
галочка «Уведомлять» + адрес, текст правила попадёт в письмо
Cooldown
30
Датчик протечки питается от батареи и «просыпается» редко — у него есть смысл дополнительно следить за zigbee2mqtt/leak_bath/battery (порог < 20) отдельным правилом-уведомлением.
Ситуация: полив по сухости почвы и обязательное закрытие крана
Правило 1 — открыть кран при сухой земле
Триггер
zigbee2mqtt/gorshok_smallone/soil_moisture<40
Действие
zigbee2mqtt/PushOK_kran/set → {"state":"ON"}
Cooldown
3600 (час — чтобы не открывать кран на каждое измерение)
Правило 2 — закрыть кран, когда земля напилась
Триггер
zigbee2mqtt/gorshok_smallone/soil_moisture>70
Действие
zigbee2mqtt/PushOK_kran/set → {"state":"OFF"}
Cooldown
60
Правило 3 — страховка: кран не должен стоять открытым
Триггер
zigbee2mqtt/PushOK_kran/state==ON
delay_seconds
600 — через 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
Действие
оставь пустым — правило ничего не переключает
Письмо
включить уведомление и указать адрес
Cooldown
3600 — чтобы не писать каждый раз, когда датчик «дышит»
В твоих правилах 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["…/voltage"] < 198
== !=
равно / не равно (работает и со строками и с числами)
Вызывать функции: time(), date(), round(), любые другие с круглой скобкой после имени
движок отклоняет выражение с текстом «в скриптовом условии запрещены вызовы функций». Это защита: раньше через условие можно было запустить произвольный код
unlink(), file_put_contents(), fopen(), mail(), exec(), system() и подобные
запрещённые функции, выражение отклонится с названием функции
Проверки «сейчас ночь / сейчас будни»
для этого нужна функция времени. Используй расписание (schedule_type) — это правильный инструмент для времени, а не условие
Сравнение с датой/временем датчика
по той же причине — нет функций времени
Правила поведения, о которых легко забыть
Условие проверяется и для триггерных правил (сейчас это исправлено). При этом правило всё равно «привязано» к своему trigger_topic: движок пересчитывает условие, когда значения меняются, а сработает по факту смены состояния.
Строки и числа. Значения из MQTT часто приходят строкой. Сравнение == "ON" работает; для чисел надёжнее сравнивать числовые операторы > <, они приводят значения к числу.
Нет датчика — условие ложно. Если топик ещё не появился в базе (например датчик не выходил на связь), обращение к нему даст пустое значение и выражение не сработает. Это нормально: правило оживёт, когда данные появятся.
Ошибка не «валит» движок. Кривое выражение просто не срабатывает, а в журнал пишется одна строка в минуту с причиной. Посмотреть: journalctl -u iot-automation -n 20.
Готовые шаблоны под твоё железо
# 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/voltage
198…253 В (ГОСТ 32144-2013: 230 В −10 %…+10 %)
it@example.com, с важности «предупреждение»
Электросеть: активная мощность, Вт
zigbee2mqtt/fish_light/power
0…600 Вт
it@example.com, с важности «только критичное»
Розетка аквариума отдаёт ещё и current (ток) и energy (накопитель) — при желании добавишь карточки на них так же.
Для «настоящего» контроля сети полезно добавить наблюдение за zigbee2mqtt/fish_light/voltage с меньшим окном нормы (например 6 часов):
тогда просадки при включении мощной нагрузки будут видны отчётливее.
Как добавить новую аномалию (карточку)
Открой /admin/anomalies.php и заполни форму «Добавить карточку наблюдения»: название, топик, единица измерения.
Укажи интервал съёма (по умолчанию 60 с) и окно нормы (24 ч). Для быстрых процессов окно уменьшают.
Пределы можно не задавать — тогда работают только статистические проверки (просадка, выброс, пропажа данных). Если знаешь безопасный диапазон — задай, это самая надёжная проверка.
Если хочешь письмо при аномалии — впиши адрес и выбери минимальную важность.
Напиши промпт для нейросети: что считать опасным, на что смотреть, в каком виде нужен ответ. Промпт можно менять в любой момент.
Как подключается нейросеть
Сейчас разбор отключён: ключа нет. Провайдер, адрес, модель и ключ задаются в одном месте — файл /etc/iot/credentials.ini, секция [ai]:
После этого перезапуск сервиса и запросы работают по-настоящему:
sudo systemctl restart iot-anomaly
# дальше — кнопка «Проанализировать нейросетью» на карточке
До подключения кнопка тоже полезна: сервис считает статистику (минимум, максимум, среднее, медиану, сигму, разбивку по суткам, список аномалий),
сохраняет её в отчёт и подсказывает, что дописать в конфиг. Эти данные попадут в запрос, когда ключ появится.
Для локальной модели на другой машине подойдёт provider = ollama с адресом вида http://10.0.0.5:11434/v1 —
данные не уйдут в интернет.
Таблицы модуля
Таблица
Что хранит
anomaly_watchers
карточки: топик, интервалы, пределы, чувствительность, промпт, адрес для писем, состояние
очередь запросов «проанализировать» — страница кладёт, сервис выполняет
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. Название расходится с настройкой; чтобы не путаться, поправь название или время.
Движок их не читает вообще. Гистерезис — двумя правилами, ограничение по времени — расписанием, повторы — не реализованы.
Задержка и подтверждения теряются при перезапуске движка
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.sh
9 разделов проверок: все семь служб (включая число перезапусков), свежесть данных, сбор точек аномалий и ложные срабатывания, ошибки в журналах, дубли и следы триггера, секреты в коде, рост логов и наличие 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
Что теперь считается «нормой» и ловится сразу
служба упала и поднялась — NRestarts больше нуля: видно, даже если сейчас всё работает;
пропали свежие данные — датчик или мост молчит;
в журнале появились Fatal error или сбой компиляции регулярки;
ложные выбросы аномалий (событие при отклонении меньше трёх шагов датчика) — признак слишком узкого порога;
вернулся триггер БД или появились строки журнала без rule_id;
логгер начал писать каждое сообщение (файл растёт);
секрет просочился в код или слетели права на конфиг;
ранее закрытые файлы (`.py`, `.bak`, выгрузки cron) снова открылись по HTTP.
Теперь «суперстабильность» проверяется не на словах: неделя тишины в письмах означает неделю без проблем,
а при первом же сбое приходит письмо с точным списком 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
Юниты systemd
round2/*.service.orig*
вернуть в /etc/systemd/system/, systemctl daemon-reload, перезапуск
nginx
round2/nginx-iot.before_acl
вернуть в /etc/nginx/sites-available/iot, nginx -t && systemctl reload nginx (вернёт открытый доступ всей сети)