Skip to content

Данные из двойника

Прочитайте это до проектирования

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

Три способа получить данные из платформы:

КаналФормаДля чего хорош
ВебхукиHTTP POST на ваш URL, с подписьюServer-to-server, переживает перезапуск вашего сервиса
WebSocketПоток socket.io, по зданиюЖивые дашборды, экраны диспетчера
ОпросОбычные GET-запросыВсё, что не покрыто первыми двумя, — в первую очередь заявки

Вебхуки

Подписаться

http
POST /api/v1/webhooks
json
{ "url": "https://fm.example.com/hooks/tango", "events": ["building.alarm.triggered"] }

В ответе придёт secretцеликом и один раз. При любом последующем чтении он маскируется до первых 8 символов. Сохраняйте его сразу при создании подписки.

Требуется право platform.webhooks:write, оно есть только у роли manager: регистрация URL означает «шлите наш поток событий туда», и это намеренно не низкопривилегированное действие.

Ваш URL должен быть публично доступен по https

При регистрации хост резолвится, и адреса loopback, приватных диапазонов (10/8, 172.16/12, 192.168/16), link-local и метаданных облака, CGNAT и multicast отклоняются. Та же проверка выполняется повторно прямо перед каждой доставкой, поэтому DNS, который позже начнёт указывать внутрь, тоже перестанет работать. Обычный http вне локальной разработки не принимается.

Практически: http://localhost:3000/hook зарегистрировать нельзя. На время разработки используйте туннель с публичным https-URL.

Редиректы не выполняются: 3xx записывается как неудачная доставка, а не преследуется.

Как выглядит доставка

http
POST /hooks/tango HTTP/1.1
Content-Type: application/json
X-TV-Signature: 9f0c…            ← HMAC-SHA256 от сырого тела, hex, на вашем секрете
X-TV-Idempotency-Key: 7c1e…:building.alarm.triggered:1754300000000
X-TV-Delivery-Attempt: 1
json
{ "event": "building.alarm.triggered", "data": { "buildingId": "…", "…": "поля события" }, "timestamp": "2026-08-04T09:12:33.120Z" }

Проверяйте подпись по сырому телу запроса до разбора JSON, сравнением за константное время. Дедуплицируйте по X-TV-Idempotency-Key: при повторе он тот же, поэтому доставка «хотя бы один раз» превращается у вас в обработку «ровно один раз».

Повторы, и когда мы сдаёмся

  • Таймаут попытки — 10 секунд. Всё, что не 2xx, считается неудачей.
  • Всего до 6 попыток с задержками 1 мин → 5 мин → 15 мин → 1 ч → 3 ч и джиттером ±10% — около 4,5 часов от первой попытки до конца. После этого доставка уходит в dead-letter и больше не повторяется.
  • 10 неудач подряд ставят подписку на паузу: новые события в неё больше не ставятся. После часа тишины одно событие пропускается как проба; успех обнуляет счётчик. Снять паузу немедленно: POST /api/v1/webhooks/{webhookId}/reset-failures.
  • GET /api/v1/webhooks/{webhookId}/deliveries возвращает последние 50 попыток со статусом, HTTP-кодом и обрезанным телом ответа — первое место, куда смотреть, когда «события перестали приходить».

Какие события реально срабатывают

Подписаться можно на четыре типа. Платформа сегодня порождает только два:

СобытиеМожно подписатьсяСрабатывает сегодняКто порождает
building.point.updatedдадаКаждый приём телеметрии
building.alarm.triggeredдадаОбнаружение неисправностей
building.element.status_changedданетНикто пока не публикует
building.changeset.appliedданетНикто пока не публикует

Подписка на нижние два принимается и молчит. Мы их перечисляем, потому что вы увидите их и в каталоге событий SDK, и в триггер-ноде n8n, а молчащая подписка иначе неотличима от сломанной.

Ещё одна асимметрия: building.fault.created платформа публикует, но его нет в списке диспетчера вебхуков, поэтому доставить его вам нельзя. Наружу смотрит именно building.alarm.triggered.

Живой сокет

Для экранов, а не серверов, tv-api отдаёт namespace socket.io:

js
import { io } from 'socket.io-client';

const socket = io('https://tv-api.k8s.tangovision.dev/buildings', {
  auth: { token: accessToken },          // либо заголовок Authorization: Bearer
});
socket.emit('join', { buildingId });      // комнаты — по зданию
socket.on('building.point.updated', console.log);

Здесь те же четыре типа building.* (и те же два реально работающих) плюс поток занятости по Wi-Fi — wifi-sensing.event, wifi-sensing.occupancy.changed, wifi-sensing.hvac.presence, wifi-sensing.meeting.lifecycle, — которого вебхуки не доставляют.

Namespace проверяет токен Keycloak по JWKS реалма, а список разрешённых источников берёт из CORS_ORIGINS контура. Он фейлится закрыто: если вашего origin в списке нет, соединение отклоняется и никакого сообщения о причине не приходит. Если браузерный клиент вообще не подключается — это первое, что нужно проверить с нами.

Заявки и наряды: что есть, а чего нет

Внутри платформы сервис-деск публикует настоящие события на внутренней шине (субъект tv.building.{buildingId}.{type}):

СобытиеКогда
service-desk.ticket.createdЗаявка создана
service-desk.ticket.updatedИзменились статус, назначение или поля
service-desk.ticket.sla_warningЧасы SLA перешли порог предупреждения
service-desk.ticket.sla_breachedSLA нарушен

CAFM так же публикует cafm.work-order.created / .updated / .completed, а сервис-деск их потребляет и связывает наряд с заявкой.

Ни одно из этих событий сегодня внешнему подписчику не доставляется. Их нет в списке диспетчера вебхуков, а сама шина живёт внутри кластера. Модуль, работающий внутри платформы, подписаться может (это уровень 3 и руководство по событиям); ваша система снаружи — нет.

Поэтому на «сообщите мне, когда заявка изменится» честный ответ сегодня — опрос:

http
GET /api/service-desk/requests?buildingId={uuid}&state=in_progress&limit=200

с origin оболочки Building OS (https://building-os.k8s.tangovision.dev/api/service-desk/requests). Фильтры: state, priority, category, assignedUserId, assignedTeamId, spaceId, storeyId, elementId, search, overdue, плюс page и limit (максимум 200).

Фильтра updatedSince пока нет

У списка нет параметра «изменённые с момента», поэтому инкрементальная синхронизация — это выбирать открытые статусы по расписанию и самим сравнивать updatedAt. Для нескольких сотен открытых заявок на здание это вполне рабочая схема; но такую схему не выбирают, когда фильтр есть. Если он вам нужен — скажите: доработка небольшая, и именно знание о том, что кто-то её ждёт, ставит её в план.

Как выбрать

  • Телеметрия или аварии, server-to-server → вебхуки.
  • Живой экран → сокет.
  • Заявки, наряды и всё остальное → опрос, и читайте В обе стороны — как держать две системы заявок в согласии, не сталкивая их лбами.

Создано на платформе Tango Vision. Вопросы? developers@tango.vision