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

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'] }}
5 Likes

Really interesting stuff here. I have a suggestion; do you think you could build something that can take a nice, verbose JSON blob that represents the configuration, and then compile it down into your bit strings for the user? This would let you get really fancy with the feature set while not having to encumber the user with the complexities of the bit language.

I’m not sure the UI templating is powerful enough, but it would be amazing if you could generate blueprint UI’s for this stuff (that allowed you to build and customize that scene bitmask)

thanks for the suggestion - TBH I am still planning to make “the bits” easier but the other way.
I’m not sure if the JSON-to-bit-configuration script/template is possible to be made in quick/easy way… there would be a need to guess different entity types, search&use different profiles etc. sounds like a proper configuration-application, not just a script/template/automation for HA :wink:

but - as I mentioned - almost since the beginning of my script I have plans to make things easier, but unfortunately didn’t found any clues/solutions that would help me to accomplish my goal: I’d love to connect HA with google sheets, so user would just enter the config into a sheet, and the rest would be done automatically - I have a sheet anyway, and I am using some formulas to “export” the piece of yaml that I then paste into my configuration files. it’s not perfect, but better than doing everything by hand. a quick glance at a fragment of my sheet is below:

formulas give me the lines needed to paste as t_config in the automation, and also t_entities, t_profile and t_volume variables in the script.

speaking about the blueprints - I must confess that I didn’t used them yet. after it was introduced, it was very tempting, but then - I wrote, and still write, all my automations by myself, and didn’t needed anything that could be found in the blueprints, and also didn’t looked at the way to create them for others. probably it wouldn’t be a good thing for my script anyway, as it would be needed to run/add blueprint everytime you change something in the scenes/profiles :frowning:

btw, I’ve just updated the pastebin code to the latest version I’m using in my environment :slight_smile: there are some small fixes/tweaks, nothing that would give a breaking change.

2 Likes