Skip to content

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

Два вида входящих данных с двумя разными контрактами:

Что у вас естьЧто использоватьКлюч
Одно текущее значение на помещение — статус аренды, число открытых заявок, CO₂, статус уборкиСлои данныхИдентификатор помещения
Поток показаний во времени — давление, температура, энергопотреблениеТелеметрия (observations)Точка на единице оборудования

Заявки — третий случай, у него своя страница: В обе стороны.

Всё ниже — обычный HTTP к tv-api. Базовый URL в облачном контуре: https://tv-api.k8s.tangovision.dev. В каждом запросе — Authorization: Bearer <token>, см. Получение доступа.

Слои данных

Слой данных — это одно значение на узел графа (сегодня — на помещение). Он отрисовывается и на 2D-плане, и на BIM-модели из одного источника.

1. Создать слой

http
POST /api/v1/buildings/{buildingId}/data-layers
json
{
  "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. Загрузить значения

http
PUT /api/v1/buildings/{buildingId}/data-layers/{layerId}/values
json
{
  "keyBy": "externalId",
  "replace": false,
  "values": {
    "3lPQxG$0v9zRZ1mB7kYtE2": 4,
    "1aBcDe$FgHiJkLmNoPqRs": 0
  }
}
ПолеЗначение
keyByexternalId (по умолчанию) — ключи это IFC GlobalId. id — ключи это внутренние UUID помещений. См. Идентификаторы.
replacefalse (по умолчанию) обновляет только те помещения, что есть в payload. true сначала удаляет все значения слоя, то есть payload становится полным состоянием.
valuesОбъект идентификатор → значение. Не массив: произвольные ключи обязаны лежать внутри одного свойства, потому что API отвергает неизвестные поля верхнего уровня.

Типы значений проверяются по каждой записи до записи в базу: NUMBER ждёт конечное JSON-число, BOOLEAN — булево, STRING и ENUM — строку. Несовпадение валит весь запрос с 400 и называет проблемный ключ: загрузка атомарна, наполовину она не применяется.

Ответ говорит, что реально записалось:

json
{ "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. Прочитать значения обратно

http
GET /api/v1/buildings/{buildingId}/data-layers/{layerId}/values?gte=3&limit=100
json
{
  "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. Мы наступили на это вживую и правили при заказчике.

Два выхода, оба нормальные:

  1. Писать значения на языке интерфейса"Занято", "Свободно". Просто и правильно, пока хватает одного языка.

  2. Передать явную 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.

Телеметрия: показания во времени

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

здание → этаж → помещение → элемент (приточная установка) → точка (температура приточного воздуха)

Поэтому шага два: убедиться, что точка существует, и слать в неё значения.

Создать точку один раз

http
POST /api/v1/buildings/{buildingId}/points
json
{
  "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, даже если он корректно ограничен нужным зданием, — см. таблицу прав ниже.

Отправлять наблюдения

http
POST /api/v1/buildings/{buildingId}/telemetry/observations
json
{
  "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:readuser и выше
Создание слоёв, загрузка значенийbuilding.layers:writemanager, building-graph-writer
Пакетный импорт элементов и точекdata-import.sync:read + data-import.sync:writeadmin, manager, accountant
Приём и чтение телеметриипо владению зданиемлюбой токен в области организации здания

Для партнёрского креденшла запрашивайте роль manager — это единственная роль, у которой есть все права на запись из этого руководства. Вторая роль в строке про запись слоёв, building-graph-writer, — это внутренняя личность конвейера импорта IFC: она по замыслу обходит проверку принадлежности здания и внешней интеграции выдаваться не должна. См. Получение доступа.

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