↩️ Назад

Категории

Раздел «Аномалии» в самописном умном доме: как это работает и какие файлы участвуют

28.09.2026 | из категории: IOT умный дом

Поиск аномалий с помощью нейросети - новый раздел для умного

Спойлер: пока сам не знаю что эта за хрень, просто придумал идею в трех предложениях и скормил гермес-агенту после прожарки проекта. Нейросеть мне склепала вот такой сервис, который я сам пока не понял как работает, но чет крутое, оставил заметку, сижу разбираюсь может еще идеи какие будут )

Умный дом на Raspberry Pi 5 · zigbee2mqtt → Mosquitto → MariaDB → PHP-панель · 28 сентября 2026
Правила автоматизации описывают то, что мы заранее придумали: «кран открыт дольше 100 секунд — закрыть», «влажность почвы ниже 40 % — полить». Раздел «Аномалии» решает обратную задачу: найти в исторических данных то, чего мы не придумали. Норма считается по самим данным (медиана и устойчивая сигма за скользящее окно), а отклонения от нормы и пропажа данных становятся событиями с письмом на почту.

1. Из чего состоит раздел

Раздел — это два независимых куска, которые общаются через базу данных:

КусокЧто делаетФайл
Сервис сбора и анализа
systemd, работает всегда
Каждые 15 секунд: снимает точки по карточкам, считает норму, находит аномалии, шлёт письма, выполняет запросы «проанализируй нейросетью» /home/iot_user/iot_anomaly.py
юнит iot-anomaly.service
Страница панели
открывается после входа
Карточки наблюдения: создать, настроить пороги, включить сбор, посмотреть события и отчёты, поставить запрос в нейросеть /var/www/html/iot/public_html/admin/anomalies.php
Почему именно так, а не «страница сама всё считает». Веб-страница живёт только когда её открыли. Аномалию нужно поймать ночью, без браузера, и на неё отреагировать письмом. Поэтому сбор и анализ — отдельный сервис, а страница — только пульт управления и витрина.

2. Конвейер данных целиком

  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) с нужным ему интервалом. Так он не зависит ни от чужого интервала записи, ни от того, включили историю для топика или нет.

3. Какие файлы участвуют

Полный список — от источника данных до витрины. Всё, что вне веб-корня, руками по 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, логи в journaldsystemd
/etc/iot/credentials.iniСекции [db], [mqtt], [smtp], [ai]. Права 640 root:www-data. Ключ нейросети живёт здесь, в коде его нетчитает python-сервис при старте
/etc/iot/credentials.envТе же доступы для shell и python через переменные окружения; подключается юнитом как EnvironmentFilesystemd, скрипты
/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_dbwatchers — карточки, 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)Только для кнопки «Проанализировать». Автопоиск аномалий работает без неё

4. Как это работает: пять таблиц

Вся логика раздела читается по схеме данных. Если понимать эти пять таблиц — понимать модуль целиком.

ТаблицаКлючевые поляСмысл
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 Готовый отчёт: что отправили (статистика) и что ответила нейросеть. Хранится вместе с промптом — потом видно, почему модель сказала именно это
Очередь как способ не держать браузер. Ответ нейросети может идти минуты. Если бы страница ждала его синхронно, PHP-запрос вис бы и по таймауту давал 504. Поэтому «Проанализировать» = INSERT в anomaly_requests; через 15 секунд сервис забирает строку, считает статистику, идёт в модель и кладёт ответ в anomaly_reports. Страница к этому времени давно отдана браузеру — отчёт просто появится при обновлении.

5. Четыре типа аномалий и одна честная тишина

Сервис ищет ровно пять видов событий. Это не «модный ИИ», а обычная статистика — и именно поэтому она работает на данных слабых и медленных Zigbee-датчиков.

Тип (kind)Как ловитсяВажностьПример из жизни дома
limit_lowзначение ниже limit_min из карточкиcriticalнапряжение сети ниже 198 В — свет мигает, аквариумный нагреватель не тянет
limit_highзначение выше limit_maxcriticalмощность розетки выше 600 Вт — что-то включили мимо плана
spikeвыше нормы на spike_factor сигмwarningскачок тока: плохой контакт, износ проводки, старт компрессора
dipниже нормы на spike_factor сигмwarningпросадка напряжения в момент включения нагрузки
no_dataточки не поступают дольше collect_interval × gap_intervalswarningдатчик отвалился от сети, села батарейка, завис zigbee2mqtt

Почему медиана и MAD, а не среднее и «три сигмы»

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

обычная сигма:  σ = √( Σ(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 сигм
Грабля, которая нашлась на реальных данных: нулевая сигма. У медленных датчиков значения часами не меняются — например мощность розетки стоит на 19 Вт. Медиана и MAD получаются равны, 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, только когда порог перейдён.

6. Как это выглядит в панели

Сейчас в системе три карточки — все по электросети, снятые с умной розетки аквариума. Ниже реальные значения из базы, а не пример «от себя».

Электросеть: ток нагрузки, мА ready
топик zigbee2mqtt/fish_light/current · 📧 it@logoakademia.ru
69точек собрано
60 синтервал съёма
24 чокно нормы
6 σпорог выброса
0…3000пределы
900 спауза между письмами
Кнопки: ✏️ Настроить 🤖 Проанализировать 🔄 Сбросить сбор ⏸️ Выключить сбор

Что настраивается прямо в карточке, без правки кода:

Реальный промпт одной из карточек (карточка «ток нагрузки»), как он лежит в базе:
Ты инженер-электрик. Ниже статистика по силе тока домашней розетки (аквариум) и найденные автоаномалии.

Оцени: стабильность тока, всплески при включении нагрузки, соответствие мощности и
напряжения (P = U*I), признаки плохого контакта или износа проводки. Учти, что свет и
обогреватель включаются по расписанию, поэтому ступеньки тока ожидаемы.

Ответ: 1) вывод по состоянию цепи; 2) что может давать скачки; 3) что проверить
физически; 4) какие данные добавить. По-русски, кратко.
Из этого видно главный приём: в промпте заранее объяснено, какие ступеньки нормальны. Без этого модель бодро объявит аномалией штатное включение обогревателя по расписанию.

7. Что уходит в нейросеть и что приходит назад

Модель не видит базу и не видит графики. Ей уходит текст, который сервис собирает сам — и это принципиально: если дать модели сырые точки, она утонет в них и начнёт сочинять.

Блок отчётаЧто содержит
Паспортимя карточки, топик, единица измерения, период, сколько точек и с каким интервалом
Статистикаминимум, максимум, среднее, медиана, устойчивая сигма, размах, жёсткие пределы карточки
Разбивка по суткаммин / среднее / макс и число точек за каждый день окна — по ней видно сезонность и сдвиг режима
Автонаходкисписок событий из 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».

8. Почему модуль сейчас в режиме ожидания

В базе три карточки, у всех enabled = 0, и сбора точек нет: последняя точка датирована 26 сентября 17:54. Это не поломка — карточки выключены сознательно вместе с отсутствием ключа нейросети, и вот тут начинается отдельная история про уведомления.

Первая версия общей проверки дома считала выключенный модуль двумя ошибками и каждый час отправляла письмо «ПРОБЛЕМЫ (2)». Одинаковое письмо через неделю уезжает в спам вместе с настоящими тревогами — то есть сторожевая функция тихо превращается в бесполезную. Поэтому проверка научена различать ситуации:

карточек активно 0  И  [ai] enabled != 1   →  [SKIP]  «выключен сознательно», счётчик ошибок не растёт
карточки есть, а точки не собираются      →  [FAIL]  настоящая поломка, письмо уходит
Плюс отдельно проверяется очередь: anomaly_requests со статусом pending не должна накапливаться — если накапливается, значит сервис встал. И есть защита от «ложных выбросов»: события за сутки вида spike с отклонением меньше трёх шагов считаются подозрительными — это симптом слишком узкого порога.

9. Как включить раздел на полную мощность

# 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

10. Грабли, которые уже пойманы

СимптомПричинаКак закрыто
Сторож каждый час шлёт «ПРОБЛЕМЫ (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; проверка дома отдельно ищет старые пароли в коде и должна находить пусто

11. Итог: зачем это вообще нужно

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

Раздел делает три вещи, и все три без нейросети: собирает точки по выбранным топикам со своим интервалом, считает норму устойчивыми методами (медиана и MAD) и сообщает письмом о том, что вышло за норму — с паузой, чтобы не превратиться в спам. Нейросеть здесь надстройка: она превращает таблицу чисел в осмысленный вывод, но находит аномалии статистика.

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

Документ описывает состояние системы на 28 сентября 2026 года. Все числа (69 точек, интервалы, пределы, содержимое промпта) взяты из рабочей базы iot_db, а не приведены как примеры. Раздел живёт в панели: 🔍 Аномалии. Смежные материалы: Статья: аномалии (как искать аномалии в исторических данных и промпты для нейросети), Архитектура, Отчёт доработки.
Теги: #анализ_данных #нейросети #поиск_аномалий #умный_дома



Категории:

Категории

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

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

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