Привет, Хабр! Сегодня я расскажу о своем опыте автоматизации умного дома, а именно о выводе изображения PiP с камеры на тв.

Пример вывода изображений с камер
Пример вывода изображений с камер

Обычно ОС (webOS, Tizen и тд) тв закрыта и не предполагает кого‑либо внешнего управления, например, включение\выключение или вывод картинки в картинке. Но на Android TV с этим проще — можно запустить любое приложение, а наделив его правами SYSTEM_ALERT_WINDOW, вывести его поверх любых других (в том числе видео плеера или изображения с HDMI), этим и воспользуемся.

Дано два телевизора: Xiaomi Q1 на Android TV к нему подключены Nvidia Shield и Xbox и LG TV на webOS с подключенным к нему FireTV Stick — цель вывести видео с камер при звонке в дверь. Умный дом управляется через Home Assistant.

Настройка андроид тв\приставки

Приложение для вывода потока с камеры нужно устанавливать именно на ТВ, а не на тв приставку, так как независимо от подключенного внешнего источника изображения (тв приставка или игровая консоль и тп), всплывающее изображение будет выведено независимо и поверх любого источника. Но если тв приставка это единственный источник сигнала, то можно и на нее.

Для начала на Android TV необходимо выдать права разработчика, для этого в настройках ОС, нужно несколько раз кликнуть по Номеру сборки, а после этого в параметрах разработчика нужно включить Отладку и тут же, не отходя от кассы, можно телевизор\приставку добавить в HomeAssistant через интеграцию Android Debug Bridge.

Добавление Android TV в Home Assistant
Добавление устройства в HA
Добавление устройства в HA
Запрос на отладку
Запрос на отладку
все домашние Андроид ТВ добавлены
все домашние Андроид ТВ добавлены

Теперь скачиваем APK приложение PiPup и устанавливаем на ТВ командами:

adb connect YOUR_ANDROID_TV_IP_ADDRESS

adb install app-debug.apk

adb shell appops set nl.rogro82.pipup SYSTEM_ALERT_WINDOW allow

Команды ADB можно вызывать прямо из Home Assistant

Приложение PiPup открывает на устройстве веб порт 7979и принимает POST JSON данные, которые необходимо вывести на экране тв, например:

{
  "duration": 30,
  "position": 0,
  "title": "Your awesome title",
  "titleColor": "#0066cc",
  "titleSize": 20,
  "message": "What ever you want to say... do it here...",
  "messageColor": "#000000",
  "messageSize": 14,
  "backgroundColor": "#ffffff",
  "media": { "image": {
    "uri": "https://mir-s3-cdn-cf.behance.net/project_modules/max_1200/cfcc3137009463.5731d08bd66a1.png", "width": 480
  }}
}

У media: "video" есть недостаток, по крайней мере на Amazon FireTV, проигрываемое основное видео на тв приставке автоматически ставится на паузу, поэтому я вообще решил не использовать такой тип медиа.

Проверить можно так:

curl -d "@post.json" -H "Content-Type: application/json" -X POST http://ANDROID_TV_IP_ADDRESS:7979/notify

Настройка умного дома

Веб запросы в HA вызываются через интеграцию rest_command, создадим удобные сервисы (для отправки уведомления в виде текста, изображения и веб содержимого), для этого нужно сохранить код ниже в файле notification_on_tv.yaml по пути /homeassistant/rest_commands

pipup_text_on_tv:
  url: http://{{ ip }}:{{ port | default(7979) }}/notify
  content_type: 'application/json'
  verify_ssl: false
  method: 'post'
  timeout: 20
  payload: >
    {
      "duration": {{ duration | default(20) }},
      "position": {{ position | default(0) }},
      "title": "{{ title | default('') }}",
      "titleColor": "{{ titleColor | default('#50BFF2') }}",
      "titleSize": {{ titleSize | default(12) }},
      "message": "{{ message }}",
      "messageColor": "{{ messageColor | default('#fbf5f5') }}",
      "messageSize": {{ messageSize | default(14) }},
      "backgroundColor": "{{ backgroundColor | default('#0f0e0e') }}"
    }

pipup_image_on_tv:
  url: http://{{ ip }}:{{ port | default(7979) }}/notify
  content_type: 'application/json'
  verify_ssl: false
  method: 'post'
  timeout: 20
  payload: >
    {
      "duration": {{ duration | default(20) }},
      "position": {{ position | default(0) }},
      "title": "{{ title | default('') }}",
      "titleColor": "{{ titleColor | default('#50BFF2') }}",
      "titleSize": {{ titleSize | default(10) }},
      "message": "{{ message }}",
      "messageColor": "{{ messageColor | default('#fbf5f5') }}",
      "messageSize": {{ messageSize | default(14) }},
      "backgroundColor": "{{ backgroundColor | default('#0f0e0e') }}",
      "media": { 
        "image": {
          "uri": "{{ url }}",
          "width": {{ width | default(640) }}
        }
      }
    }
 
pipup_url_on_tv:
  url: http://{{ ip }}:{{ port | default(7979) }}/notify
  content_type: 'application/json'
  verify_ssl: false
  method: 'post'
  timeout: 20
  payload: >
    {
      "duration": {{ duration | default(20) }},
      "position": {{ position | default(0) }},
      "title": "{{ title | default('') }}",
      "titleColor": "{{ titleColor | default('#50BFF2') }}",
      "titleSize": {{ titleSize | default(10) }},
      "message": "{{ message }}",
      "messageColor": "{{ messageColor | default('#fbf5f5') }}",
      "messageSize": {{ messageSize | default(14) }},
      "backgroundColor": "{{ backgroundColor | default('#0f0e0e') }}",
      "media": { 
        "web": {
          "uri": "{{ url }}", 
          "width": {{ width | default(640) }},
          "height": {{ height | default(480) }}
        }
      }
    }

и прописать этот файл в основном файле конфигурации HA /homeassistant/configuration.yaml

rest_command: !include_dir_merge_named rest_commands

После этого нужно перезагрузить HA и мы сможем вызывать наши новые сервисы для вывода текста, изображений или веб содержимого, например:

service: rest_command.pipup_image_on_tv
data:
  title: Привет
  message: Хабр
  ip: 192.168.0.94
  titleColor: red
  position: 0
  url: https://mir-s3-cdn-cf.behance.net/project_modules/max_1200/cfcc3137009463.5731d08bd66a1.png

Вывод live видео с камер

Для вывода потока с подключенных камер HA использует внутренний проксирующий сервис на основе ffmpeg, который отдает фронтенду поток в виде «бесконечной» картинки с заголовком multipart/x-mixed-replace; boundary=--frameboundary ссылку на поток можно получить через атрибуты устройства камера - entity_picture, который (к нашей радости) содержит также одноразовый токен доступа, это, в свою очередь, дает доступ к картинке без авторизации, пример:

http://HA_IP:8123/api/camera_proxy/camera.doorbell_repeater_b8b7?token=fd31891d4e34b6a45bf26fbba48af42f71f6abd0d797994fe529c280536e5e78

Атрибуты подключенных камер

Поток с (почти) любой IP камеры можно завести в HomeAssistant через интеграцию https://github.com/AlexxIT/go2rtc#go2rtc-home-assistant-integration

К сожалению такой поток «бесконечной» картинки нельзя просто так отдать в наш новый сервис pipup_image_on_tv — ничего не выведется (компонента не умеет работать с такими картинками). Так же не сработает если отдать в pipup_url_on_tv, нужна обертка в виде простой веб страницы с этой картинкой.

HA умеет хранить и отдавать статичные пользовательские ресурсы, которые лежат по пути /homeassistant/www, создадим там страницу с выводом картинки с камеры, например, /homeassistant/www/fullscreen/my_fullscreen_panel.html

<!DOCTYPE html>
<html>
<head>
    <title>Fullscreen Panel</title>
    <style>
       body, html {
            margin: 0;
            padding: 0;
            width: 100%;
            height: 100%;
            overflow: hidden;
        }
    </style>
</head>
<body>
    <img id="myImage" src="" alt="stream">
<script>
    async function getState(entity_id) {
        const response = await fetch('/api/states/' + entity_id, {
            headers: {
                'Authorization': 'Bearer СЕКРЕТНЫЙ_КОД',
                'Content-Type': 'application/json'
            }
        });
        if (response.ok) {
            const data = await response.json();
            return data.attributes;
        } else {
            console.error('Failed to fetch state');
            return null;
        }
    }

    document.addEventListener("DOMContentLoaded", async function() {
        const urlParams = new URLSearchParams(window.location.search); 
        const entity_id = urlParams.get('camera_entity');
        const attributes = await getState(entity_id);
        if (attributes) {
            const img_stream_url = attributes.entity_picture.replace('camera_proxy', 'camera_proxy_stream');
            document.getElementById('myImage').src = img_stream_url;
        }
    });
</script>
</body>
</html>

ID камеры на страницу будем передать через параметр camera_entity, а url к картинке (с токеном) будем получать через встроенный в HA REST сервис /api/states/, для доступа к нему в HA нужно получить постоянный авторизационный токен, тут http://HA_IP:8123/profile/security

И прописать его в HTML тут (осторожно: этот ключ дает полный доступ к HA, но для внутренней сети это нестрашно)

const response = await fetch('/api/states/' + entity_id, {
            headers: {
                'Authorization': 'Bearer СЕКРЕТНЫЙ_КОД',
                'Content-Type': 'application/json'
            }
        });

Еще, атрибут camera_entity содержит только статичную картинку, поэтому в пути нужно заменить camera_proxy на camera_proxy_stream, что уже сделано у меня в коде.

Теперь в браузере на PC, вызвав http://HA_IP:8123/local/fullscreen/my_fullscreen_panel.html?camera_entity=camera.doorbell_repeater_b8b7 должна появится картинка с камеры на весь экран.

Автоматизации

Создадим красивый скрипт обертку, для вызова статичной страницы с передачей ID камеры и текста, здесь у меня (при вызове скрипта) параллельно выводится изображение с камер на два домашних тв (192.168.0.215, 192.168.0.94) используем сервис pipup_url_on_tv.

alias: Показать live с камеры с переданным ID и текстом
sequence:
  - parallel:
      - service: rest_command.pipup_url_on_tv
        metadata: {}
        data:
          ip: 192.168.0.215
          title: "{{ title }}"
          url: >-
            http://HA_IP:8123/local/fullscreen/my_fullscreen_panel.html?camera_entity={{
            camera_entity }}&nocache={{ range(1, 51) | random }}
          width: 320
          height: 200
      - service: rest_command.pipup_url_on_tv
        metadata: {}
        data:
          ip: 192.168.0.94
          title: "{{ title }}"
          url: >-
            http://HA_IP:8123/local/fullscreen/my_fullscreen_panel.html?camera_entity={{
            camera_entity }}&nocache={{ range(1, 51) | random }}
          width: 320
          height: 200
description: ""
icon: mdi:camera
fields:
  camera_entity:
    selector:
      entity: {}
    name: Сущность камеры
    required: true
  title:
    selector:
      text: null
    name: Сообщение

Также, на всякий случай, я создал автоматизацию, которая запускает сервис PiPup на тв (при переходе из спящего режима), если вдруг телевизор решит его прибить:

alias: Перезапуск сервиса картинка в картинке на XiaomiTV, если тв прибьет его
description: ""
trigger:
  - platform: device
    device_id: ID ANDROID TV
    domain: media_player
    entity_id: Player ID
    type: idle
condition: []
action:
  - service: androidtv.adb_command
    target:
      device_id: ID ANDROID TV
    data:
      command: >-
        ps -ef | grep -v grep | grep pipup || shell am start
        --activity-task-on-home -n nl.rogro82.pipup/.MainActivity
mode: single

Ну и, как пример, основная автоматизация, когда курьер принес заказ:

alias: Курьер пришел
description: ""
trigger:
  - platform: state
    entity_id:
      - sensor.smartintercom_line_status
    to: Открытие двери
condition:
  - condition: state
    entity_id: switch.smartintercom_delivery
    state: "on"
action:
  - service: script.alice_play_text
    data:
      message: Кажется курьер пришел, открыла дверь
  - wait_for_trigger:
      - platform: state
        entity_id:
          - binary_sensor.camera_hub_g2h_e718_motion_sensor
        to: "on"
    timeout:
      hours: 0
      minutes: 4
      seconds: 0
      milliseconds: 0
    continue_on_timeout: false
  - service: script.alice_play_text
    data:
      message: Курьер у входной двери
  - service: script.live_stream
    metadata: {}
    data:
      camera_entity: camera.camera_hub_g2h_e718_camera_stream_management0
      title: Курьер у входной двери
mode: single

И автоматизация при звонке на дверной замок: включение экрана планшета и трансляция видео на тв:

alias: Включение экрана планшета при звонке и показ live с камеры звонка
description: ""
trigger:
  - platform: device
    device_id: bba92ec78885fa381662ae945f2ee231
    domain: homekit_controller
    type: doorbell
    subtype: single_press
condition: []
action:
  - service: script.door_tablet_screen_on
    metadata: {}
    data: {}
  - service: script.live_stream
    metadata: {}
    data:
      camera_entity: camera.doorbell_repeater_b8b7
      title: Звонят в дверь
mode: single

Мира!

Комментарии (9)


  1. kuzmingor
    22.07.2024 08:12

    классный пример как инновационный подход может дать новую жизнь старым идеям


  1. booroondookZ
    22.07.2024 08:12

    Сделал все по инструкции. На экран ТВ вместо картинки с камеры выводит белый прямоугольник. Текст заголовка и рамку вокруг квадрата рисует правильно. При этом при тестировании в браузере картинка с камеры выводится нормально.


    1. foxyrus Автор
      22.07.2024 08:12

      Какая версия Android TV? WebView?

      А простая статическая картинка открывается?

      Путь к картинке через IP или доменное имя?


      1. booroondookZ
        22.07.2024 08:12

        Андроид 7.1 - это спутниковый ресивер Openbox AS4K CI Pro.
        Все тестовые запросы, приведенные в статье, выводятся нормально. В частности, картинка с тестовой ТВ-таблицей. Запрос в браузере типа
        http://<ip-address>:8123/local/fullscreen/my_fullscreen_panel.html?camera_entity=<camera_entity>
        также выводится нормально.
        Проблема только при отработке конечного скрипта.
        Но если в скрипте заменить
        http://<ip-address>:8123/local/fullscreen/my_fullscreen_panel.html?camera_entity={{camera_entity}}&nocache={{ range(1, 51) | random }}
        на
        https://mir-s3-cdn-cf.behance.net/project_modules/max_1200/cfcc3137009463.5731d08bd66a1.png
        ...то тестовая картинка выводится нормально.

        Если вызвать скрипт, указав в качестве объекта не камеру, а объект ХА типа "image" , то также выводится белый экран


        1. foxyrus Автор
          22.07.2024 08:12

          Откройте на Openbox в браузере http://<ip-address>:8123/local/fullscreen/my_fullscreen_panel.html?camera_entity=<camera_entity> будет стрим картинка? если нет, наверно старый Андроид 7 не поддерживает в WebView такой поток


          1. foxyrus Автор
            22.07.2024 08:12

            Если это так, то создайте поток с камеры в "формате" WebRTC  через сервис webrtc.create_link и интеграцию другую WebRTC

            Hidden text
            sequence:
              - service: webrtc.create_link
                data:
                  link_id: '{{ link_id }}'
                  entity: camera.entity_id
                  open_limit: 1
                  time_to_live: 60
              - service: rest_command.pipup_url_on_tv
                data:
                  title: test
                  message: test
                  width: 640
                  height: 480
                  url: HA_IP/webrtc/embed?url={{ link_id }}&webrtc=false


            1. booroondookZ
              22.07.2024 08:12

              Пробовал такие потоки (и не потоки):
              - поток через интеграцию ONVIF
              - поток через интеграцию Frigate
              - статическую картинку через интеграцию Frigate
              Во всех случаях белый квадрат.


          1. booroondookZ
            22.07.2024 08:12
            +1

            Вопреки прогнозам, в браузере показывает отлично!

            P.S. Ура! Всё заработало. Оказывается, проблема была в том, что на этом медиаплеере (спутниковом ресивере, то есть) не был установлен браузер )(вообще никакой). После установки Chrome из Маркета всё заработало и в браузере, и в приложении.


  1. stanislavskijvlad
    22.07.2024 08:12
    +1

    Завтра новый астро-обзор, кстати.