Script to copy one bulb to another with colour and brightness variations

Copy-Bulb

Home Assistant script to copy one bulb to another with random colour and brightness variations.

I wrote this because we have a couple of places that I wanted one or two particular light bulbs to be set to a different (but pleasant) contrast to the other bulbs in the room.

We have two outside lights on a porch, and a third light above the house number plate - no matter what colours and settings the two porch lights are at I wanted the plate light to be a contrast to them.
In another use case there is a room with four ceiling lights and a table lamp, I wanted the table lamp to be the same colour as the ceiling lights but always a slightly brighter and richer colour.

Rather than implementing this as an automation, I’ve done it as a script that can then be used multiple times by several automations in different situations.

Install

Script is here, just copy and past it into the script editor in yaml mode:

alias: Copy Bulb
description: Copy one bulb to another with random variations.
fields:
  from:
    name: From
    description: Bulb settings to copy from.
    selector:
      entity:
        filter:
          - domain: light
    example: light.pg1
    required: true
  to:
    name: To
    description: Bulb settings to copy to.
    selector:
      entity:
        filter:
          - domain: light
    example: light.psr1
    required: true
  hue_mode:
    name: (OPTIONAL) Hue Mode
    description: Method to vary hue. Defaults to none.
    selector:
      select:
        options:
          - none
          - complementary
          - analogous
          - close
          - subtle
          - triadic
          - tetradic
          - random
    example: complementary
    required: false
  sat_mode:
    name: (OPTIONAL) Saturation Mode
    description: Method to vary saturation. Defaults to none.
    selector:
      select:
        options:
          - none
          - desaturate
          - resaturate
          - random
    example: desaturate
    required: false
  saturation_range_low:
    name: (OPTIONAL) Saturation Range Low
    description: >-
      When varying saturation, vary by at least this much or if random at least
      this. Defaults to 30%.
    selector:
      number:
        min: 0
        max: 100
        unit_of_measurement: "%"
    example: 25
    required: false
  saturation_range_high:
    name: (OPTIONAL) Saturation Range High
    description: >-
      When varying saturation, vary by at most this much or if random at most
      this. Defaults to 80%.
    selector:
      number:
        min: 0
        max: 100
        unit_of_measurement: "%"
    example: 25
    required: false
  brightness_mode:
    name: (OPTIONAL) Brightness Mode
    description: >-
      Method to vary brightness by. Defaults to none but if varied will make
      sure brightness is > 20%.
    selector:
      select:
        options:
          - none
          - brighter
          - dimmer
          - random
    example: dimmer
    required: false
  brightness_range_low:
    name: (OPTIONAL) Brightness Range Low
    description: >-
      When varying brightness, vary by at least this much or if random always
      above this. Defaults to 30%.
    selector:
      number:
        min: 0
        max: 100
        unit_of_measurement: "%"
    example: 25
    required: false
  brightness_range_high:
    name: (OPTIONAL) Brightness Range High
    description: >-
      When varying brightness, vary by at most this much or if random always
      below this. Defaults to 80%.
    selector:
      number:
        min: 0
        max: 100
        unit_of_measurement: "%"
    example: 25
    required: false
  transition:
    name: (OPTIONAL) Timeout
    description: How long in seconds to transition the copy. Defaults to 0 seconds.
    selector:
      number:
        min: 0
        max: 100
        step: 0.1
        unit_of_measurement: sec
    example: 3
    required: false
sequence:
  - if:
      - condition: template
        value_template: "{{ states(from) == 'off' }}"
    then:
      - data: {}
        target:
          entity_id: "{{ to }}"
        action: light.turn_off
    else:
      - variables:
          src_hs: "{{ state_attr(from, 'hs_color') }}"
          src_br: "{{ state_attr(from, 'brightness') }}"
          h_mode: "{{ hue_mode if hue_mode is defined else \"none\" }}"
          s_mode: "{{ sat_mode if sat_mode is defined else \"none\" }}"
          b_mode: "{{ brightness_mode if brightness_mode is defined else \"none\" }}"
          srl: >-
            {{ saturation_range_low if saturation_range_low is defined else "30"
            }}
          srh: >-
            {{ saturation_range_high if saturation_range_high is defined else
            "80" }}
          brl: >-
            {{ brightness_range_low if brightness_range_low is defined else "30"
            }}
          brh: >-
            {{ brightness_range_high if brightness_range_high is defined else
            "80" }}
          dst_br: |-
            {%- macro b_random_dimmer(b,l,h) -%}
              {{- [50, (b * (1 - ((range(l,h) | random) / 100)))] | max | round(3) | float -}}
            {%- endmacro -%}

            {%- macro b_random_brighter(b,l,h) -%}
              {{- [255, (b + ((255 - b) * ((range(l,h) | random) / 100)))] | min | round(3) | float -}}
            {%- endmacro -%}

            {%- macro b_random(l, h) -%}
              {{- [255, [50, (range(l,h) | random) * 2.55] | max] | min | round(3) | float -}}
            {%- endmacro -%}

            {%- if b_mode == "none" -%}
              {{- src_br -}}
            {%- elif b_mode == "brighter" -%}
              {{- b_random_brighter(src_br, brl, brh) -}}
            {%- elif b_mode == "dimmer" -%}
              {{- b_random_dimmer(src_br, brl, brh) -}}
            {%- elif b_mode == "random" -%}
              {{- b_random(brl, brh) -}}
            {%- else -%}
              {{- src_br -}}
            {%- endif -%}
          dst_h: |-
            {%- macro h_complementary(c) -%}
              {{- ((c + 180) % 360) | round(3) -}}
            {%- endmacro -%}

            {%- macro h_random_analogous(c) -%}
              {{- ((c + ([-30, 30] | random)) % 360) | round(3) -}}
            {%- endmacro -%}

            {%- macro h_random_analogous_close(c) -%}
              {{- ((c + ([-15, 15] | random)) % 360) | round(3) -}}
            {%- endmacro -%}

            {%- macro h_random_analogous_subtle(c) -%}
              {{- ((c + ([-7.5, 7.5] | random)) % 360) | round(3) -}}
            {%- endmacro -%}

            {%- macro h_random_triadic(c) -%}
              {{- ((c + 120 * ([1, 2] | random)) % 360) | round(3) -}}
            {%- endmacro -%}

            {%- macro h_random_tetradic(c) -%}
              {{- ((c + 90 * ([1, 2, 3] | random)) % 360) | round(3) -}}
            {%- endmacro -%}

            {%- macro h_random() -%}
              {{- (range(0, 360) | random) | round(3) -}}
            {%- endmacro -%}

            {%- if h_mode == "none" -%}
              {{- src_hs[0] -}}
            {%- elif h_mode == "complementary" -%}
              {{- h_complementary(src_hs[0])|float -}}
            {%- elif h_mode == "analogous" -%}
              {{- h_random_analogous(src_hs[0])|float -}}
            {%- elif h_mode == "close" -%}
              {{- h_random_analogous_close(src_hs[0])|float -}}
            {%- elif h_mode == "subtle" -%}
              {{- h_random_analogous_subtle(src_hs[0])|float -}}
            {%- elif h_mode == "triadic" -%}
              {{- h_random_triadic(src_hs[0])|float -}}
            {%- elif h_mode == "tetradic" -%}
              {{- h_random_tetradic(src_hs[0])|float -}}
            {%- elif h_mode == "random" -%}
              {{- h_random()|float -}}
            {%- else -%}
              {{- src_hs[0] -}}
            {%- endif -%}
          dst_s: |-
            {%- macro s_random_desat(c,l,h) -%}
              {{- [0, (c * (1 - ((range(l,h) | random) / 100)))] | max | round(3) | float -}}
            {%- endmacro -%}

            {%- macro s_random_resat(c,l,h) -%}
              {{- [100, (c + ((100 - c) * ((range(l,h) | random) / 100)))] | min | round(3) | float -}}
            {%- endmacro -%}

            {%- macro s_random(l,h) -%}
              {{- [100, [0, (range(l,h) | random)] | max] | min | round(3) | float -}}
            {%- endmacro -%}

            {%- if s_mode == "none" -%}
              {{- src_hs[1] -}}
            {%- elif s_mode == "resaturate" -%}
              {{- s_random_resat(src_hs[1],srl,srh)|float -}}
            {%- elif s_mode == "desaturate" -%}
              {{- s_random_desat(src_hs[1],srl,srh)|float -}}
            {%- elif s_mode == "random" -%}
              {{- s_random(srl,srh)|float -}}
            {%- else -%}
              {{- src_hs[1] -}}
            {%- endif -%}
      - data: >-
          { {%- if transition is defined -%} "transition" : {{ transition }},
          {%- endif -%} "hs_color" : ({{ dst_h }}, {{ dst_s }}), "brightness" :
          {{ dst_br }} }
        target:
          entity_id: "{{ to }}"
        action: light.turn_on
mode: parallel
icon: mdi:lightbulb-multiple
max: 30
max_exceeded: silent

Examples

Here is an automation that uses the script to copy the colour of one of the porch bulbs to the bulb above the house number plate, using a tetradic colour, making it slightly brighter and less saturated:

alias: House Number Light Controller
description: Change the colour of the house number light on to stand out.
triggers:
  - entity_id:
      - light.porch_light_1
    attribute: hs_color
    for:
      hours: 0
      minutes: 0
      seconds: 30
    trigger: state
    id: Colour
  - entity_id:
      - light.porch_light_1
    from: "on"
    to: "off"
    trigger: state
    id: "Off"
  - entity_id:
      - light.porch_light_1
    from: "off"
    to: "on"
    trigger: state
    id: "On"
conditions: []
actions:
  - delay:
      hours: 0
      minutes: 0
      seconds: 5
      milliseconds: 0
  - if:
      - condition: trigger
        id:
          - "Off"
    then:
      - action: homeassistant.turn_off
        metadata: {}
        data: {}
        target:
          entity_id: light.house_number
    else:
      - data:
          from: light.porch_light_1
          to: light.house_number
          hue_mode: tetradic
          brightness_mode: brighter
          sat_mode: desaturate
        action: script.copy_bulb
mode: restart

Stuck this on Github just in case it ever updates: Github: Copy-Bulb