Home Assistant: немного о шаблонах

19 февраля, 2021

Тема шаблонов (Templates) в HA всегда вызывает много вопросов, причем порой и у пользователей системы со стажем. Здесь я хочу собрать некоторое кол-во примеров с комментариями, которые помогут лучше понять принцип работы и синтаксис темплейтов.

Для погружения в тему, начать стоит как всегда с изучения официальной документации, а так же руководства на шаблонизатор Jinja2, именно его под капотом использует HA.
Шаблоны можно применять во множестве мест, начиная от объектов (например, создав собственный sensor или switch) и заканчивая скриптами, автоматизациями и интерфейсом lovelace. С их помощью можно получать необходимые данные из объектов (сущностей) – стейты и атрибуты, при необходимости модифицировать их, а так же реализовывать всевозможную логику (переменные, циклы, проверки и т.д.). Это по-настоящему мощный инструмент, открывающий массу возможностей.

Один из главных инструментов для работы с шаблонами находится в разделе “Панель разработчика” – “Шаблоны” (/developer-tools/template), здесь можно в он-лайн режиме проверить корректность составленного шаблона и оценить его результат. Не забывайте про него, это сохранит множество времени и нервов при отладке сложных конструкций, а так же поможет лучше понять как все это работает.

Коротко про синтаксис

Обратите внимание на оформление, в шаблонах могут использоваться строки с разным обрамлением:

  • {{ }}Expressions (выражения), эти конструкции после вычисления возвращают какой-то объект.
    Пример: {{ as_timestamp(now()) | timestamp_custom ('%W') }} вернет номер текущей недели.
    А {{ (state_attr('light.0x86bd7fffe616b72_light', 'brightness') }} – атрибут brightness (яркость) лампы.
  • {% %}Statements (не знаю как правильно перевести), в этих конструкциях размещается все то, что содержит в себе какую-то “логику”: переменные, проверки if\else, циклы и т.д.
    Пример: {% if is_state('group.family_persons', 'not_home') %}Никого нет{% endif %} вернет текст “Никого нет” если стейт у группы group.family_persons равен not_home.

По умолчанию, каждая строка шаблона на выходе будет оканчиваться символом новой строки “\n”.
Причем даже те строки, которые ничего не выводят (например, объявление переменной – {% set a = 123 %}).
Об этом необходимо помнить и учитывать при составлении темплейта.
К примеру, если шаблон используется для вывода чего-либо на экран, можно потратить уйму времени в борьбе с лишними пустыми строками на выходе.
Для управления этим поведением в jinja2 существует специальный механизм:

  • {{- }} или {%- %} знак минуса в левой части уберет символ новой строки до текущей.
  • {{ -}} или {% -%} знак минуса в правой части уберет символ новой строки после текущей.

Помимо этого, стоит уделить время ресурсу yaml-multiline.info, на котором можно наглядно разобраться с вопросом форматирования и переноса строк в YAML’е (символы |, > и т.д.), это бывает полезно при форматировании многострочных шаблонов.

Отдельно стоит упомянуть о том, какие операторы можно использовать в шаблонах.
Полную информацию об этом стоит искать в документации на jinja, а вкратце это:

Помимо этого, в выражениях можно использовать фильтры jinja и HA, передавая значения в них через символ “|”.
Пример: ((states('sensor.0x158d0003230618_pressure') | float) / 1.333) | round(2)

Важные моменты в документации, на которые стоит обратить особое внимание:

– ~ –

Примеры

Сенсоры и их атрибуты

С помощью шаблонов можно создавать как бинарные (on\off), так и обычные сенсоры:

binary_sensor:
  - platform: template
    sensors:
      # Создаем выделенный сенсор для водонагревателя, отражающий его статус (вкл\выкл)
      boiler_status:
        device_class: power
        # Сенсор примет состояние 'on', если стейт объекта switch.tplink_smartplug_01 будет 'on'
        value_template: "{{ is_state('switch.tplink_smartplug_01', 'on') }}"

      # Собственный датчик протечки, меняющий свое состояние в зависимости от input_boolean
      neptun_water_leakage:
        friendly_name: Датчики протечки Нептун
        device_class: 'moisture'
        value_template: >
          # Если input_boolean.neptun_activated = 'on', то у нас протечка
          {{ is_state('input_boolean.neptun_activated', "on") }}

sensor:
  - platform: template
    sensors:
      # Создаем выделенный сенсор на основе атрибута другого объекта
      gismeteo_temperature:
        unit_of_measurement: °C
        # В этот сенсор будет попадать данные из атрибута temperature (температура)
        value_template: '{{ state_attr("weather.gismeteo","temperature") }}'

      # Сенсор уровня воды в увлажнителе воздуха (в %)
      smartmi_humidifier_01_water_level:
        friendly_name: "Остаток воды"
        value_template: >
          # Здесь сырые данные из атрибута depth переводятся в проценты
          {{ ((state_attr('fan.xiaomi_miio_device', 'depth') / 120) * 100) | int }}

В примерах выше используются простые выражения (Expressions) с использованием встроенных в HA функций is_state() и state_attr(). С полным перечнем функций и правилами их применения лучше всего ознакомится в документации.

Более интересный пример:

sensor:
  - platform: template
    sensors:
      date_formatted:
        friendly_name: 'Date (DD.MM.YYYY)'
        value_template: "{{ as_timestamp(states('sensor.date_time_iso')) | timestamp_custom('%d.%m.%Y') }}"
        icon_template: mdi:calendar
        attribute_templates:
          day_of_week: >-
            {% set day_num = ["Понедельник", "Вторник", "Среда", "Четверг", "Пятница", "Суббота", "Воскресенье"] %}
            {% set day_of_week = day_num[now().weekday()] %}
            {{ day_of_week }}

Здесь в качестве стейта (state) сенсора date_formatted будет выведен результат выражения
{{ as_timestamp(states('sensor.date_time_iso')) | timestamp_custom('%d.%m.%Y') }} и дополнительно, в виде атрибута – день недели, получаемый с помощью ряда операций.

В качестве значения сенсора можно использовать произвольный текст, в этом примере в зависимости от состояния датчика двери (геркона) в стейт будет записан текст “Открыта” или “Закрыта” (речь про дверь):

sensor:
  - platform: template
    sensors:
      entrance_door_status:
        value_template: >
          {% if is_state('binary_sensor.0x158d00031c790f_contact', "on") %}Открыта
          {% elif is_state('binary_sensor.0x158d00031c790f_contact', "off") %}Закрыта
          {% else %}Unavailable
          {% endif %}

Более комплексный пример:

- platform: template
  sensors:
    local_pressure_mmhg:
      value_template: >
        {% set pressure = states('sensor.0x158d0003230618_pressure') | float(default=-1) %}
        {% if pressure != -1 and (((states('sensor.0x158d0003230618_pressure') | float) / 1.333) | round(2)) > 500 %}
        {{ ((states('sensor.0x158d0003230618_pressure') | float) / 1.333) | round(2) }}
        {% else %}
        {{ states('sensor.local_pressure_mmhg') }}
        {% endif %}

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

- platform: template
  sensors:
    tplinksmartplug01_amps:
      friendly_name_template: "{{ states.switch.tplink_smartplug_01.name}} Current"
      value_template: '{{ state_attr("switch.tplink_smartplug_01","current_a") | float }}'
      unit_of_measurement: 'A'
    tplinksmartplug01_watts:
      friendly_name_template: "{{ states.switch.tplink_smartplug_01.name}} Current Consumption"
      value_template: '{{ state_attr("switch.tplink_smartplug_01","current_power_w") | float }}'
      unit_of_measurement: 'W'

Или меняя иконку сенсора в зависимости от различных условий:

mirobot_1s_battery:
  friendly_name: "Xiaomi Vacuum Cleaner 1S"
  device_class: battery
  unit_of_measurement: '%'
  icon_template: >                                                                                                                                 
    {% if state_attr('vacuum.xiaomi_vacuum_cleaner', 'battery_level') %}                                                                                     
      {% if state_attr('vacuum.xiaomi_vacuum_cleaner', 'battery_level') >= 98 %}                                                                             
        mdi:battery                                                                                                                                
      {% elif state_attr('vacuum.xiaomi_vacuum_cleaner', 'battery_level') >= 85 %}                                                                           
        mdi:battery-90                                                                                                                             
      {% elif state_attr('vacuum.xiaomi_vacuum_cleaner', 'battery_level') >= 75 %}                                                                           
        mdi:battery-80                                                                                                                             
      {% elif state_attr('vacuum.xiaomi_vacuum_cleaner', 'battery_level') >= 65 %}                                                                           
        mdi:battery-70                                                                                                                             
      {% elif state_attr('vacuum.xiaomi_vacuum_cleaner', 'battery_level') >= 50 %}                                                                           
        mdi:battery-50                                                                                                                             
      {% elif state_attr('vacuum.xiaomi_vacuum_cleaner', 'battery_level') >= 35 %}                                                                           
        mdi:battery-30                                                                                                                             
      {% elif state_attr('vacuum.xiaomi_vacuum_cleaner', 'battery_level') >= 25 %}                                                                           
        mdi:battery-20                                                                                                                             
      {% elif state_attr('vacuum.xiaomi_vacuum_cleaner', 'battery_level') <= 15 %}                                                                           
        mdi:battery-10                                                                                                                             
      {% else %}                                                                                                                                   
        mdi:battery-outline                                                                                                                        
      {% endif %}                                                                                                                                  
    {% endif %}
  value_template: "{{ state_attr('vacuum.xiaomi_vacuum_cleaner', 'battery_level') }}"

Switches

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

switch:
  - platform: template
    switches:
      samsungtv_40c5100:
        # Считать выключатель включенным, если binary_sensor.samsungtv_40c5100 = 'on'
        value_template: "{{ is_state('binary_sensor.samsungtv_40c5100', 'on') }}"
        # Выключатель доступен если выполняется условие ниже, иначе он становится unavailable
        availability_template: "{{ is_state('binary_sensor.smartir_01_status', 'on') }}"
        turn_on:
          service: switch.turn_on
          data:
            entity_id: switch.smartir_01_tv_samsung_power
        turn_off:
          service: script.turn_on
          data:
            entity_id: script.power_off_samsungtv_40c5100
        # Иконка у выключателя будет меняться в зависимости от состояния сенсора binary_sensor.samsungtv_40c5100
        icon_template: >-
          {% if is_state('binary_sensor.samsungtv_40c5100', 'on') %}
            mdi:television
          {% else %}
            mdi:television-off
          {% endif %}

Скрипты и автоматизации

Самое широкое применение шаблоны встречают в скриптах и автоматизациях.

Скрипт для запуска уборки роботом-пылесосом конкретной комнаты (в интерфейсе input_select.room_to_vacuum это выпадающий список с комнатами):

script:
  start_vacuum_room:
    alias: 'Clean Selected Room [Mi Robot]'
    sequence:
        - service: script.turn_on
          data_template:
            entity_id: >-
              {% if is_state("input_select.room_to_vacuum", "Прихожая") %}
                script.start_vacuum_hallway
              {% elif is_state("input_select.room_to_vacuum", "Детская") %}
                script.start_vacuum_nursery
              {% elif is_state("input_select.room_to_vacuum", "Гостиная") %}
                script.start_vacuum_living_room
              {% elif is_state("input_select.room_to_vacuum", "Кухня") %}
                script.start_vacuum_kitchen
              {% elif is_state("input_select.room_to_vacuum", "Спальня") %}
                script.start_vacuum_bedroom
              {% endif %}

Автоматизация, меняющая мощность работы робота-пылесоса:

automation:
  - alias: 'Set cleaning mode'
    trigger:
      platform: state
      entity_id: input_select.vacuum_power
    action:
      - service: >
          {% if trigger.to_state.state == 'Silent' %}
            script.set_vacuum_power_silent
          {% elif trigger.to_state.state == 'Standard' %}
            script.set_vacuum_power_standard
          {% elif trigger.to_state.state == 'Medium' %}
            script.set_vacuum_power_medium
          {% elif trigger.to_state.state == 'Turbo' %}
            script.set_vacuum_power_turbo
          {% elif trigger.to_state.state == 'Gentle' %}
            script.set_vacuum_power_gentle
          {% endif %}

Оповещение о произошедшей ошибке.
С помощью шаблона можно получить текст ошибки из атрибута error:

automation:
  - alias: 'Оповещение об ошибке'
    initial_state: true
    trigger:
      platform: state
      entity_id: vacuum.xiaomi_vacuum_cleaner
      to: "error"
    action:
      - service: notify.telegram
        data_template:
          message: |
            Mi Robot: Произошла *ошибка*!
            {{ state_attr('vacuum.xiaomi_vacuum_cleaner', "error") }}

Автоматизация меняющая яркость лампы по двойному клику на кнопку (по кругу):

automation:
  - alias: 'Яркость света в детской'
    initial_state: true
    trigger:
      platform: state
      entity_id: sensor.0x158d00033efd9e_action
      to: 'double'
    action:
      service: light.turn_on
      data_template:
        entity_id: light.detskaia
        transition: '0.5'
        brightness: >
          {%- if (state_attr('light.detskaia', 'brightness') | int) <= 3 %}
            51
          {% elif (state_attr('light.detskaia', 'brightness') | int) <= 51 %}
            102
          {% elif (state_attr('light.detskaia', 'brightness') | int) <= 102 %}
            153
          {% elif (state_attr('light.detskaia', 'brightness') | int) <= 153 %}
            204
          {% elif (state_attr('light.detskaia', 'brightness') | int) <= 204 %}
            255
          {% elif (state_attr('light.detskaia', 'brightness') | int) <= 255 %}
            3
          {% endif %}

Плавное включение света в заданное в интерфейсе HA время (будильник):

automation:
  - alias: Sunrise Lighting (Bedroom)
    initial_state: true
    trigger:
      platform: template
      # Триггером служит совпадение времени в sensor.time и input_datetime.sunrise_in_bedroom
      value_template: "{{ states('sensor.time') == (states('input_datetime.sunrise_in_bedroom')[:5]) }}"
    condition:
      condition: and
      conditions:
      - condition: state
        entity_id: binary_sensor.workday_sensor
        state: 'on'
      - condition: sun
        before: sunrise
        before_offset: "00:30:00"
    action:
    - service: light.turn_on
      entity_id: light.spalnia_stol
      data:
        effect: SunriseBW

Постепенное увеличение громкости:

script:
  googlehome3792_increase_volume:
    alias: Increase volume by 5%
    sequence:
      - service: media_player.volume_set
        entity_id: media_player.googlehome3792
        data_template:
          volume_level: >
            {% set level = (state_attr('media_player.googlehome3792', 'volume_level') | float) + (0.05 | float) %}
            {% if level < 1 %} {{ level }}
            {% else %} 1
            {% endif %}

automation:
  - alias: Increase volume loop - Childrens Room
    initial_state: false
    trigger:
      platform: time_pattern
      minutes: "/3"
    action:
      - service: script.turn_on
        entity_id: script.googlehome3792_increase_volume

Изменение громкости у выбранного из выпадающего списка источника:

automation:
  - alias: 'Громкость радио'
    trigger:
      platform: state
      entity_id: input_number.volume_radio
    action:
      service: media_player.volume_set
      data_template:
        entity_id: >
          {% if is_state("input_select.output_device", "Гостинная (TV)") %} media_player.gostinaia
          {% elif is_state("input_select.output_device", "Гостинная (Home Mini)") %} media_player.googlehome9967
          {% elif is_state("input_select.output_device", "Детская (TV)") %} media_player.detskaia
          {% elif is_state("input_select.output_device", "Детская (Home Mini)") %} media_player.googlehome3792
          {% endif %}
        volume_level: '{{  states.input_number.volume_radio.state  }}'

Отправка оповещений о приходе или уходе из дома:

automation:
  - alias: 'Home Presence Alert'
    initial_state: true
    trigger:
      platform: state
      entity_id: person.alexander, person.irina
    condition:
      condition: and
      conditions:
        - condition: template
          value_template: "{{ trigger.to_state.state != trigger.from_state.state }}"
    action:
      - service: notify.telegram
        data_template:
          message: >
            {{ trigger.to_state.attributes.friendly_name }}
            {% if trigger.to_state.state == 'home' %}дома!
            {% else %}скорее всего вне дома.
            {% endif %}

В примере выше сообщение будет выглядеть примерно так – “Александр скорее всего вне дома.”
Шаблон соберется в одну строку, т.к. перед ним указан символ “>”, означающий замену символов новой строки на пробелы.

Еще одно оповещение, на этот раз о температуре в комнате (холодно или жарко):

automation:
  - alias: Termo alert [Living Room]
    trigger:
      - platform: template
        value_template: "{{ (states('sensor.0x158d0003230618_temperature') | float) < 22 }}"
        for:
          minutes: 5
      - platform: template
        value_template: "{{ (states('sensor.0x158d0003230618_temperature') | float) > 25 }}"
        for:
          minutes: 5
    action:
      - service: notify.telegram
        data_template:
          message: >-
            В *Гостиной*
            {% if (trigger.to_state.state | float) > 23 -%} жарко,
            {% elif (trigger.to_state.state | float) < 23 -%} холодно,
            {% endif -%} температура: *{{ trigger.to_state.state }}°C*

Команда для телеграм-бота, возвращающая имена находящихся дома:

automation:
  - alias: 'Telegram Bot - Who is home?'
    trigger:
      platform: event
      event_type: telegram_command
      event_data: 
        command: '/whoishome'
    action:
      service: telegram_bot.send_message
      data_template:
        target: '{{ trigger.event.data.user_id }}'
        message: |
          Сейчас дома:
          {%- set entites = expand('group.family_persons') %}{% for prs in entites %}{% if prs.state == "home" %}
          {{ prs.attributes.friendly_name }}{% endif %}{% endfor %}
          {% if is_state("group.family_persons", "not_home") %}Никого нет{% endif %}

Здесь, в отличии от предыдущего примера, перед шаблоном указан символ “|”, означающий сохранение всех переносов строк в шаблоне. Лишняя пустая строка из вывода убрана с помощью конструкции “{%-“.

Еще одна команда для бота, присылающая текущую погоду:

- alias: 'Telegram Bot - Weather'
  trigger:
    platform: event
    event_type: telegram_command
    event_data: 
      command: '/weather'
  action:
    - service: telegram_bot.send_photo
      data_template:
        target: '{{ trigger.event.data.user_id }}'
        file: '/config/www/weather_icons/{{ states("weather.gismeteo") }}.webp'
        caption: |
          Температура {{ state_attr('weather.gismeteo', 'temperature') }}°C
          Влажность {{ state_attr('weather.gismeteo', 'humidity') }}%
          Давление {{ states('sensor.gismeteo_pressure_mmhg') }} mmHg

Здесь, в зависимости от стейта сенсора weather.gismeteo (sunny, rainy, snowy и т.д.) отправляется картинка соответствующая текущей погоде (картинки подготовлены заранее).

– ~ –

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