В этой статье я расскажу про написание скрипта HA, который продолжает воспроизведение видео запущенного с Plex на другом устройстве: выключает источник, включает устройство назначения, включает Plex, включает текущее видео, перематывает видео в нужную позицию, устанавливает необходимую громкость.

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

В статье будет рассмотрено использование переменных (variables), триггеров (wait triggers), циклов (loops), условий (conditions), шаблонов значений (value templates).

Вот так выглядит скрипт в визуальном редакторе
Вот так выглядит скрипт в визуальном редакторе

А теперь давайте посмотрим каждое действие по отдельности.

1. Определение переменных target, source, plextarget и volume

Эти переменные будут использоваться в скрипте:

target - телевизор назначение

source - телевизор источник

plextarget - медиаплеер Plex назначение. Каждый клиент plex регистрируется в Home Assistant как отдельный device - mediaplayer. Управление контентом и позицией возможно только через него, так как сам телевизор знает только какой запущен источник (hdmi или приложение) и статус (idle, playing, paused) и не понимает, что конкретно на нем проигрывается и какая текущая позиция.

volume - текущая громкость на источнике. Так как в фильмах используется разный уровень аудиодорожек, то не хочется руками регулировать громкость на ТВ назначении, а тем более чтобы он заорал на всю квартиру, если там был установлен высокий уровень.

Сначала нужно определить откуда и куда необходимо перенести просмотр фильма. Так как я автоматизировал только для сценария перехода между двумя комнатами (кухней и спальней), то предполагаем что один ТВ в момент запуска скрипта включен, другой выключен (можно реализовать и более сложные проверки, например, что включен источник plex, телевизор в статусе playing и т.д.).

Соответственно если ТВ спальня включен, то target = ТВ кухня, иначе target = ТВ спальня. Аналогично поступаем с source, plextarget и volume.

Для получения значения entity необходимо воспользоваться states, параметром с который передается entity, а он возвращает его значение.

Код
variables:
  target: >-
    {{ 'media_player.lg_webos_kitchen' if
    (states('media_player.lg_webos_bedroom') == 'on') else
    'media_player.lg_webos_bedroom' }}
  source: >-
    {{ 'media_player.lg_webos_bedroom' if
    (states('media_player.lg_webos_bedroom') == 'on') else
    'media_player.lg_webos_kitchen' }}
  plextarget: >-
    {{ 'media_player.plex_plex_for_lg_lg_65up751c0zf' if
    (states('media_player.lg_webos_bedroom') == 'on') else
    'media_player.plex_plex_for_lg_lg_oled65c1rla' }}
  volume: >-
    {{ state_attr('media_player.lg_webos_bedroom', 'volume_level') if
    (states('media_player.lg_webos_bedroom') == 'on') else
    state_attr('media_player.lg_webos_kitchen', 'volume_level') }}

2. Поставить Plex на паузу

Придется немного забежать вперед, чтобы объяснить зачем ставить Plex на паузу на источнике. Как выяснилось, Plex обновляет атрибут media_position с большим интервалом, а значит узнать точную текущую позицию нельзя, но оказалось, что позиция обновляется при постановке на паузу.

Код
action: media_player.media_pause
metadata: {}
data: {}
target:
  entity_id: "{{ source }}"
alias: Поставить Plex на паузу, чтобы обновилась media_position

3. Дождаться пока Plex станет на паузу

Поскольку в wait trigger нельзя использовать template entity, то я не смог использовать переменную source. А так как один из медиаплеер все равно в данный момент находится в статусе unavailable (напоминаю, что он выключен), то просто добавляем оба в entity_id (далее по этой причине в wait trigger везде будут добавляться и источник и назначение) и при изменении статуса любого на paused продолжаем выполнение скрипта.

Код
wait_for_trigger:
  - trigger: state
    entity_id:
      - media_player.plex_plex_for_lg_lg_65up751c0zf
      - media_player.plex_plex_for_lg_lg_oled65c1rla
    to: paused
    for:
      hours: 0
      minutes: 0
      seconds: 1
timeout:
  hours: 0
  minutes: 0
  seconds: 10
  milliseconds: 0

4. Определить переменные media_content_id и media_position

В интерфейсе HA идем в Developer tools\States, находим mediaplayer Plex и смотрим какие там есть атрибуты:

Entity

State

Attributes

media_player.plex_plex_for_lg_lg_oled65c1rla

Plex (Plex for LG - LG OLED65C1RLA)

playing

friendly_name: Plex (Plex for LG - LG OLED65C1RLA)
supported_features: 152127
media_content_id: 1658
media_content_type: movie
media_duration: 8337
media_position: 666
media_position_updated_at: 2025-01-06T10:15:29.607078+00:00
media_title: Анора (2024)
media_content_rating: R
media_library_title: Фильмы
player_source: PMS
media_summary: Молодая стриптизёрша из Бруклина внезапно выходит замуж за сына русского олигарха. Узнав об этом, родители парня тотчас вылетают из Москвы в Нью-Йорк. Они готовы на всё, лишь бы расстроить брак. username: tmv002
entity_picture: /api/media_player_proxy/media_player.plex_plex_for_lg_lg_oled65c1rla?token=ae6b1a44a1007aed0da3e35a96895486bcd4da6b4f49a58b13c02ac788dba228&cache=8fbb4c41247a4811 volume_level: 1
is_volume_muted: false

Из полезного видим media_content_id и media_position. Проверяем и убеждаемся, что в этих атрибутах содержится id видео и текущая позиция в секундах.

Сохраняем эти значения в соответствующие переменные, они нам еще пригодятся. Для этого нам понадобится if, который мы использовали ранее и state_attr. Если у entity, кроме state еще есть атрибуты, то их значения можно получить с помощью state_attr, первым параметром в него передается entity, вторым имя атрибута.

Код
variables:
  media_content_id: >-
    {{ state_attr('media_player.plex_plex_for_lg_lg_oled65c1rla',
    'media_content_id') if (states('media_player.lg_webos_bedroom') == 'on')
    else state_attr('media_player.plex_plex_for_lg_lg_65up751c0zf',
    'media_content_id') }}
  media_position: >-
    {{ state_attr('media_player.plex_plex_for_lg_lg_oled65c1rla',
    'media_position') if (states('media_player.lg_webos_bedroom') == 'on') else
    state_attr('media_player.plex_plex_for_lg_lg_65up751c0zf', 'media_position')
    }}

5. Выключить ТВ источник

Код
action: media_player.turn_off
metadata: {}
data: {}
target:
  entity_id: "{{ source }}"
alias: Выключить ТВ источник

6. Включить ТВ назначение

Код
action: media_player.turn_on
metadata: {}
data: {}
target:
  entity_id: "{{ target }}"
alias: Включить ТВ назначение

7. Ждать пока ТВ не включится

Код
wait_for_trigger:
  - trigger: state
    entity_id:
      - media_player.lg_webos_kitchen
      - media_player.lg_webos_bedroom
    from: "off"
    to: "on"
    for:
      hours: 0
      minutes: 0
      seconds: 2
timeout:
  hours: 0
  minutes: 1
  seconds: 0
  milliseconds: 0
alias: Ждать пока ТВ не включится

8. Убавить звук до 0

Код
action: media_player.volume_set
metadata: {}
data:
  volume_level: 0
target:
  entity_id: "{{ target }}"
alias: Убавить громкость до 0

9. Запустить приложение Plex на ТВ

Код
action: media_player.select_source
metadata: {}
data:
  source: Plex
target:
  entity_id: "{{ target }}"
alias: Включить источник Plex

10. Определить переменную timestamp

Получаем ее с помощью now().timestamp() и преобразовываем в int. Объясню ее назначение в следующем пункте.

Код
variables:
  timestamp: "{{ (now().timestamp() | int) }}"

11. Сканировать клиентов Plex

Как выяснилось, mediaplayer в HA остается очень долгое время в статусе unavailable после запуска Plex на ТВ. В документации на интеграцию не нашел никакой информации, что с этим можно было бы сделать. Но если нет документации, то идем методом научного тыка. Интеграция Plex кроме медиаплееров создает еще одно устройство с несколькими сенсорами и одной кнопкой "Scan clients". Пробуем ее нажимать и видим, что это существенно ускоряет переход клиента в статус idle (запущен, но ничего не проигрывается). В этом статусе можно управлять клиентом.

Действие выглядит предельно простым: нажимать scan clients каждые 5 секунд, пока один из медиаплееров (кухня, спальня) не перейдет в состояние idle. Но чтобы скрипт не завис хочется ожидание статуса idle ограничить одной минутой. Иначе я устану ждать включу фильм сам и вдруг в какой-то момент скрипт начнет управлять медиаплеером, когда это уже не нужно.

Казалось бы элементарная задача, объявить переменную, после каждой итерации делать инкремент и на 12 раз (цикл длится 5 секунд) остановить цикл, однако в HA область действия переменных не позволяет это сделать. Можно почитать подробнее здесь.

Поэтому воспользуемся созданной в предыдущем пункте переменной timestamp и будем ждать пока текущее время в секундах не станет больше timestamp на 60 или пока один из медиаплееров не перейдет в статус idle.

Код
alias: Сканировать клиентов Plex каждые 5 секунд, пока не найдется клиент
repeat:
  sequence:
    - delay:
        hours: 0
        minutes: 0
        seconds: 5
        milliseconds: 0
    - action: button.press
      metadata: {}
      data: {}
      target:
        entity_id: button.media_scan_clients
  while:
    - condition: and
      conditions:
        - condition: template
          value_template: "{{ (now().timestamp() | int) - timestamp < 60 }}"
        - condition: not
          conditions:
            - condition: or
              conditions:
                - condition: state
                  entity_id: media_player.plex_plex_for_lg_lg_oled65c1rla
                  state: idle
                - condition: state
                  entity_id: media_player.plex_plex_for_lg_lg_65up751c0zf
                  state: idle
enabled: true

12. Включить фильм на Plex

Для этого воспользуемся переменной media_content_id

Код
action: media_player.play_media
metadata: {}
data:
  media_content_id: "{{ media_content_id }}"
  media_content_type: movie
target:
  entity_id: "{{ plextarget }}"
alias: Включить фильм на Plex

13. Ждать пока статус Plex не станет Playing

Пару секунд фильм успеет проиграться пока встанет на паузу, на случай если там громкая заставка мы ранее убавили звук на ТВ на 0.

Код
alias: Ждать пока статус Plex не станет Playing
wait_for_trigger:
  - trigger: state
    entity_id:
      - media_player.plex_plex_for_lg_lg_oled65c1rla
      - media_player.plex_plex_for_lg_lg_65up751c0zf
    to: playing
    for:
      hours: 0
      minutes: 0
      seconds: 1
timeout:
  hours: 0
  minutes: 1
  seconds: 0
  milliseconds: 0

14. Поставить на паузу

Код
action: media_player.media_pause
metadata: {}
data: {}
target:
  entity_id: "{{ target }}"
alias: Поставить на паузу

15. Ждать пока не станет на паузу

Код
wait_for_trigger:
  - trigger: state
    entity_id:
      - media_player.lg_webos_kitchen
      - media_player.lg_webos_bedroom
    to: paused
    for:
      hours: 0
      minutes: 0
      seconds: 1
timeout:
  hours: 0
  minutes: 0
  seconds: 10
  milliseconds: 0
alias: Ждать пока не станет на паузу

16. Перемотать фильм

Воспользуемся переменной media_position чтобы перемотать фильм в нужное место

Код
action: media_player.media_seek
metadata: {}
data:
  seek_position: "{{ media_position }}"
target:
  entity_id: "{{ plextarget }}"
alias: Перемотать фильм в нужное место

17. Установить уровень громкости

Воспользуемся переменной volume, чтобы установить уровень громкости.

Код
action: media_player.volume_set
metadata: {}
data:
  volume_level: "{{ volume }}"
target:
  entity_id: "{{ target }}"
alias: Установить уровень громкости как на источнике

Ну и на последок полный текст скрипта.

Полный текст скрипта
alias: plex_transfer_movie_position
sequence:
  - variables:
      target: >-
        {{ 'media_player.lg_webos_kitchen' if
        (states('media_player.lg_webos_bedroom') == 'on') else
        'media_player.lg_webos_bedroom' }}
      source: >-
        {{ 'media_player.lg_webos_bedroom' if
        (states('media_player.lg_webos_bedroom') == 'on') else
        'media_player.lg_webos_kitchen' }}
      plextarget: >-
        {{ 'media_player.plex_plex_for_lg_lg_65up751c0zf' if
        (states('media_player.lg_webos_bedroom') == 'on') else
        'media_player.plex_plex_for_lg_lg_oled65c1rla' }}
      volume: >-
        {{ state_attr('media_player.lg_webos_bedroom', 'volume_level') if
        (states('media_player.lg_webos_bedroom') == 'on') else
        state_attr('media_player.lg_webos_kitchen', 'volume_level') }}
  - action: media_player.media_pause
    metadata: {}
    data: {}
    target:
      entity_id: "{{ source }}"
    alias: Поставить Plex на паузу, чтобы обновилась media_position
  - wait_for_trigger:
      - trigger: state
        entity_id:
          - media_player.plex_plex_for_lg_lg_65up751c0zf
          - media_player.plex_plex_for_lg_lg_oled65c1rla
        to: paused
        for:
          hours: 0
          minutes: 0
          seconds: 1
    timeout:
      hours: 0
      minutes: 0
      seconds: 10
      milliseconds: 0
  - variables:
      media_content_id: >-
        {{ state_attr('media_player.plex_plex_for_lg_lg_oled65c1rla',
        'media_content_id') if (states('media_player.lg_webos_bedroom') == 'on')
        else state_attr('media_player.plex_plex_for_lg_lg_65up751c0zf',
        'media_content_id') }}
      media_position: >-
        {{ state_attr('media_player.plex_plex_for_lg_lg_oled65c1rla',
        'media_position') if (states('media_player.lg_webos_bedroom') == 'on')
        else state_attr('media_player.plex_plex_for_lg_lg_65up751c0zf',
        'media_position') }}
  - action: media_player.turn_off
    metadata: {}
    data: {}
    target:
      entity_id: "{{ source }}"
    alias: Выключить ТВ источник
  - action: media_player.turn_on
    metadata: {}
    data: {}
    target:
      entity_id: "{{ target }}"
    alias: Включить ТВ назначение
  - wait_for_trigger:
      - trigger: state
        entity_id:
          - media_player.lg_webos_kitchen
          - media_player.lg_webos_bedroom
        from: "off"
        to: "on"
        for:
          hours: 0
          minutes: 0
          seconds: 2
    timeout:
      hours: 0
      minutes: 1
      seconds: 0
      milliseconds: 0
    alias: Ждать пока ТВ не включится
  - action: media_player.volume_set
    metadata: {}
    data:
      volume_level: 0
    target:
      entity_id: "{{ target }}"
    alias: Убавить громкость до 0
  - action: media_player.select_source
    metadata: {}
    data:
      source: Plex
    target:
      entity_id: "{{ target }}"
    alias: Включить источник Plex
  - variables:
      timestamp: "{{ (now().timestamp() | int) }}"
  - alias: Сканировать клиентов Plex каждые 5 секунд, пока не найдется клиент
    repeat:
      sequence:
        - delay:
            hours: 0
            minutes: 0
            seconds: 5
            milliseconds: 0
        - action: button.press
          metadata: {}
          data: {}
          target:
            entity_id: button.media_scan_clients
      while:
        - condition: and
          conditions:
            - condition: template
              value_template: "{{ (now().timestamp() | int) - timestamp < 60 }}"
            - condition: not
              conditions:
                - condition: or
                  conditions:
                    - condition: state
                      entity_id: media_player.plex_plex_for_lg_lg_oled65c1rla
                      state: idle
                    - condition: state
                      entity_id: media_player.plex_plex_for_lg_lg_65up751c0zf
                      state: idle
    enabled: true
  - action: telegram_bot.send_message
    metadata: {}
    data:
      message: |-
        media_content: {{ media_content_id }} 
         media_position: {{ media_position }}
      target: -1001108604669
    enabled: false
  - action: media_player.play_media
    metadata: {}
    data:
      media_content_id: "{{ media_content_id }}"
      media_content_type: movie
    target:
      entity_id: "{{ plextarget }}"
    alias: Включить фильм на Plex
  - alias: Ждать пока статус Plex не станет Playing
    wait_for_trigger:
      - trigger: state
        entity_id:
          - media_player.plex_plex_for_lg_lg_oled65c1rla
          - media_player.plex_plex_for_lg_lg_65up751c0zf
        to: playing
        for:
          hours: 0
          minutes: 0
          seconds: 1
    timeout:
      hours: 0
      minutes: 1
      seconds: 0
      milliseconds: 0
  - action: media_player.media_pause
    metadata: {}
    data: {}
    target:
      entity_id: "{{ target }}"
    alias: Поставить на паузу
  - wait_for_trigger:
      - trigger: state
        entity_id:
          - media_player.lg_webos_kitchen
          - media_player.lg_webos_bedroom
        to: paused
        for:
          hours: 0
          minutes: 0
          seconds: 1
    timeout:
      hours: 0
      minutes: 0
      seconds: 10
      milliseconds: 0
    alias: Ждать пока не станет на паузу
  - action: media_player.media_seek
    metadata: {}
    data:
      seek_position: "{{ media_position }}"
    target:
      entity_id: "{{ plextarget }}"
    alias: Перемотать фильм в нужное место
  - action: media_player.volume_set
    metadata: {}
    data:
      volume_level: "{{ volume }}"
    target:
      entity_id: "{{ target }}"
    alias: Установить уровень громкости как на источнике
description: ""
icon: mdi:movie

Буду рад вашим комментариям и советам по улучшению. Спасибо.

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