Спойлер: пока сам не знаю что эта за хрень, просто придумал идею в трех предложениях и скормил гермес-агенту после прожарки проекта. Нейросеть мне склепала вот такой сервис, который я сам пока не понял как работает, но чет крутое, оставил заметку, сижу разбираюсь может еще идеи какие будут )
Раздел — это два независимых куска, которые общаются через базу данных:
| Кусок | Что делает | Файл |
|---|---|---|
| Сервис сбора и анализа systemd, работает всегда |
Каждые 15 секунд: снимает точки по карточкам, считает норму, находит аномалии, шлёт письма, выполняет запросы «проанализируй нейросетью» | /home/iot_user/iot_anomaly.pyюнит iot-anomaly.service |
| Страница панели открывается после входа |
Карточки наблюдения: создать, настроить пороги, включить сбор, посмотреть события и отчёты, поставить запрос в нейросеть | /var/www/html/iot/public_html/admin/anomalies.php |
Zigbee-датчик
│ (радио)
▼
zigbee2mqtt ──────────► MQTT-топик zigbee2mqtt/<устройство>/<поле>
│ например zigbee2mqtt/fish_light/voltage = 218
▼
Mosquitto (127.0.0.1:1883, логин/пароль)
│
▼
mqtt_to_mysql.py ── последнее значение ──► sensor_data (одна строка на топик)
│ ── история (если нужно) ─► sensor_history (много строк по времени)
│
▼
iot_anomaly.py (цикл 15 с)
│
├─(1) съём точек ─────────────► anomaly_samples (число + время)
│
├─(2) расчёт нормы и поиск ───► anomaly_events (тип, важность, отклонение)
│ │
│ └── важность ≥ порога карточки ──► письмо через msmtp
│
└─(3) запрос ИИ из очереди ───► anomaly_requests (страница кладёт, сервис забирает)
│
└── статистика + промпт ──► нейросеть ──► anomaly_reports (готовый отчёт)
collect_samples, analyse, process_requests.sensor_data лежит только последнее значение по топику — это удобно для правил («кран сейчас ON»), но тренд по нему не построишь. История пишется отдельно, в sensor_history, и только для топиков, у которых в справочнике sensor_info стоит save_history = 1.
Поэтому модуль аномалий ведёт собственное хранилище точек (anomaly_samples) с нужным ему интервалом. Так он не зависит ни от чужого интервала записи, ни от того, включили историю для топика или нет.
Полный список — от источника данных до витрины. Всё, что вне веб-корня, руками по HTTP не открывается (и это правильно: .py и .sh в public_html nginx отдаёт как 403).
| Файл / объект | Роль в разделе | Кто запускает |
|---|---|---|
/home/iot_user/iot_anomaly.py | Сердце раздела. Сбор точек, расчёт нормы, поиск аномалий, письма, запросы в нейросеть. 438 строк | systemd, юнит iot-anomaly.service, от пользователя iot_user |
/etc/systemd/system/iot-anomaly.service | Описание сервиса: Restart=always, запуск после mosquitto и mariadb, логи в journald | systemd |
/etc/iot/credentials.ini | Секции [db], [mqtt], [smtp], [ai]. Права 640 root:www-data. Ключ нейросети живёт здесь, в коде его нет | читает python-сервис при старте |
/etc/iot/credentials.env | Те же доступы для shell и python через переменные окружения; подключается юнитом как EnvironmentFile | systemd, скрипты |
/var/www/html/iot/public_html/admin/anomalies.php | Страница раздела: карточки, пороги, кнопка «Проанализировать», список событий и отчётов, подтверждение аномалий. 516 строк | PHP-FPM по HTTP, после входа в панель |
/var/www/html/iot/public_html/include/links_panel.php | Меню панели: пункт «🔍 Аномалии» и ссылки на статьи | включается всеми страницами |
/var/www/html/iot/config/iot_config.php | Общий конфиг PHP: функция iot_pdo() и константы доступа. Лежит вне корня сайта | все PHP-файлы |
/home/iot_user/iot_anomaly.log | Рабочий лог сервиса: цикл, найденные аномалии, отправленные письма, ошибки | пишет сам сервис |
/home/iot_user/healthcheck.sh | Общая проверка дома. Раздел 3 — модуль аномалий: собираются ли точки, пуста ли очередь запросов, нет ли ложных выбросов | cron и вручную |
таблицы anomaly_* в базе iot_db | watchers — карточки, samples — точки, events — найденные аномалии, requests — очередь запросов, reports — ответы нейросети | — |
/home/iot_user/mqtt-env/bin/python | Окружение python, из которого запускается сервис (там pymysql) | юнит |
| Объект | Зачем нужен разделу |
|---|---|
admin/mqtt_to_mysql.py + mosquitto + zigbee2mqtt | Наполняют sensor_data. Нет свежего значения — сервису нечего снимать |
msmtp (/usr/bin/msmtp, настроен как sendmail) | Отправка письма об аномалии. Адрес и минимальная важность берутся из карточки |
| нейросеть по OpenAI-совместимому API (облако или локальная ollama) | Только для кнопки «Проанализировать». Автопоиск аномалий работает без неё |
Вся логика раздела читается по схеме данных. Если понимать эти пять таблиц — понимать модуль целиком.
| Таблица | Ключевые поля | Смысл |
|---|---|---|
anomaly_watchers |
name, topic, unit, enabled, collect_interval, window_hours, min_samples, limit_min/max, spike_factor, cooldown_events, gap_intervals, prompt, notify_email, notify_min_severity, status, samples_count |
Карточка наблюдения. Один топик + все настройки, по которым он анализируется. Всё, что можно менять не трогая код — здесь |
anomaly_samples |
watcher_id, value, taken_at |
Собственные точки: только число и время. На них и считается норма |
anomaly_events |
kind, severity, value, baseline, deviation, note, detected_at, acknowledged |
Найденные аномалии. baseline — норма в момент находки, deviation — насколько ушли от неё |
anomaly_requests |
watcher_id, prompt, status, error, started_at, finished_at |
Очередь на нейросеть. Страница не ходит в интернет сама — она кладёт строку, сервис её забирает |
anomaly_reports |
provider, model, period_from/to, samples_used, events_used, summary, response, status |
Готовый отчёт: что отправили (статистика) и что ответила нейросеть. Хранится вместе с промптом — потом видно, почему модель сказала именно это |
anomaly_requests; через 15 секунд сервис забирает строку, считает статистику, идёт в модель и кладёт ответ в anomaly_reports. Страница к этому времени давно отдана браузеру — отчёт просто появится при обновлении.
Сервис ищет ровно пять видов событий. Это не «модный ИИ», а обычная статистика — и именно поэтому она работает на данных слабых и медленных Zigbee-датчиков.
Тип (kind) | Как ловится | Важность | Пример из жизни дома |
|---|---|---|---|
limit_low | значение ниже limit_min из карточки | critical | напряжение сети ниже 198 В — свет мигает, аквариумный нагреватель не тянет |
limit_high | значение выше limit_max | critical | мощность розетки выше 600 Вт — что-то включили мимо плана |
spike | выше нормы на spike_factor сигм | warning | скачок тока: плохой контакт, износ проводки, старт компрессора |
dip | ниже нормы на spike_factor сигм | warning | просадка напряжения в момент включения нагрузки |
no_data | точки не поступают дольше collect_interval × gap_intervals | warning | датчик отвалился от сети, села батарейка, завис zigbee2mqtt |
Среднее и обычная сигма считаются по всем точкам сразу, поэтому одна выбросная точка раздувает сигму и делает порог шире. То есть датчик сам себя «прикрывает»: чем хуже он глючит, тем меньше аномалий находится.
обычная сигма: σ = √( Σ(xᵢ − среднее)² / n ) ← одна выбросная точка раздувает σ устойчивая: медиана m, MAD = медиана(|xᵢ − m|), σ = 1.4826 × MADМножитель 1.4826 подобран так, чтобы для нормального распределения «устойчивая сигма» совпадала с обычной. Логика в коде:
med, sigma = median_abs_dev(values) # медиана и 1.4826 × MAD по окну window_hours
if sigma > 0:
threshold = spike_factor * sigma # по умолчанию 6 сигм
sigma = 0, и порог 6 × 0 = 0 превратил бы в аномалию любой шаг округления: 19 Вт → 20 Вт.
Лечится это честно: когда разброса нет, порог берётся от шага квантования самого датчика, и требуется отклонение не меньше трёх шагов:
uniq = sorted(set(values)); steps = [b - a for a, b in zip(uniq, uniq[1:]) if b - a > 0] step = min(steps) if steps else max(abs(med) * 0.02, 0.02) threshold = max(3 * step, abs(med) * 0.02, 0.02)В журнал пишется честная причина порога:
норма без разброса, шаг датчика 1, порог 3. Без этого пришлось бы гадать, почему 20 Вт — «выброс».
Пока в окне меньше min_samples точек, анализ по карточке просто не выполняется. Норма, посчитанная по пяти значениям, — это не норма, а случайность. По умолчанию нужно 20 точек, и статус карточки меняется с collecting на ready, только когда порог перейдён.
Сейчас в системе три карточки — все по электросети, снятые с умной розетки аквариума. Ниже реальные значения из базы, а не пример «от себя».
zigbee2mqtt/fish_light/current · 📧 it@logoakademia.ruЧто настраивается прямо в карточке, без правки кода:
min_samples.spike_factor, по умолчанию 6.cooldown_events. Это защита от спама: датчик, который «залип» в аномалии, больше не пишет о ней каждые 15 секунд.gap_intervals, по умолчанию 3.Ты инженер-электрик. Ниже статистика по силе тока домашней розетки (аквариум) и найденные автоаномалии. Оцени: стабильность тока, всплески при включении нагрузки, соответствие мощности и напряжения (P = U*I), признаки плохого контакта или износа проводки. Учти, что свет и обогреватель включаются по расписанию, поэтому ступеньки тока ожидаемы. Ответ: 1) вывод по состоянию цепи; 2) что может давать скачки; 3) что проверить физически; 4) какие данные добавить. По-русски, кратко.Из этого видно главный приём: в промпте заранее объяснено, какие ступеньки нормальны. Без этого модель бодро объявит аномалией штатное включение обогревателя по расписанию.
Модель не видит базу и не видит графики. Ей уходит текст, который сервис собирает сам — и это принципиально: если дать модели сырые точки, она утонет в них и начнёт сочинять.
| Блок отчёта | Что содержит |
|---|---|
| Паспорт | имя карточки, топик, единица измерения, период, сколько точек и с каким интервалом |
| Статистика | минимум, максимум, среднее, медиана, устойчивая сигма, размах, жёсткие пределы карточки |
| Разбивка по суткам | мин / среднее / макс и число точек за каждый день окна — по ней видно сезонность и сдвиг режима |
| Автонаходки | список событий из anomaly_events: время, тип, важность, значение, норма, пояснение — до 120 записей |
| Инструкция | системный промпт карточки: роль модели и требуемый формат ответа |
Ответ вместе со всем отправленным текстом сохраняется в anomaly_reports. Через месяц видно, на каких данных модель сделала вывод, — это важно, когда разбираешь ложную тревогу.
/etc/iot/credentials.ini в секции [ai] стоит enabled = 0 и нет ключа, поэтому кнопка «Проанализировать» ставит запрос в очередь, а сервис кладёт в отчёт реальный ответ:
Анализ нейросетью пока не подключён. Данные собраны и статистика посчитана (см. ниже). Чтобы включить разбор ИИ: 1) в /etc/iot/credentials.ini в секции [ai] указать enabled = 1, provider (openai/mistral/локальный), base_url, model и api_key; 2) нажать «Проанализировать» на карточке заново.Так сделано намеренно: система не должна падать и молча терять запросы, если внешний сервис не настроен. Пользователь получает понятный текст с инструкцией, а не «ошибка 500».
В базе три карточки, у всех enabled = 0, и сбора точек нет: последняя точка датирована 26 сентября 17:54. Это не поломка — карточки выключены сознательно вместе с отсутствием ключа нейросети, и вот тут начинается отдельная история про уведомления.
Первая версия общей проверки дома считала выключенный модуль двумя ошибками и каждый час отправляла письмо «ПРОБЛЕМЫ (2)». Одинаковое письмо через неделю уезжает в спам вместе с настоящими тревогами — то есть сторожевая функция тихо превращается в бесполезную. Поэтому проверка научена различать ситуации:
карточек активно 0 И [ai] enabled != 1 → [SKIP] «выключен сознательно», счётчик ошибок не растёт карточки есть, а точки не собираются → [FAIL] настоящая поломка, письмо уходитПлюс отдельно проверяется очередь:
anomaly_requests со статусом pending не должна накапливаться — если накапливается, значит сервис встал. И есть защита от «ложных выбросов»: события за сутки вида spike с отклонением меньше трёх шагов считаются подозрительными — это симптом слишком узкого порога.
# 1) ключ нейросети: секция [ai] в /etc/iot/credentials.ini
# enabled = 1, provider = openai | mistral | ollama, base_url, model, api_key
sudo -n nano /etc/iot/credentials.ini
sudo -n systemctl restart iot-anomaly
# 2) включить карточку наблюдения (в панели — кнопкой, из консоли — так)
mysql iot_db -e "UPDATE anomaly_watchers SET enabled = 1 WHERE id = 1;"
# 3) проверить, что точки пошли (ждать интервал съёма, обычно минуту)
mysql iot_db -e "SELECT w.name, COUNT(s.id) точек, MAX(s.taken_at) последняя
FROM anomaly_watchers w LEFT JOIN anomaly_samples s ON s.watcher_id = w.id
WHERE w.enabled = 1 GROUP BY w.id;"
# 4) смотреть работу сервиса вживую
tail -f /home/iot_user/iot_anomaly.log
# 5) общая проверка дома (раздел 3 — про аномалии)
/home/iot_user/healthcheck.sh
| Симптом | Причина | Как закрыто |
|---|---|---|
| Сторож каждый час шлёт «ПРОБЛЕМЫ (2)», письмо уходит в спам | Выключенный модуль аномалий считался поломкой | Проверка различает «выключено сознательно» ([SKIP], ошибок 0) и «включено, но не работает» ([FAIL]) |
| Аномалия на ровном месте: 20 Вт объявлены выбросом при норме 19 Вт | У медленного датчика значения не меняются → sigma = 0 → порог = 6 × 0 = 0 |
При нулевом разбросе порог берётся от шага квантования датчика и требует минимум три шага |
| Письма об одной и той же аномалии каждые 15 секунд | Цикл сервиса короткий, состояние не меняется | cooldown_events на карточке (по умолчанию 900 с) — однотипное событие гасится на это время |
| Карточка создана, а точек нет | Значение топика — не число: сервис пытается привести его к float и отбрасывает |
Для карточек берутся числовые топики (…/voltage, …/power, …/current, …/soil_moisture). JSON-объект целиком (например zigbee2mqtt/PushOK_kran) в карточку не годится — у него нет одного числа |
| Аномалии находятся на пустом месте сразу после создания карточки | Норма посчитана по трём точкам | min_samples: пока точек меньше порога, анализ не выполняется; статус карточки остаётся collecting |
| Страница «Проанализировать» висит и отдаёт 504 | Попытка ждать ответ модели синхронно в PHP-запросе | Запрос уходит в таблицу-очередь, отвечает сервис; страница отдаётся браузеру сразу |
| Секретов в коде нет, но модулю нужны доступы | Хардкод паролей в исходниках — так было в первой версии | Всё в /etc/iot/credentials.ini и .env с правами 640; проверка дома отдельно ищет старые пароли в коде и должна находить пусто |
Правила автоматизации — это то, что мы предусмотрели. Аномалии — это то, чего мы не предусмотрели, но что уже произошло и оставило след в данных: подросший ток на розетке, севшая батарейка датчика, датчик, который молчит вторые сутки, просадка напряжения при включении нагрузки.
Раздел делает три вещи, и все три без нейросети: собирает точки по выбранным топикам со своим интервалом, считает норму устойчивыми методами (медиана и MAD) и сообщает письмом о том, что вышло за норму — с паузой, чтобы не превратиться в спам. Нейросеть здесь надстройка: она превращает таблицу чисел в осмысленный вывод, но находит аномалии статистика.
Практический вывод за две недели эксплуатации: самое ценное в модуле — не поиск выбросов, а контроль молчания. Датчик, который перестал выходить на связь, не даёт ни тревоги, ни ошибки — просто тишина в журнале. Событие no_data — единственное, что эту тишину делает видимой.