Данные в двойник
Два вида входящих данных с двумя разными контрактами:
| Что у вас есть | Что использовать | Ключ |
|---|---|---|
| Одно текущее значение на помещение — статус аренды, число открытых заявок, CO₂, статус уборки | Слои данных | Идентификатор помещения |
| Поток показаний во времени — давление, температура, энергопотребление | Телеметрия (observations) | Точка на единице оборудования |
Заявки — третий случай, у него своя страница: В обе стороны.
Всё ниже — обычный HTTP к tv-api. Базовый URL в облачном контуре: https://tv-api.k8s.tangovision.dev. В каждом запросе — Authorization: Bearer <token>, см. Получение доступа.
Слои данных
Слой данных — это одно значение на узел графа (сегодня — на помещение). Он отрисовывается и на 2D-плане, и на BIM-модели из одного источника.
1. Создать слой
POST /api/v1/buildings/{buildingId}/data-layers{
"key": "fm-open-tickets",
"name": "Открытые заявки",
"description": "Открытые заявки по помещениям, из системы эксплуатации",
"valueType": "NUMBER"
}| Поле | Правила |
|---|---|
key | Обязательно. URL-безопасный слаг: ^[a-z0-9][a-z0-9-]{1,62}$. Уникален в пределах здания — повтор вернёт 409. |
name | Обязательно. То, что пользователь видит в списке слоёв. |
description | Необязательно. |
valueType | Обязательно, одно из NUMBER, STRING, BOOLEAN, ENUM. Неизменяемо — PATCH намеренно не меняет его, иначе все сохранённые значения станут недопустимыми. Нужен другой тип — создайте другой слой. |
valueSchema | Необязательная JSON Schema: компилируется при создании и проверяется на каждом значении при загрузке. Некорректная схема — 400 уже на создании. |
legend | Необязательная цветовая легенда — см. предупреждение ниже. |
metadata | Необязательный произвольный объект (мы помечаем демо-слои через {"synthetic": true}). |
2. Загрузить значения
PUT /api/v1/buildings/{buildingId}/data-layers/{layerId}/values{
"keyBy": "externalId",
"replace": false,
"values": {
"3lPQxG$0v9zRZ1mB7kYtE2": 4,
"1aBcDe$FgHiJkLmNoPqRs": 0
}
}| Поле | Значение |
|---|---|
keyBy | externalId (по умолчанию) — ключи это IFC GlobalId. id — ключи это внутренние UUID помещений. См. Идентификаторы. |
replace | false (по умолчанию) обновляет только те помещения, что есть в payload. true сначала удаляет все значения слоя, то есть payload становится полным состоянием. |
values | Объект идентификатор → значение. Не массив: произвольные ключи обязаны лежать внутри одного свойства, потому что API отвергает неизвестные поля верхнего уровня. |
Типы значений проверяются по каждой записи до записи в базу: NUMBER ждёт конечное JSON-число, BOOLEAN — булево, STRING и ENUM — строку. Несовпадение валит весь запрос с 400 и называет проблемный ключ: загрузка атомарна, наполовину она не применяется.
Ответ говорит, что реально записалось:
{ "written": 812, "matched": 812, "unmatched": ["3lPQ...unknown"], "deleted": 0 }unmatched — идентификаторы, которым не нашлось помещения в этом здании. Читайте это поле. 200 с written: 0 — успешный запрос, который ничего не изменил, и это самый частый способ получить «интеграция работает» при пустом плане.
Если не совпало ничего, в ответе появляется diagnostics — чтобы вы искали в правильном месте:
reason | Что значит |
|---|---|
BUILDING_HAS_NO_SPACES | Граф здания пуст: модель не импортировали, помещения не создавали. |
BUILDING_HAS_NO_EXTERNAL_IDS | Помещения есть, но ни у одного нет GlobalId, поэтому keyBy: "externalId" не совпадёт никогда. Используйте keyBy: "id". |
IDENTIFIERS_NOT_IN_BUILDING | Обе стороны заполнены, но эти идентификаторы — из другого здания или другой модели. |
Ограничения на пакетную загрузку
- 10 000 идентификаторов на запрос. Больше —
400, делите payload. - 10 МБ на тело запроса.
- tv-api ограничивает частоту вызовов (порядка 100 запросов в минуту) и отвечает
429сRetry-After. Пакетируйте: один запрос на 10 000 значений, а не 10 000 запросов по одному. - Чтение постранично, серверный потолок — 100 записей на страницу (
limit,offset). Большийlimitотклоняется, а не обрезается молча, — так что полное чтение большого слоя это цикл, а не один вызов.
3. Прочитать значения обратно
GET /api/v1/buildings/{buildingId}/data-layers/{layerId}/values?gte=3&limit=100{
"data": [
{ "entityId": "0d2f…", "externalId": "3lPQxG$0v9zRZ1mB7kYtE2", "value": 4, "updatedAt": "2026-08-04T09:12:33.120Z" }
],
"total": 41, "limit": 100, "offset": 0
}Фильтры: eq (точное совпадение, приводится к типу слоя), gt / gte / lt / lte (только для слоёв NUMBER, иначе 400) и in. Учтите, что in сейчас принимает одно значение: список через запятую вернёт 400 с предложением слать по одному eq на значение. Пока это не изменится, рассчитывайте на один запрос на значение.
В ENUM-слое подпись легенды — это само значение
Если у слоя не задана legend, цвета и подписи выводятся из данных: для слоёв ENUM и STRING каждое различное значение становится записью легенды, подпись которой равна самому значению, отсортированному по алфавиту. Загрузите OCCUPIED / VACANT — русскоязычный оператор прочитает на экране OCCUPIED / VACANT. Мы наступили на это вживую и правили при заказчике.
Два выхода, оба нормальные:
Писать значения на языке интерфейса —
"Занято","Свободно". Просто и правильно, пока хватает одного языка.Передать явную
legend, а в значениях оставить машинные коды. Явная легенда, совпавшая хотя бы с одним значением, побеждает целиком — вместе с подписями:json"legend": [ { "value": "OCCUPIED", "label": "Занято", "color": "#ef4444" }, { "value": "VACANT", "label": "Свободно", "color": "#22c55e" } ]Запись задаётся либо через
value(точное совпадение), либо черезmin/max(включительный диапазон, для числовых слоёв). Легенда, не совпавшая ни с чем, считается негодной, и включается автоматическая раскраска, — то есть опечатка вvalueдеградирует тихо, а не гасит слой.
Ещё две особенности автоматической легенды: слои BOOLEAN без легенды подписываются «да»/«нет», а сверх десяти категорий остаток сворачивается в «прочее (N)». Это буквальные русские строки в общем пакете отрисовки, независимо от языка интерфейса. Если аудитория англоязычная — задайте легенду явно.
Как наполнить слой вообще без кода
n8n покрывает тот же контракт без программирования: расписание, HTTP-вызов, преобразование, повторы. Наш пакет нод @tv/n8n-nodes-building-os добавляет ресурсы Building OS (площадки, здания, этажи, помещения, элементы, точки, телеметрия) и триггер по событиям. Отдельной ноды для слоёв данных пока нет — используйте штатную ноду HTTP Request, выбрав тот же креденшл Building OS API как предустановленный тип, и укажите эндпоинты выше. (Если ваша версия n8n не предлагает там наш креденшл, подойдёт и Header Auth с заголовком Authorization: Bearer … — тогда токен вы обновляете сами.)
Креденшл — это сервисная учётная запись Keycloak (client credentials): базовый URL, URL Keycloak, realm, client id, client secret. Токен n8n обновляет сам, когда tv-api отвечает 401.
Телеметрия: показания во времени
Данные мониторинга — давление, температура, счётчик — это не слой данных. Они идут в точку, а точки висят на оборудовании в графе:
здание → этаж → помещение → элемент (приточная установка) → точка (температура приточного воздуха)Поэтому шага два: убедиться, что точка существует, и слать в неё значения.
Создать точку один раз
POST /api/v1/buildings/{buildingId}/points{
"elementId": "…uuid приточной установки…",
"pointType": "SENSOR",
"name": "Температура приточного воздуха",
"quantityKind": "Temperature",
"unit": "Cel"
}elementId обязателен — точка всегда принадлежит оборудованию. pointType — одно из SENSOR, COMMAND, SETPOINT, ALARM, STATUS, PARAMETER. Если вашего оборудования ещё нет в графе, сначала создайте элементы (POST /api/v1/buildings/{buildingId}/elements — принимает spaceId и ваши собственные externalId + sourceSystem) либо загрузите их пакетно через POST /api/v1/buildings/{buildingId}/import/elements и …/import/points.
Два маршрута import/* закрыты отдельно от одиночного создания элементов выше: им нужны data-import.sync:read и data-import.sync:write, а несут их только роли admin, manager и accountant. Креденшл с другой ролью получит 403 PERMISSION_DENIED, даже если он корректно ограничен нужным зданием, — см. таблицу прав ниже.
Отправлять наблюдения
POST /api/v1/buildings/{buildingId}/telemetry/observations{
"observations": [
{ "pointId": "…", "value": "21.4", "timestamp": "2026-08-04T09:12:00.000Z" },
{ "pointId": "…", "value": "on", "source": "bms-gateway" }
]
}valueпередаётся строкой;timestampпо умолчанию — текущий момент.- Каждый
pointIdдолжен уже существовать в этом здании — неизвестные идентификаторы валят весь запрос с400и перечисляются в ответе. Точки неявно не создаются. - Числовые значения попадают в базу временных рядов. Нечисловые (
"on","fault") как ряд не сохраняются: они обновляют последнее значение точки и порождают живое событие, но в исторической выборке их не будет. Нужна история статуса — кодируйте его числом. - Приём наблюдений порождает
building.point.updated— именно поэтому показания приезжают в интерфейс вживую и уходят в вебхуки.
Либо присылать их по MQTT
Если ваш мониторинг уже говорит по MQTT, tv-api может подписаться сам — вместо того чтобы вы слали POST. Публикуйте в нативный топик:
tv/{orgId}/{siteId}/{buildingId}/telemetryс тем же телом, что и у REST-вызова: {"observations": [...]}. Сообщения попадают ровно в тот же путь приёма, поэтому все правила выше сохраняются: точки должны существовать, нечисловые значения не сохраняются как ряд, building.point.updated по-прежнему порождается.
MQTT выключен, пока контур его не включит
Приём работает, только когда в окружении задано MQTT_ENABLED=true, и ему нужны креденшлы брокера — это часть развёртывания, а не то, что вы выпускаете сами. Уточните у нас, включён ли он в вашем контуре, прежде чем строить на нём: иначе вы публикуете в брокер, которого никто не слушает.
Есть и второй топик — rec/{deviceId}/observations для граничных устройств REC. Он разбирается, но пока не принимается: обработчик пишет сообщение в лог и останавливается. Не закладывайтесь на него — используйте нативный топик или REST.
Читать обратно: GET …/telemetry/observations?pointId=…&from=…&to=… (необязательные aggregation из avg|min|max|sum|count|last с interval вида 5m, limit до 10 000) либо снимок последних значений по всему зданию — GET …/telemetry/latest.
Какое право нужно
| Эндпоинт | Право | Роли, у которых оно есть |
|---|---|---|
| Чтение слоёв и значений | building.layers:read | user и выше |
| Создание слоёв, загрузка значений | building.layers:write | manager, building-graph-writer |
| Пакетный импорт элементов и точек | data-import.sync:read + data-import.sync:write | admin, manager, accountant |
| Приём и чтение телеметрии | по владению зданием | любой токен в области организации здания |
Для партнёрского креденшла запрашивайте роль manager — это единственная роль, у которой есть все права на запись из этого руководства. Вторая роль в строке про запись слоёв, building-graph-writer, — это внутренняя личность конвейера импорта IFC: она по замыслу обходит проверку принадлежности здания и внешней интеграции выдаваться не должна. См. Получение доступа.