One script, to rule them all! a.k.a. universal scene changer

Tags: #<Tag:0x00007f326fdb9090> #<Tag:0x00007f326fdb8f78>

ok, so the first thought emerged when one of the Home Assistant’s updated introduced changes to scene system, which required to give “state” to all entites controlled by scene - and because my scenes also control media players’ volume settings, it was a deal breaker for me. WAF dropped by few points, because of constant turning off/breaking the stream played by this or that Google Home Mini and the solution was needed ASAP.
first plan: move all scenes to scripts, so I can make some conditions and check if media player is idle or not, and then change volume if it won’t interrupt anything [well, state is not needed by scripts, so it won’t stop the stream for sure]. but because I use a lot of scenes with many entities, I was just too lazy to rewrite them all.
second plan: make a big fat script to control scenes, bonus points for some kind of universal/relatively simple way of configuring those scenes.

weeks passed by and then the solution just popped to my head: BITS. let’s dismantle all scene config and represent each device with one bit; then join all the bits into one word/variable/string and send it to script. boom! I present to you: one script to rule them all!

some important facts:

  • script is universal - no matter how many entities you want to control - it will adapt automatically
  • light_profiles.csv file doesn’t work too good, because it can work ONLY with xy_color, and I’ve noticed that some rgb bulbs give different outputs when turned on with xy_color, rgb_color or color_temp - that’s why my script uses it’s own profile “system” with support to color_temp, rgb_color and white_value
  • different media players’ volumes for different time of day? no problem! it has a simple volume profile/setting too
  • you can have up to 36 different light profiles and up to 36 different volume settings that can be used in scenes
  • right now script is compatible with ANY entity that can be turned on/off by it’s service: domain.turn_on(off) [light, switch, input_boolean, media_player etc.]
  • media_player additional function: set volume [to given profile, or default setting]
  • light additional settings: brightness, rgb_color or color_temp, transition, white_value
  • media player won’t be interrupted, as the script checks if it’s idle before any action
  • instead of dozen lines of scene config, you can write all as 01102-0-1G - each character, or: bit, is setting for different entity. “0” means turn off, “1” turn on; “-” is “do not touch/change the state”, and higher numbers and letters are different profiles. don’t worry - detailed guide and examples are in the pastebin link!

I’m guessing that script can be updated to work with other entities too [like input_select] but I don’t need that, so didn’t included this part.

ok, and now the code & guide itself - the same thing is also here, at pastebin, where I’m updating it in case of found errors.

#################### documentation #############################################
#
#################### configuration
#
# t_entities - list of all the entities you want to control. remember - order
#              will be important when sendig the bits with configuration,
#              so think how to organize/sort the entities. I suggest that ones
#              used the most should be put first, so when you'll want to turn on
#              or off just them - you'll be able to omit the rest of config bits
# t_profile - list of your light profiles. each profile has required parameters
#             and those that are optional. there are different lights and bulbs,
#             some of them gives better output when set with color_temp, some of
#             them work better with rgb_color. there are also RGBW lights, that
#             need the white_value parameter if you want to turn it all white.
#             script covers them all - profile schema below:
#             [A (int), B (int or array of int), C (string), D (string)]
#             A = brightness [0-255]
#             B = color, set with color_temp [1 number] or rgb_color [3 numbers]
#             C = transition time prefixed with "T" [seconds], defaults to 1
#             D = white_value setting prefixed with "W" [0-255] defaults to 0
#             required values are: A and B.
#             the easiest way to describe it is by examples:
#             1) "50% brightness" = [128]
#             2) "100% brightness with color_temp set to 342" = [255, 342]
#             3) "50% brightness red light with rgb_color" = [255, [255, 0, 0]]
#             4) same as 3) + 10 sec. transition = [255, [255, 0, 0], 'T10']
#             5) "full white, white_value set to 100%, and 5 sec. transition" =
#                [255, [255, 255, 255], 'T5', 'W255']
#            IMPORTANT: first two profiles are kinda standard "turn off", and
#            "turn on" being on positions - respectively 0 and 1, so I strongly
#            suggest to leave the first two just as they are - [0] and [255].
# t_volume - list of volume profiles - those are used for media_player entities
#            and by default are in 0-1 range, so any number you'll enter in the
#            profile, would be divided by 100 to recalculate (0-100 int)
# d_volume - default, failsafe volume (0-1 float)
# t_config - argument given at the start of script, containing all (or just the
#            first) bits of scene configuration. each bit (place in config)
#            represents one entity in the same order that they are listed in the
#            t_entities variable. value of 0 means "turn off", 1 means "turn on"
#            a dash (-) means "do not change", and other values 0-9 and A-Z are
#            for choosing light or volume profiles
#
#################### info
#
# script will evaluate all configuration bits, and will take some actions based
# on entity and bit value. configuration bits are using numbers with base of 36.
# that means, you can use numbers from 0 to 9, and letters from A to Z, to
# represent states of your entities.
#
# most entities will be fine with just two states: 0 and 1 being "turn off" and
# "turn on", but with all the RGB lighting at home, you can have up to 36
# different light colors, temperatures, brightness' etc. configured and then you
# can call them easily with number or letter.
#
# although order of the configuration bits must be corelated with entities order
# given in t_entities variable, you can omit a part of the config - for example:
# first 5 entities are lights in the living room, next 20 entities are not so
# important - they could be anything. you can send only 5 bits of config to the
# script, if you want to control all the living room lights - and then script
# will stop. but because script uses bits order to control entities, if you want
# to control those 20 next entities but not 5 living room lights - you will need
# to give all 25 bits of config.
#
# if the entity is media player, and it is about to have it's volume changed,
# script first checks if it is playing something. volume changes only if there's
# no active playback. but if the scene is going to turn media player off - it
# will, no matter what.
#
#################### running the script
#
# easy, similar as other scripts:
# - service: script.run_scene
#   data:
#     t_config: 'STRING-WITH-CONFIGURATION-BITS'
#
# example:
# - service: script.run_scene
#   data:
#     t_config: '--3-20-GH---'
#
#################### some more detailed examples
#
# based on example entities and profiles configured in the script:
# 1. turn on kitchen light, turn off bathroom light, leave rgb_bulb intact, and
#    turn xmas tree off, with no changes to party mode and media player:
#    t_config: '10-0'
# 2. turn all off:
#    t_config: '000000'
# 3. leave kitchen & bathroom intact, turn on rgb_bulb full white (last profile
#    = [255,[255,255,255],'W255']), turn off xmas tree, turn on party mode and
#    set media player to 50% (4th profile):
#    t_config: '--5013'
# ...and so on.
#
#################### important notes
#
# profiles are numbered from 0, so to choose first one, enter 0; to choose third
# enter 2; to choose profile number 10 enter A, profile 16th enter G; etc.
#
# you can have as many entities as you want. script just checks all config bits
# and as long as number of configuration bits is less or equal than number of
# entities - all will work fine ;)
#
# you can use any entity you want - it is only important that it uses services
# turn_on and turn_off - the rest is done automatically
#
#################### companion entity and automation
#
# not needed for script to run, but I included it as another example of using
# the script and also to show how it is possible to manage scenes now - with
# ease and all based in one place. (example automation has "example 2" set as
# the default scene for failsafe configuration)
#
################################################################################

script:
  run_scene:
    alias: run_scene
    icon: "mdi:checkbox-multiple-marked-outline"
    description: "scene switcher with n-bit configuration"
    variables:
      t_entities: ['light.kitchen', 'light.bathroom', 'light.rgb_bulb', 'switch.xmas_tree', 'input_boolean.party_mode', 'media_player.kitchen']
      t_profile: [[0], [255], [255,334], [189,342,'T5'], [255,[255,0,0]], [255,[255,255,255],'W255']]
      t_volume: [0, 5, 25, 50, 100]
      d_volume: 0.25
    fields:
      t_config:
        description: "scene configuration bits"
        example: "0-0110--3330------0-0000111333130000--"
    mode: restart
    sequence:
    #################### loopy loop
    - repeat:
        while: "{{ repeat.index <= t_config|length if t_config|length <= t_entities|length else t_entities|length }}"
        sequence:
        - variables:
            t_bit: "{{ t_config[repeat.index-1]|int(base=36) }}"
        - condition: template
          value_template: "{{ (t_config|length > 0) and (t_config[repeat.index-1] != '-') }}"
        - choose:
          #################### media_players - change volume
          - conditions: "{{ t_entities[repeat.index-1].split('.')[0] == 'media_player' and t_bit|int > 0 and not is_state(t_entities[repeat.index-1],'playing') }}"
            sequence:
            - service: media_player.volume_set
              data:
                entity_id: "{{ t_entities[repeat.index-1] }}"
                volume_level: "{{ t_volume[t_config[repeat.index-1]|int]/100 if t_config[repeat.index-1]|int < t_volume|length else d_volume }}"
          #################### media_players - don't break anything
          - conditions: "{{ t_entities[repeat.index-1].split('.')[0] == 'media_player' and t_bit|int > 0 and is_state(t_entities[repeat.index-1], 'playing') }}"
            sequence:
            - delay:
                milliseconds: 5
          #################### turn on lights - with color_temp
          - conditions: "{{ t_entities[repeat.index-1].split('.')[0] == 'light' and t_bit|int > 1 and t_profile[t_bit|int]|length > 1 and t_profile[t_bit|int][1][0] is not defined }}"
            sequence:
            - service: light.turn_on
              data:
                entity_id: "{{ t_entities[repeat.index-1] }}"
                brightness: "{{ t_profile[t_bit|int][0] }}"
                color_temp: "{{ t_profile[t_bit|int][1] }}"
                transition: "{{ t_profile[t_bit|int][2]|regex_replace('T', '')|int if (t_profile[t_bit|int][2] is defined and t_profile[t_bit|int][2][0] == 'T') else 1 }}"
                white_value: "{{ t_profile[t_bit|int][2]|regex_replace('W','')|int if (t_profile[t_bit|int][2] is defined and t_profile[t_bit|int][2][0] == 'W') else t_profile[t_bit|int][3]|regex_replace('W','')|int if (t_profile[t_bit|int][3] is defined and t_profile[t_bit|int][3][0] == 'W') else 0 }}"
          #################### turn on lights - with rgb_color
          - conditions: "{{  t_entities[repeat.index-1].split('.')[0] == 'light' and t_bit|int > 1 and t_profile[t_bit|int]|length > 1 and t_profile[t_bit|int][1][0] is defined }}"
            sequence:
            - service: light.turn_on
              data:
                entity_id: "{{ t_entities[repeat.index-1] }}"
                brightness: "{{ t_profile[t_bit|int][0] }}"
                rgb_color:
                  - '{{ t_profile[t_bit|int][1][0]|int }}'
                  - '{{ t_profile[t_bit|int][1][1]|int }}'
                  - '{{ t_profile[t_bit|int][1][2]|int }}'
                transition: "{{ t_profile[t_bit|int][2]|regex_replace('T', '')|int if (t_profile[t_bit|int][2] is defined and t_profile[t_bit|int][2][0] == 'T') else 1 }}"
                white_value: "{{ t_profile[t_bit|int][2]|regex_replace('W','')|int if (t_profile[t_bit|int][2] is defined and t_profile[t_bit|int][2][0] == 'W') else t_profile[t_bit|int][3]|regex_replace('W','')|int if (t_profile[t_bit|int][3] is defined and t_profile[t_bit|int][3][0] == 'W') else 0 }}"
          #################### turn the rest on or off
          default:
          - service: "{{  t_entities[repeat.index-1].split('.')[0] }}.{{ 'turn_on' if (t_config[repeat.index-1]|int == 1 and not is_state(t_entities[repeat.index-1],'playing')) else 'turn_off' }}"
            data:
              entity_id: "{{ t_entities[repeat.index-1] }}"

#################### additional, companion entity and automation

input_select:
  my_scene:
    name: my_scene
    options:
      - "example 1"
      - "example 2"
      - "example 3"

automation:
- id: scene_change
  alias: scene_change
  initial_state: on
  mode: restart
  trigger:
  - platform: state
    entity_id: input_select.my_scene
  - platform: homeassistant
    event: start
  action:
  - service: script.run_scene
    data:
      t_config: >
        {% set t_scenes = {
        'example 1': '10-0',
        'example 2': '000000',
        'example 3': '--5013'
        } %}
        {{ t_scenes[states('input_select.my_scene')] if states('input_select.my_scene') in t_scenes.keys() else t_scenes['example 2'] }}
2 Likes