Conversation
Dom.ru now exposes cameras of neighboring entrances via
/rest/v1/places/{placeId}/screen-sections (viewable with a Pro
subscription). The home page loads that endpoint per place and renders
one card per camera, with its own Home Assistant snippet.
Cameras are matched by externalCameraId / forpostGroupId instead of the
array index, which broke with more than one place or camera.
Door permissions are taken from /rest/v1/places/{placeId}/accesscontrols:
after a subscription the embedded list in subscriberplaces advertises
allowOpen for neighboring doors that cannot actually be opened. Neighbor
cards never get an "Open" button.
Extra endpoints failing (or 404 on operators without screen-sections)
degrade gracefully: the basic camera list stays visible.
Add optional integrations that watch incoming intercom calls and let the
user open the door while ending the call:
- pkg/sipclient: sipgo v0.28 client for one intercom (REGISTER + Digest,
180/486 by timer, 200/ACK/BYE with recvonly G.711 and RTP sink);
credentials via POST .../sipdevices with a persistent installationId.
- pkg/callcontrol: single door-opening coordinator with modes off,
reject and answer-bye; one operation at a time, an old notification
never ends a newer call.
- pkg/telegram: long-polling bot, photo notification with a permanent
"Открыть дверь" button, state in telegram-state.json, silent success and
a single warning reply on failure.
- pkg/webhook: {"event":"Ringing"} with Idempotency-Key per call.
- /api/integrations/state, /api/.../open-and-end-call and optional
per-call diagnostics; the home page button and HA snippet of the SIP
intercom use open-and-end-call.
HTTP client fixes needed by the background components: replay the POST
body after a 401 refresh, close the previous response, serialize token
refresh, atomic 0600 writes of credentials, never retry door POSTs.
Live check on one intercom: only answer-bye silences the panel; reject
(486) ends just our leg. Default mode stays off.
The photo sent on INVITE came from the door's own snapshot endpoint, which only ever renders a 500x281 thumbnail and upscales it when width/height are given (1920x1080 comes back as a blurry 1920x1079). Take the frame from the forpost camera endpoint keyed by externalCameraId instead: exactly 1920x1080, grabbed from the stream, ~2 s. The door path stays as a fallback when the camera is unknown, and the snapshot timeout grows from 3 to 6 s accordingly. The photo is now followed by a 30 s MP4 (15 s before and after the call), cut from the operator's cloud archive rather than buffered from the live stream: a request with TS=<unix> replays the recording in real time, so one request per call replaces any ring buffer. pkg/videoclip remuxes the HTTP-FLV (H.264 + MP3) into MP4 in memory. Off by default, DOMRU_TELEGRAM_VIDEO turns it on; a failed clip only shows up in the integrations status, since the photo and the door button have already gone out.
…thout recording The cloud archive (/video?TS=) comes with the subscription; without it the streamer silently answers a TS request with live video and no clip is sent. - videoclip.Buffer keeps the last 20 s of the camera's light stream (960x528, ~0.45 Mbit/s) in RAM: frames only, no decoding, nothing on disk. Reconnects with backoff, a 10 s watchdog replaces the missing body read timeout, timestamps of a new connection are stitched to the previous one, the window always starts at a keyframe (the FLV demuxer prepends SPS/PPS). - videoclip.Auto serves the archive and switches to the buffer for good on the first errLive (startup probe or a call); that call still gets the seconds after the ring. - DOMRU_TELEGRAM_VIDEO: true = archive with fallback to the buffer, buffer = live buffer only. Remux is split into parseFLV and mux; the MP4 now starts at dts 0 (live timestamps are in the hundreds of millions).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Зачем
В исходной реализации, решение умеет показывать камеры и открывать дверь, при этом при открытии двери, звонок домофона не прекращался до таймаута, а событие звонка было только внутри официального приложения.
Этот PR добавляет то самое отсутствующее звено — событие входящего звонка. Прокси регистрируется на домофоне как второй SIP-абонент (реквизиты выдаёт сам API оператора), поэтому узнаёт о звонке в момент INVITE, одновременно с приложением. Дальше это событие можно разослать куда угодно: в Telegram, в webhook, в Home Assistant.
Что теперь происходит при звонке
answer-byeгасит вызов на панели; проверено на живом домофоне).{"event":"Ringing"}— для Home Assistant и любых своих сценариев.Всё это необязательно и по умолчанию выключено: без переменных окружения поведение прокси не меняется совсем. Можно включить только Telegram, только webhook или только SIP.
Как это сделано
pkg/sipclient(sipgo v0.28) — REGISTER + Digest у SIP-домофона, на INVITE отвечает 180, держит вызовRingTime(25 с) и завершает его. Реквизиты берутся изPOST /accesscontrols/{id}/sipdevicesс постояннымinstallationId(файл рядом сaccounts.json). Ретрансмиты INVITE дедуплицируются по Call-ID/From-tag, так что на звонок приходит ровно одно событие, а не пачка.pkg/callcontrol— единственная точка открытия двери для новых интерфейсов:TryLock(параллельное нажатие →busy, без очереди) и режимы завершения вызоваoff/reject(486) /answer-bye(200→ACK→BYE с recvonly G.711). Открытие с явным ID старого уведомления никогда не завершает новый вызов — старая кнопка в истории чата не оборвёт свежий звонок.pkg/telegram— long polling,sendPhotoсо снимком домофона и бессрочной inline-кнопкой. Состояние вtelegram-state.json: привязки кнопка→вызов/чат/сообщение, offset, результаты callback. Callback помечаетсяprocessingдо открытия, после рестарта становитсяunknown— повторного открытия двери не будет. Ошибки никогда не содержат URL с токеном.pkg/webhook—{"event":"Ringing"}сIdempotency-Key= ID вызова, повтор только на 429/5xx.pkg/videoclip— ролик 30 с (15 до и 15 после звонка) вслед за фото. Не буфер live-потока, а облачный архив оператора:/video?TS=<unix>воспроизводит запись в реальном времени, поэтому один запрос на звонок заменяет любой ring buffer. FLV (H.264 + MP3) ремуксится в MP4 в памяти, без ffmpeg и без записи на диск./accesscontrols/{id}/snapshotsфизически отдаёт только миниатюру 500×281 и наwidth/heightеё апскейлит (запрос 1920×1080 возвращает мыльные 1920×1079). Настоящий кадр из потока даёт/forpost/cameras/{externalCameraId}/snapshots?width=1920&height=1080— ровно 1920×1080; на нём читаются номера машин и вывески. Цена — ~2 с на захват кадра против ~0.2 с у миниатюры; от размера картинки это время не зависит, так что понижать разрешение смысла нет. Дверной путь остался фолбэком, если камера неизвестна./api/integrations/stateраз в 10 с и скрыта, когда интеграции выключены.Правки существующего кода вне интеграций:
authorizedhttpбуферизует тело (≤1 MiB) и повторяет POST после 401 под мьютексом обновления токена, ответы закрываются;auth.CredentialsStoreпишетaccounts.jsonатомарно с правами0600(pkg/atomicfile). Они полезны сами по себе — если SIP/Telegram целиком не нужны, могу вынести их в отдельный PR.Переменные окружения
Полная таблица — в README. Кратко:
DOMRU_SIP_ENABLED+DOMRU_SIP_IP,DOMRU_SIP_ACCESS_CONTROL_ID,DOMRU_SIP_END_MODE;DOMRU_TELEGRAM_BOT_TOKEN+DOMRU_TELEGRAM_CHAT_ID+DOMRU_TELEGRAM_VIDEO;DOMRU_WEBHOOK_URL. Для SIP нужна host-сеть — пример вdocker-compose.sip.yml.Грабли sipgo, важные для ревью
Server.handleRequestвызываетtx.Terminate()сразу после возврата, иначе CANCEL получает 481, а отложенный 486 не уходит. Каждый запрос обрабатывается в своей горутине, так что блокировка безопасна.ServeUDPдобавляет слушатель в пул асинхронно; запрос до этого открывает второй сокет на том же порту и падает, поэтому старт ждёт появления соединения черезGetConnection.sipclient.New, чтобы SIP-реквизиты не попали в лог.Проверка
gofmt,go vet,go test ./...иgo test -race ./...зелёные; тесты покрывают дедупликацию INVITE, 486 после CANCEL, режимыcallcontrol, идемпотентность webhook, персистентность Telegram-кнопок между рестартами, ремукс FLV→MP4 и выбор эндпоинта снимка.Живьём на своём домофоне:
reject(486) завершает только нашу ветку вызова, панель продолжает звонить;answer-byeгасит вызов на панели — именно он и даёт «нажал кнопку, дверь открылась, домофон замолчал». Дефолт в коде поэтому оставленoff: гасить чужой звонок молча — не то поведение, которое стоит включать без спроса. Кнопка в Telegram, фото и видеоклип проверены на реальных звонках.