Данные из двойника
Прочитайте это до проектирования
Наружу отдаётся меньше, чем принимается внутрь. Передать внутрь можно почти что угодно; обратно сегодня уходят телеметрия и аварии — через вебхуки или живой сокет. События заявок и нарядов внешнему подписчику пока не доходят: они живут на внутренней шине платформы, а внешний способ следить за заявками — опрос. Таблицы ниже показывают, что реально срабатывает, а что нет, — чтобы вы проектировали по фактической поверхности, а не по задуманной.
Три способа получить данные из платформы:
| Канал | Форма | Для чего хорош |
|---|---|---|
| Вебхуки | HTTP POST на ваш URL, с подписью | Server-to-server, переживает перезапуск вашего сервиса |
| WebSocket | Поток socket.io, по зданию | Живые дашборды, экраны диспетчера |
| Опрос | Обычные GET-запросы | Всё, что не покрыто первыми двумя, — в первую очередь заявки |
Вебхуки
Подписаться
POST /api/v1/webhooks{ "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 записывается как неудачная доставка, а не преследуется.
Как выглядит доставка
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{ "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:
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_breached | SLA нарушен |
CAFM так же публикует cafm.work-order.created / .updated / .completed, а сервис-деск их потребляет и связывает наряд с заявкой.
Ни одно из этих событий сегодня внешнему подписчику не доставляется. Их нет в списке диспетчера вебхуков, а сама шина живёт внутри кластера. Модуль, работающий внутри платформы, подписаться может (это уровень 3 и руководство по событиям); ваша система снаружи — нет.
Поэтому на «сообщите мне, когда заявка изменится» честный ответ сегодня — опрос:
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 → вебхуки.
- Живой экран → сокет.
- Заявки, наряды и всё остальное → опрос, и читайте В обе стороны — как держать две системы заявок в согласии, не сталкивая их лбами.