
Салют, Хабр!
Меня зовут Иван, я разработчик на Go. В SberDevices я занимаюсь реализацией интеграций — обеспечиваю возможность подключать устройства разных брендов к Умному дому Сбер. Он построен на микросервисной архитектуре. Сервисы, которые участвуют в интеграции с другими брендами умного дома, делятся на несколько ключевых блоков:
OAuth2.0 для связывания учетных записей пользователя в двух умных домах — Сбера и партнёрском;
управление сторонним устройством и получение обратной связи от него;
сервис-адаптер к партнёрской системе умного дома.
Сервис-адаптер — это промежуточный компонент между двумя умными домами, партнёрским и Сбер. Он принимает запросы в формате протокола Умного дома Сбер и преобразует их в API партнера или наоборот. Это stateless-микросервис (он не хранит бизнес-состояние устройства), поэтому адаптеры легко масштабируются. Мы называем такой способ интеграции cloud-to-cloud интеграцией.

Кажется, что всё просто: чтобы интегрировать два умных дома между собой, нужно узнать их протоколы — наборы типов устройств и их возможностей с определёнными форматами их описания — и на основе протокола создать адаптер. Но есть проблема: как правило, каждая модель устройства, например, чайника, уникальна. Производители по-разному кодируют одни и те же сущности — режимы, состояния, границы значений. Поэтому интеграцию не провести, просто обеспечив в партнёрском адаптере конвертацию управления типов устройств из JSON формата партнёра в Умный дом Сбер на уровне протокола. Нельзя написать универсальную функцию f:SberSH -> PartnerSH , которая будет выполнять преобразования всего множества устройств, ведь сложность задачи масштабируется вместе с их количеством. Но можно упростить и унифицировать процесс создания этих преобразований. Рассказываем, как сделали это в Умном доме Сбер.
Декомпозируя чайник
Перефразируя классику, стандартизация знает всё. Не знает только, почему у одного производителя в разных моделях по-разному включается чайник. Чтобы снизить сложность управления партнёрским устройством в нашем умном доме, мы придумали приводить каждое устройство к протоколо-независимому виду. Он не ограничен наборами типов или возможностей устройств — это скорее способ описания функций устройств и конвертация этих функций друг в друга. Словом, нужна полная декомпозиция управления.
Мы используем понятие датапоинт (datapoint); датапоинт — это минимальная атомарная функция управления или наблюдения. Датапоинты описаны так, что можно однозначно перевести протокол любого умного дома в их набор.
Для каждого устройства мы собираем все датапоинты — например, включено / выключено / текущая температура / целевая температура. В среднем у девайса около пяти датапоинтов.
Для каждого датапоинта определён набор параметров, который позволяет описать любую модель устройства:
название;
тип данных — число, текст, %, булева переменная;
существующие ограничения в рамках типа данных — например, диапазон для числового типа, доступные текстовые значения (enum);
режим доступа: read/write/read-write. То есть возможность получить из датапоинта данные, записать их туда, или и то, и другое.
/ DataPointDefinition - schema for a data point type DataPointDefinition struct { ID string `json:"id"` Name string `json:"name"` Description string `json:"description"` Type DataType `json:"type"` Unit string `json:"unit"` Constraints *Constraints `json:"constraints,omitempty"` Access AccessMode `json:"access"` Meta map[string]string `json:"meta,omitempty"`
Каждый производитель определяет собственные датапоинты — и не только для категории, но и для конкретных моделей. Они не обязательно совпадают с датапоинтами на нашей стороне. У одного чайника есть датапоинт on/off (с булевым типом данных). У другой модели его нет, но есть датапоинт «режим» с тремя числовыми значениями: 1, 2, 3. Узнать, что это означает, мы можем двумя способами: либо оперируя реальным устройством — то есть переключая режимы — либо из детальной документации. Иногда у партнёра её нет, а иногда она на китайском языке. Обычно приходится сочетать оба способа. В модели из примера выше оказалось, что режимы 1,2,3 – это «подогрев», «кипячение» и «ожидание» соответственно.
Чтобы сопоставить датапоинты «у нас» и «у них», мы определяем понятие оператора, который позволяет преобразовывать данные. Всего их более десятка: от and и or до int_to_float и merge, словом, большой диапазон возможных операций. Операторы композиционны — их можно объединять в цепочки любой глубины.
type Operator interface { Name() string InputType() DataType OutputType() DataType InputsCount() int OutputsCount() int Evaluate(ctx context.Context, inputs ...Value) ([]Value, error)
На схеме — простой пример: преобразование умной розетки партнёра (слева) в категорию socket Умного дома Сбер (справа). Все датапоинты совпадают по типам данных, потребовалось лишь сопоставить их названия.

Рассмотрим более сложный пример: увлажнитель. У него больше датапоинтов, и они не совпадают с нашими.
В данном случае одно устройство партнера конвертируется в два устройства нашего умного дома. Увлажнитель воздуха партнера умеет измерять температуру и влажность. С точки зрения Умного дома Сбер это два устройства: увлажнитель и датчик температуры.

Разберём логику преобразования подробно:
С первым датапоинтом всё просто: 2_1_Switch_status в партнёрском устройстве означает то же, что и on_off в Умном доме Сбер — включение/выключение устройства.
Однако датапоинт 2_5_Fan_level (strong, normal, low) уже не преобразуется в датапоинт hvac_air_flow_power (auto, quiet, low, medium, high, turbo) простым маппингом, так как режим auto задается в партнёрском устройстве через другой датапоинт – 2_8_Mode. Более того, 2_8_Mode может быть выставлен в режим night, что в Умном доме Сбер определяется датапоинтом hvac_night_mode. Эти нестыковки приводят к сложной композиции преобразований.
4_1_filter_life_level в партнёрском устройстве определяет оставшийся ресурс фильтра в процентном соотношении. Наиболее подходящий по смыслу датапоинт в Умном доме Сбер — hvac_replace_filter (булевый тип данных), который сообщает владельцу устройства, что пора заменить фильтр. Для преобразования используем оператор int_to_bool с пороговым значением 10, что соответствует следующей функции: True, если x ≥ 10; False, если x < 10 — и дополнительным параметром inverse, чтобы инвертировать исходящий результат. Получается, когда оставшийся ресурс падает ниже 10%, в Умном доме Сбер пользователь получит уведомление, что пора заменить фильтр.
3_1_Relative_humidity через оператор split передаётся в два датапоинта humidity, один из которых соответствует устройству «увлажнитель», а второй — устройству «датчик температуры и влажности». Значение температуры передаётся как есть в датапоинт temperature.
Выстроенная нами модель позволяет создавать такие мини-адаптеры для любого устройства.
Структура данных
Адаптер для устройства представляет собой ориентированный ациклический граф, где датапоинты и адаптеры выступают вершинами, а значения передаваемых данных — направленными связями между ними. Граф реконструируется и вычисляется при каждом запросе от одного умного дома к другому. При необходимости он может быть кэширован в памяти адаптера. В ходе валидации графа мы проверяем все пути на:
соответствие типов данных — две связные вершины должны иметь одинаковый тип данных;
количество исходящих и входящих связей;
отсутствие циклов.
Выходит, что каждая модель устройства — это два графа. Один обеспечивает передачу данных от нашего умного дома в умный дом партнёра, а другой — от умного дома партнёра в наш. Фактически мы получили двунаправленную модель трансформации: Сбер → партнёр и партнёр → Сбер.
Для модели адаптеров мы написали библиотеку, которая рассчитывает граф и итоговые значения датапоинтов, а также конвертирует датайпоинты в протокол Умного дома Сбер. Фактически всё, что требуется для создания адаптера к протоколу нового партнёра — определить, как представить описание устройства в формате датапоинтов. Это легко сделать, зная протокол умного дома партнёра. Именно он определяет, как описаны возможности устройств, а эти устройства и конвертируются в набор датапоинтов.
Если конвертировать протокол партнёра в протокол Умного дома Сбер напрямую, сложность разработки будет расти вместе с числом моделей и их особенностей. Но модель datapoints + operators меняет сам характер задачи: достаточно один раз определить универсальный способ описания устройства, и создание адаптера сводится к конфигурации, а не к разработке с нуля. Сложность задачи становится управляемой: описание — единым, а инструменты построения адаптеров — повторно используемыми. Фронтенд модели позволяет нам довольно легко создавать эти гибкие адаптеры в формате low code. Требуется лишь десериализовать описание графа, далее его можно загружать в сервис адаптера.
Заключение
Зачастую интеграции между двумя умными домами — это высокоуровневый реверс-инжиниринг. Мы видим, как производитель «под капотом» реализовал функции своих умных устройств, и подстраиваемся под его решение. Сейчас, наблюдая за рынком, мы видим, что стандарты становятся универсальнее. Но пока каждая модель чайника уникальна, система датапоинтов позволяет нам легко решать задачу интеграции каждого нового умного дома.
