Как подружить Ростелеком Ключ и Home Assistant

da343c9c676bc12c7506af35f0bb5d8b.png

Недавно ко мне в руки попал ключ для домофона «Ростелеком» непривычной прямоугольной формы. А вскоре после этого на моем телефоне появилось приложение «Ростелеком Ключ», которое позволило полностью перестать пользоваться физическим ключом. В этом приложении можно просматривать видео с различных камер вокруг дома и в подьезде, создавать временные коды для открытия дверей, а также открывать все двери дистанционно.

Мне сразу же захотелось добавить все эти функции в Home Assistant, который у меня уже управляет кучей устройств внутри квартиры. Ну, а интеграция с сервисами «Ростелекома» позволит расширить радиус контроля Home Asstatant’а и при желании реализовать новые интересные автоматизации, например, открывать дверь подъезда, просто внимательно посмотрев в камеру домофона, и в этот момент предупреждать детей в квартире, что папа через 5 минут будет дома =)

Изучаем приложение «Ростелеком Ключ»

Сайт предлагает скачать Android и iOS версии приложения «Ростелеком Ключ», однако неприметная кнопка «Войти» позволяет использовать его веб-версию.

Заходим с использованием телефона, привязанного к аккаунту «Ростелеком Ключ», далее переходим на https://key.rt.ru/main/pwa/dashboard и анализируем сетевые запросы, которые отправляет данное приложение.

Первым уходит запрос на получение списка камер. В нем присутствует Authorization Header, содержащий Bearer Token. Декодируем токен с помощью https://jwt.io/ и видим, что срок жизни токена составляет один год. Это значит, что мы смело можем сохранить данный токен и использовать его для последующих запросов к апи.

Ответ на запрос содержит полный список доступных камер, у которых есть следующий интересный аттрибут:

"screenshot_precise_url_template": "https://media‑vdk4.camera.rt.ru/image/precise/{size}/8f3a52fc‑aa67–48f6-aeaa‑fb1379e2c8f5/{timestamp}.jpg?token={cdn_token}"

Очевидно, что мы уже можем получать скриншот с каждой камеры, а вот как получить видеопоток, пока непонятно.

Далее нажимаем на просмотр видео с камеры и видим вот такой запрос: wss://live‑vdk4.camera.rt.ru/stream/8f3a52fc‑aa67–48f6-aeaa‑fb1379e2c8f5/1 706 625 123.mp4? mp4-fragment‑length=0.5&mp4-use‑speed=0&mp4-afiller=1

Видеопоток приходит порциями через вебсокет, что не очень удобно, и пока непонятно, как видео в таком формате можно добавить в Home Assistant. Я попробовал поискать информацию о стандартных протоколах передачи видео данных через вебсокеты, но не нашел никаких упоминаний о таких структурах данных:

406e04aeac9260399f397a657f7fcc3a.png

Похоже, что «Ростелеком» использует какую‑то собственную реализацию.

И тут в голову приходит идея —, а что если поменять wss на https? Пробуем, и такая ссылка работает. В новой вкладке браузера мы видим видео с нашей камеры в реальном времени!

Ну и наконец, пробуем открыть какую-нибудь дверь из приложения, и видим запрос https://household.key.rt.ru/api/v2/app/devices/23616/open с тем же самым Bearer Token. Пробуем повторить этот запрос с помощью утилиты curl, и дверь успешно открывается!

На этом исследовательская часть закончена — мы научились получать видеопотоки, скриншоты и открывать двери. Теперь остается добавить соответствующие сущности в Home Assistant.

Пишем интеграцию для Home Assistant

Те, кому важен результат, могут сразу посмотреть на получившуюся интеграцию тут. Далее я кратко опишу основные шаги для создания интеграции.

Создание интеграций неплохо описано в документации Home Assistant. Также в качестве источника для вдохновения удобно пользоваться исходниками дефолтных интеграций и официальными их примерами.

Создание интеграции начнем с файла manifest.json:

{
  "domain": "rtkey",
  "name": "rtkey",
  "documentation": "https://github.com/artgl/hass_rtkey",
  "iot_class": "cloud_polling",
  "version": "0.0.1",
  "requirements": ["transliterate", "pyjwt"],
  "config_flow": true
}

Обратите внимание на следующие атрибуты:

iot_class: "cloud_polling" говорит Home Assistant, что наша интеграция требует активного интернет-соединения, и обновления сущностей могут происходит с задержкой в зависимости от частоты опроса (polling).

requirements — дополнительные пакеты питона, которые использует наша интеграция.

config_flow: true — наша интеграция конфигурируется не с помощью файла конфигурации, а с помощью мастера кофигурации, который запускается из настроек Home Assistant.

Добавляем файл config_flow.py. Он содержит класс с единственной функцией async_step_user, которая вызывается, когда юзер нажимает кнопку «Добавить интеграцию» в настройках Home Assistant. Подробнее о config_flow можно прочитать тут.

Добавляем файл init.py. В самом его начале определим спискок классов сущностей, которые будет создавать наша интеграция, в переменной PLATFORMS. В текущем варианте создаются только сущности типа Image для скриншотов. В следущих версиях будут добавлены сущности Camera для видеопотоков и Button для действия «открыть дверь». Подробнее о классах сущностей можно почитать здесь. Для каждой платформы нужно будет добавить соотвествующий файл, в нашем случае это будет файл image.py

Далее в этом файле мы создадим две функции: async_setup_entry и async_unload_entry. Первая отвечает за создание экземпляра нашей интеграции при старте Home Assistant, вторая вызывается, когда пользователь решил удалить нашу интеграцию. Важный момент: некоторые интеграции работают только в единственном экземпляре и добавляют эту проверку в эту функцию. Но в нашем случае пользователь может иметь доступ к нескольким ключам «Ростелекома», и мы разрешим пользователю иметь несколько экземпляров нашей интеграции.

async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
  await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
  return True

В нашем случае вся инициализация произойдет внутри файла image.py при инициализации соответствующей платформы.

Переходим к файлу image.py. Здесь тоже нужна функция async_setup_entry, в которой мы получаем список доступных камер с помощью апи вызовов «Ростелекома» из первой части и создаем соответствующие сущности Home Assistant:

async def async_setup_entry(hass, config_entry, async_add_entities):
    cameras_api = RTKeyCamerasApi(hass, config_entry)

    cameras_info = await cameras_api.get_cameras_info()

    entities = []
    for camera_info in cameras_info["data"]["items"]:
        camera_id = camera_info["id"]
        entities.append(RTKeyCameraImageEntity(hass, config_entry, cameras_api, camera_id, camera_info))
    async_add_entities(entities)

Класс RTKeyCameraImageEntity является наследником класса Image и в минимальной реализации должен иметь только одну функцию, возвращающую картинку в бинарном виде:

    class RTKeyCameraImageEntity(ImageEntity):
    async def async_image(self) -> bytes | None:
        return self.cameras_api.get_camera_image(self.camera_id)

Моя реализация в github содержит чуть больше кода, который отвечает за кэширование и автоматическое обновление картинки, если пользователь смотрит на нее в настоящий момент.

© Habrahabr.ru