Превращаем китайский домофон за 400 рублей в Telegram-звонок с фото и видео

от автора

Несколько лет у меня лежал недорогой китайский видеодомофон Doorbell M8, работающий через приложение Ulooka. Покупал я его на одном из маркетплейсов примерно за 400 рублей — скорее из любопытства, чем из реальной необходимости.

Doorbell

Doorbell

Устройство оказалось вполне типичным представителем дешёвого китайского IoT:

  • включается только после нажатия кнопки;

  • подключается к Wi-Fi;

  • отправляет уведомление через китайские серверы;

  • показывает изображение в приложении Ulooka;

  • почти не предоставляет возможностей для локальной интеграции.

В целом свою задачу домофон выполнял, но всё работало как-то коряво. Уведомления приходили с задержкой, приложение выглядело сомнительно, а зависимость от неизвестных облачных серверов энтузиазма не добавляла.

В какой-то момент я просто перестал им пользоваться. Причём не только как видеодомофоном, но даже как обычным звонком.

Недавно устройство снова попалось мне на глаза, и я решил разобраться: можно ли получить от него изображение напрямую, минуя Ulooka?

Попытка найти RTSP и локальный API

Первой мыслью было поискать у Doorbell M8 стандартный RTSP-поток.

Для IP-камер это обычная история:

rtsp://192.168.0.100/stream

или что-нибудь похожее:

rtsp://user:password@192.168.0.100/live/ch00_0

Я перебрал несколько типичных путей, посмотрел открытые порты, изучил сетевую активность устройства. Но RTSP так и не нашёл.

Следующая идея — перехватить протокол между домофоном и приложением Ulooka. Возможно, устройство поднимает локальный TCP- или UDP-сервис, через который можно получить кадр или видеопоток.

Однако быстро выяснилось, что всё завязано на собственный закрытый протокол и китайскую облачную инфраструктуру. Домофон выходит в интернет, устанавливает соединения с внешними серверами, а приложение уже общается с устройством через них.

Теоретически можно было продолжить реверс-инжиниринг:

  • анализировать пакеты;

  • разбирать бинарный протокол;

  • исследовать APK приложения;

  • подменять DNS;

  • пытаться воспроизвести авторизацию.

Но для домофона стоимостью 400 рублей это выглядело неоправданно сложно.

Тогда я решил зайти с другой стороны.

Не обязательно получать изображение именно с домофона

Мне не нужен был полный контроль над устройством. На самом деле требовалось решить две простые задачи:

  1. понять, что кто-то нажал кнопку;

  2. получить изображение человека перед дверью.

Первую задачу мог решить сам домофон. Вторую — любая другая камера.

И тут я вспомнил, что почти над домофоном у меня уже висит обычная IP-камера с поддержкой RTSP.

Камера достаточно старая. Когда-то я устанавливал её для наблюдения за садом, но нормальную детекцию движения так и не настроил. Она просто постоянно пишет изображение, зато предоставляет обычный RTSP-поток.

Получилась простая схема:

Нажатие кнопки Doorbell M8            ↓MikroTik замечает сетевую активность домофона            ↓MikroTik отправляет HTTP POST на локальный сервер            ↓Python получает свежий кадр с RTSP-камеры            ↓Сохраняет фото и несколько секунд видео            ↓Отправляет всё в Telegram

То есть сам Doorbell M8 используется только как беспроводная кнопка с Wi-Fi.

Изображение берётся уже с нормальной камеры.

Как определить нажатие кнопки через MikroTik

Самой интересной частью проекта оказалось не получение изображения, а определение самого факта нажатия кнопки.

Doorbell M8 большую часть времени спит. В таком состоянии он почти не создаёт сетевого трафика. После нажатия кнопки устройство просыпается, подключается к Wi-Fi и начинает обмениваться пакетами как с локальной сетью, так и с внешними серверами Ulooka.

Получается, что нажатие можно определить, даже ничего не зная о внутреннем протоколе домофона. Достаточно найти характерный пакет, который появляется сразу после пробуждения устройства.

Закрепляем IP-адрес домофона

Сначала нужно определить IP- и MAC-адрес устройства.

В WinBox это можно посмотреть здесь:

IP → DHCP Server → Leases

В моём случае домофон получил адрес:

192.168.0.15

Чтобы адрес не изменился после перезагрузки роутера или повторного подключения устройства, lease необходимо сделать статическим:

IP → DHCP Server → Leases → Make Static

После этого правила MikroTik можно привязать к конкретному IP.

При желании правило можно строить по MAC-адресу, но для обычного домашнего сценария статического DHCP lease вполне достаточно.

Смотрим, что происходит после нажатия

Далее я запустил встроенный Packet Sniffer MikroTik и несколько раз нажал кнопку домофона.

В WinBox это находится здесь:

Tools → Packet Sniffer

Для начала достаточно указать:

Interface: интерфейс или bridge, к которому подключён домофонIP Address: 192.168.0.15

Полезно сделать два захвата:

  1. несколько минут, пока домофон просто лежит и его никто не трогает;

  2. отдельный захват непосредственно после нажатия кнопки.

Так проще отделить регулярные фоновые пакеты от трафика, который действительно связан с вызовом.

В моём дампе после нажатия домофон начал отправлять широковещательные UDP-пакеты на адрес 255.255.255.255 по портам 10104 и 17900. Позже появился короткий UDP-пакет с локального адреса 192.168.0.15 на внешний адрес 47.105.85.47, порт 10102. Размер этого пакета составлял всего 82 байта, то есть видеоданных в нём явно не было — это был только служебный обмен.

Внутри полезной нагрузки встречалась последовательность:

31 54 45 47

В ASCII это выглядит как:

1TEG

Но по одному короткому пакету нельзя надёжно определить название или устройство закрытого протокола. Для моей задачи это было и не нужно: важен не смысл содержимого, а сам факт появления пакета после нажатия.

Какой пакет использовать в качестве триггера

Сначала я попробовал ловить исходящий пакет на внешний порт 10102:

/ip firewall filter add \    chain=forward \    src-address=192.168.0.15 \    protocol=udp \    dst-port=10102 \    action=add-src-to-address-list \    address-list=doorbell_pressed \    address-list-timeout=5s \    comment="Doorbell cloud trigger"

Такой вариант работал, но пакет появлялся не сразу. Домофон сначала выполнял локальный обмен, и только потом соединялся с внешним сервером.

Для уведомления о звонке лишняя задержка не нужна.

Поэтому более удобным триггером оказался первый широковещательный UDP-пакет на порт 10104. Он появляется почти сразу после пробуждения устройства.

Итоговое правило:

/ip firewall filter add \    chain=input \    src-address=192.168.0.15 \    protocol=udp \    dst-address=255.255.255.255 \    dst-port=10104 \    src-address-list=!doorbell_cooldown \    action=add-src-to-address-list \    address-list=doorbell_pressed \    address-list-timeout=5s \    comment="Doorbell M8 press trigger"

В ходе настройки использовался аналогичный вариант правила для chain=input и UDP-порта 10104.

Параметры здесь означают следующее:

chain=input

Пакет направлен на широковещательный адрес локальной сети и попадает в IP-стек самого MikroTik.

src-address=192.168.0.15

Учитываем только пакеты от домофона.

dst-address=255.255.255.255dst-port=10104

Ловим конкретный широковещательный UDP-пакет, обнаруженный в дампе.

src-address-list=!doorbell_cooldown

Не создаём новое событие, пока действует защита от повторного срабатывания.

action=add-src-to-address-list

MikroTik не блокирует пакет, а просто добавляет IP устройства во временный список.

address-list=doorbell_pressed

Название списка, который будет означать: «обнаружено нажатие кнопки».

Проверяем правило

После добавления правила нужно открыть:

IP → Firewall → Filter Rules

и посмотреть на счётчики:

PacketsBytes

После нажатия кнопки они должны увеличиться.

Затем открываем:

IP → Firewall → Address Lists

Там должен появиться адрес:

192.168.0.15

в списке:

doorbell_pressed

Если счётчики не меняются, стоит проверить несколько вещей.

Во-первых, правило должно находиться выше общих запрещающих правил. Иначе пакет может быть отброшен раньше, чем попадёт в наше правило.

Во-вторых, для конкретной конфигурации MikroTik широковещательный пакет может обрабатываться не в input, а в forward. В таком случае достаточно изменить:

chain=input

на:

chain=forward

В-третьих, нужно проверить, что домофон действительно имеет адрес 192.168.0.15. После переподключения устройства адрес мог измениться, если DHCP lease не был сделан статическим.

Ещё одна распространённая ошибка при вводе команды:

/ip firewall filteradd ...

Правильно:

/ip firewall filter add ...

Между filter и add обязательно должен быть пробел.

Передаём событие на Python-сервер

Само правило firewall не умеет отправлять HTTP-запрос. Оно только создаёт запись в address list.

Для передачи события я добавил скрипт MikroTik:

/system script add name=doorbell-webhook source={    :local isPressed [/ip firewall address-list find where list="doorbell_pressed"]    :if ([:len $isPressed] > 0) do={        /ip firewall address-list add \            list=doorbell_cooldown \            address=192.168.0.15 \            timeout=10s \            comment="Doorbell repeat protection"        /ip firewall address-list remove $isPressed        /tool fetch \            url="http://192.168.0.16:5000/doorbell" \            http-method=post \            keep-result=no    }}

Скрипт выполняет три действия.

Сначала он добавляет домофон в список:

doorbell_cooldown

на 10 секунд. Пока устройство находится в этом списке, firewall-правило игнорирует следующие пакеты от него.

Это важно, потому что после пробуждения Doorbell M8 отправляет не один пакет, а целую серию. Без защиты одно физическое нажатие могло бы превратиться в несколько HTTP-запросов.

Затем скрипт удаляет запись из:

doorbell_pressed

и вызывает локальное Python-приложение:

POST http://192.168.0.16:5000/doorbell

В RouterOS для POST-запроса используется параметр:

http-method=post

а не просто:

method=POST

Запускаем проверку раз в секунду

Остаётся добавить скрипт в Scheduler:

/system scheduler add \    name="Check Doorbell" \    interval=1s \    start-time=startup \    on-event=doorbell-webhook

Теперь MikroTik раз в секунду проверяет наличие записи в списке doorbell_pressed.

Полная последовательность выглядит так:

1. Гость нажимает кнопку.2. Doorbell M8 просыпается.3. Домофон отправляет UDP broadcast на порт 10104.4. Firewall MikroTik добавляет 192.168.0.15 в doorbell_pressed.5. Scheduler запускает скрипт doorbell-webhook.6. Скрипт включает десятисекундную защиту от повторов.7. MikroTik отправляет POST на Python-сервер.8. Python-приложение получает кадр и записывает видео с RTSP-камеры.9. Фото и ролик отправляются в Telegram.

Почему не стал привязываться к внешнему IP

Можно было бы ловить только пакет на конкретный внешний адрес:

47.105.85.47:10102

Но у такого решения есть недостаток: адрес облачного сервера может измениться.

Сегодня устройство связывается с одним сервером, а после обновления прошивки, изменения DNS или переключения облачной инфраструктуры может начать использовать другой.

Поэтому в основном правиле я использую:

  • постоянный локальный IP домофона;

  • UDP;

  • широковещательный адрес;

  • характерный порт 10104.

Такой триггер не зависит от текущего адреса китайского сервера.

При этом порт тоже не стоит считать универсальным для всех устройств Ulooka. У другой ревизии Doorbell M8 или другой версии прошивки сетевое поведение может отличаться. Перед созданием правила лучше выполнить собственный захват и сравнить трафик до и после нажатия.

Проверяем отсутствие ложных срабатываний

После настройки я рекомендую провести простой тест:

  1. не трогать домофон 10–15 минут;

  2. убедиться, что список doorbell_pressed сам по себе не появляется;

  3. нажать кнопку 10 раз с интервалом около минуты;

  4. проверить, что на каждое нажатие приходится ровно одно событие;

  5. несколько раз быстро нажать кнопку подряд;

  6. убедиться, что защита doorbell_cooldown не допускает спама.

Если домофон периодически просыпается самостоятельно и отправляет те же broadcast-пакеты, одного порта 10104 может оказаться недостаточно. Тогда правило можно сделать строже:

  • учитывать направление и интерфейс;

  • выбрать другой пакет из дампа;

  • использовать комбинацию адреса и порта;

  • ловить исходящий пакет на 10102, пожертвовав небольшой задержкой;

  • увеличить cooldown;

  • добавить дополнительную проверку на Python-сервере.

В моём приложении также есть программная защита от повторных событий. Поэтому даже при нескольких последовательных запросах сервер не начинает одновременно записывать десяток одинаковых роликов. На этом задача обнаружения нажатия была решена. MikroTik уже умел превращать сетевую активность домофона в обычный HTTP-запрос. Оставалось обработать этот запрос на локальном сервере, получить свежий кадр с расположенной рядом RTSP-камеры и отправить результат в Telegram.

Получение кадра из RTSP

Для работы с камерой я использовал OpenCV:

import cv2RTSP_URL = "rtsp://192.168.0.199/stream.sdp1"cap = cv2.VideoCapture(RTSP_URL)ret, frame = cap.read()if ret:    cv2.imwrite("snapshot.jpg", frame)cap.release()

Но открывать RTSP-соединение только после звонка оказалось не лучшей идеей.

Камера может отвечать несколько секунд, первый кадр иногда приходит старым, а часть дешёвых камер вообще нестабильно переносит частые переподключения.

Поэтому в итоговой версии приложения работает отдельный фоновый поток, который постоянно читает RTSP и хранит последний свежий кадр в памяти.

Условно это выглядит так:

latest_frame = Nonewhile True:    ret, frame = cap.read()    if ret:        latest_frame = frame

Когда приходит сигнал от MikroTik, приложению не нужно заново подключаться к камере. Оно сразу берёт уже готовый свежий кадр.

Это заметно уменьшает задержку.

Запись короткого видео

Одного скриншота иногда недостаточно. Человек может отвернуться, наклониться или оказаться между кадрами.

Поэтому я добавил запись короткого ролика продолжительностью от 5 до 10 секунд.

Упрощённо запись выглядит так:

writer = cv2.VideoWriter(    "doorbell.mp4",    cv2.VideoWriter_fourcc(*"mp4v"),    10,    (width, height),)for _ in range(70):    frame = get_latest_frame()    if frame is not None:        writer.write(frame)    time.sleep(0.1)writer.release()

В реальном приложении длительность и FPS задаются через настройки.

Также видео при наличии FFmpeg перекодируется в H.264, чтобы Telegram и браузеры нормально его воспроизводили.

Отправка в Telegram

Для Telegram используется обычный Bot API.

Фотография отправляется методом sendPhoto:

url = f"https://api.telegram.org/bot{TOKEN}/sendPhoto"with open("snapshot.jpg", "rb") as photo:    requests.post(        url,        data={            "chat_id": CHAT_ID,            "caption": "🔔 Кто-то звонит в дверь!",        },        files={            "photo": photo,        },        timeout=30,    )

Видео отправляется через sendVideo:

url = f"https://api.telegram.org/bot{TOKEN}/sendVideo"with open("doorbell.mp4", "rb") as video:    requests.post(        url,        data={            "chat_id": CHAT_ID,            "caption": "🎥 Видео после звонка",            "supports_streaming": "true",        },        files={            "video": video,        },        timeout=90,    )

В итоговом варианте я добавил три режима:

  • только фотография;

  • только видео;

  • фотография и видео.

Последний режим оказался наиболее удобным.

Сразу после звонка приложение фиксирует стоп-кадр и начинает отправлять его в Telegram. Одновременно продолжается запись ролика. После завершения видео отправляется вторым сообщением.

Таким образом, фотография приходит быстро, а через несколько секунд появляется и полный фрагмент события.

Веб-интерфейс

Сначала скрипт представлял собой небольшой Flask-обработчик с одним endpoint:

@app.post("/doorbell")def doorbell():    capture_and_send()    return "", 202

Но довольно быстро стало понятно, что управлять настройками через исходный код неудобно.

Поэтому я добавил простой локальный веб-интерфейс.

Главная страница

Главная страница

Через него можно:

  • указать RTSP URL;

  • ввести токен Telegram-бота;

  • указать Chat ID;

  • выбрать режим фото, видео или фото плюс видео;

  • задать длительность ролика;

  • проверить подключение к камере;

  • отправить тестовое сообщение в Telegram;

  • посмотреть живой предпросмотр;

  • вручную сделать снимок;

  • вручную записать видео;

  • посмотреть историю звонков;

  • открыть сохранённые фотографии и ролики.

Настройки сохраняются в обычный файл:

settings.json

Никаких переменных окружения или сложной конфигурации для домашнего использования не требуется.

Главная страница доступна по адресу:

http://192.168.0.16:5000/

Настройки:

http://192.168.0.16:5000/settings
Страница настроек

Страница настроек

История событий

Каждый звонок записывается в SQLite.

Для события сохраняются:

  • время;

  • источник;

  • выбранный режим;

  • статус обработки;

  • имя фотографии или видео;

  • результат отправки в Telegram;

  • текст ошибки, если что-то пошло не так.

Файлы хранятся в каталоге:

media

Логи приложения:

logs

Также есть автоматическое удаление старой истории через заданное количество дней.

Проблемы с китайским H.264

Во время работы в консоли иногда появляются сообщения вроде:

error while decoding MBnon-existing SPS referenceddecode_slice_header errormissing picture in access unit

Это не ошибки Telegram или Flask.

Их выводит декодер H.264 при чтении RTSP-потока. Старая камера иногда присылает повреждённые или неполные кадры.

В моём случае поток продолжает работать: отдельные кадры могут повреждаться, но на следующем ключевом кадре изображение восстанавливается.

Для уменьшения количества ошибок можно:

  • использовать RTSP поверх TCP;

  • отключить в камере H.264+ или Smart Codec;

  • снизить битрейт;

  • уменьшить FPS;

  • использовать дополнительный поток меньшего разрешения;

  • уменьшить интервал между ключевыми кадрами.

В приложении RTSP принудительно переводится на TCP:

os.environ.setdefault(    "OPENCV_FFMPEG_CAPTURE_OPTIONS",    "rtsp_transport;tcp",)

Что получилось

В результате старый китайский домофон получил вторую жизнь.

Я не стал ломать закрытый протокол Ulooka, подменять китайские серверы или пытаться вытащить видео из самого устройства.

Вместо этого разделил задачу на две части:

  • Doorbell M8 определяет сам факт нажатия кнопки;

  • обычная RTSP-камера предоставляет изображение.

MikroTik связывает эти два события, а небольшой Python-сервис отправляет результат в Telegram.

Теперь после нажатия кнопки я получаю:

  1. уведомление в Telegram;

  2. фотографию человека у двери;

  3. короткий видеоролик;

  4. локальную запись в истории.

И всё это работает внутри моей сети, за исключением самой отправки сообщения в Telegram.

Итоговая архитектура

┌──────────────────┐│   Doorbell M8    ││  кнопка + Wi-Fi  │└────────┬─────────┘         │ сетевая активность         ▼┌──────────────────┐│     MikroTik     ││ firewall + script│└────────┬─────────┘         │ HTTP POST /doorbell         ▼┌──────────────────┐│ Python + Flask   ││ очередь событий  │└───────┬──────────┘        │        ├──────────────► RTSP-камера        │                фото + видео        │        ├──────────────► SQLite        │                история        │        └──────────────► Telegram Bot API                         уведомления

Иногда для полезной автоматизации не обязательно полностью взламывать или переписывать существующее устройство, достаточно определить, какую часть задачи оно уже умеет выполнять, и дополнить её теми компонентами, которые есть под рукой.

В моём случае дешёвый домофон стал кнопкой, старая камера — источником изображения, MikroTik — детектором события, а Telegram — удобным интерфейсом уведомлений.

ссылка на оригинал статьи https://habr.com/ru/articles/1060478/