Smart relative dimming of light group...?

Screen Recording 2022-03-04 at 00.10.13

:bulb:

Eureka, halleluja, light-bulb-moment, wohoo and all that!
After what feels like solving a brain puzzle and diving deep into programming and mathematics over the last few days I finally figured it out myself :smiley:

First off - after extensive searching here I’m amazed no one else seems to have done this, or asked for it… am I really the first? :open_mouth: :thinking:

Secondly - I’m equally amazed that I managed to pull this off with just a template light and a group; no automations or scripts needed :sunglasses:

This should be pretty much on par with the dimming of lights in the IKEA Smart Home app:

  • When the group brightness is adjusted, all individual lights are adjusted by the same amount
    • So the “brightness distance” between lights will remain the same (until any lights reach max or minimum brightness)
  • Individual lights that would turn off as the result of changing the group brightness will remain turned on
  • Lights that have been turned off remain turned off when the group brightness is changed
    • However, if all lights are off then any change to group brightness will turn all of them on
  • When the group brightness is set to 100%, all lights currently on will also change to 100% brightness
    • If all the lights are currently on when setting the group to 100%, all lights in the group will be set to 100%

Alright, enough talking, here’s my code:

groups.yaml

office:
  name: Office 
  entities:
  # The template light will ignore anything other than a light
    - light.office_spotlights
    - light.office_top_corner
    - light.office_desk_lamp

configuration.yaml

light:
  - platform: template
    lights:
      office_lights_smart:
        friendly_name: "Office lights smart"
        # Average of all lights currently on
        level_template: >
          {{ ( expand('group.office') 
            | map(attribute='attributes.brightness', default=0) 
            | select("greaterthan", 0)
            | list or [0])
            | average 
            | round }}
        # True if any light has a brightness larger than 0
        value_template: >
          {{ expand('group.office') 
            | map(attribute='attributes.brightness', default=0) 
            | sum > 0 }}
        turn_on:
          service: light.turn_on
          target:
            entity_id: group.office
        turn_off:
          service: light.turn_off
          target:
            entity_id: group.office
        set_level:
          # Set brightness by amount to all lights (when all are off), OR only the lights that would remain on by the brightness change
          - service: light.turn_on
            data:
              brightness_step: "{{ brightness - state_attr('light.office_lights_smart', 'brightness')|int }}"
            target:
              entity_id: >
                {% set brightness_from = state_attr('light.office_lights_smart', 'brightness')|int %}
                {% set brightness_change = brightness - brightness_from %}
                {{ expand("group.office") 
                  | selectattr('attributes.brightness')
                  | selectattr('attributes.brightness', '>', -brightness_change)
                  | map(attribute='entity_id') 
                  | list or 
                  expand('group.office') 
                  | selectattr('domain', '==', 'light')
                  | map(attribute='entity_id') 
                  | list if brightness != 255 else [] }}
          # Set brightness to 1 for all lights that would turn off, keeping them on
          - service: light.turn_on
            data:
              brightness_pct: 1
            target:
              entity_id: >
                {% set brightness_from = state_attr('light.office_lights_smart', 'brightness')|int %}
                {% set brightness_change = brightness - brightness_from %}
                {{ expand('group.office') 
                | selectattr('attributes.brightness')
                | selectattr('attributes.brightness', '<=', -brightness_change)
                | map(attribute='entity_id') 
                | list 
                }}
          # Set brightness to 100 for all lights currently on when slider moves to 100%, or all the lights if all are currently off
          - service: light.turn_on
            data:
              brightness_pct: 100
            target:
              entity_id: >
                {{ expand("group.office") 
                  | selectattr('attributes.brightness')
                  | map(attribute='entity_id') 
                  | list or 
                    expand('group.office') 
                  | selectattr('domain', '==', 'light')
                  | map(attribute='entity_id') 
                  | list if brightness == 255 else [] }}
Lovelace YAML (for GIF above)
type: grid
columns: 1
square: false
cards:
  - type: entities
    state_color: true
    title: Office lights
    show_header_toggle: true
    entities:
      - entity: light.office_lights_smart
        type: custom:slider-entity-row
        step: 1
        full_row: true
      - entity: light.office_top_corner
        type: custom:slider-entity-row
        step: 1
        toggle: true
      - entity: light.office_spotlights
        type: custom:slider-entity-row
        step: 1
        toggle: true
      - entity: light.office_desk_lamp
        type: custom:slider-entity-row
        step: 1
        toggle: true

Plugins used: slider-entity-row

Some lessons learned

When using brightness_step to change the brightness of a light group the step is not forwarded to each individual light, but instead applied to the group as a whole. Would have made everything so much easier if it wasn’t like that :sweat_smile:

I started getting errors in the logs because I was trying to calculate the | average on an empty list when all lights were off. The trick was to put that part of the filter within round brackets and add the or [0]. :brain:

Then I used the same technique to list all the light entities whenever the list of “lights being so close to 0 that they would turn off” was empty. :brain: :brain:

So in the end what happens is that set_level will apply

  • Brightness step: (difference in brightness)
    • on all lights that wouldn’t get a negative brightness by the change, i.e. turning off
      or
    • on all lights whenever all lights are already turned off
  • Brightness percent: 1
    • on all lights that would get a negative brightness by the change, which basically keeps them turned on
  • Brightness percent: 100
    • when group brightness is adjusted to max, set all lights currently on to 100% or all the lights if none were on

Don’t hesitate to ask if you want me to explain something further! :smiley: I really hope this helps someone else who wants a smarter way of dimming their group of lights!

Now I just need to figure out how I can re-use this code, basically just changing out group.office_lights for different rooms…

11 Likes

I forgot the case of what should happen when the group brightness is increased to max brightness, 100%. I’ve now updated my solution above so it mimics the IKEA app, so that all individual lights currently on will also go to 100%. Or all the lights if none was on to begin with.

I tried to go the templating route for this but I just couldn’t get it working. For any other slow-learners like me there is a quick and dirty solution here and that’s to have an automation that updates the brightness after it’s set. Personally I wanted to set a 10% cap on the brightness. If it helps anyone:

alias: LivRoom CONTROL Restrict under Projector Lights to 10% brightness
description: ''
trigger:
  - platform: numeric_state
    entity_id: light.livroom_undscrledstrip_tuyawifi
    attribute: brightness
    above: '26'
    below: '256'
condition:
  - condition: device
    type: is_on
    device_id: e39bd7cae8c3cdfa68be44f2c1b3a8ca
    entity_id: light.livroom_undscrledstrip_tuyawifi
    domain: light
action:
  - service: light.turn_on
    data:
      brightness: 25
    target:
      entity_id: light.livroom_undscrledstrip_tuyawifi
mode: single
1 Like

Dig your solution for the brightness problem, really don’t like how it works natively for groups. Did you find a way to reuse it better? My initial though was just to move the set level logic into a script for re-use, but there is still a big template you need to create for each group.

Sorry for the late reply! :bowing_man: Looking at my current code, sadly no.

Even with scripts, if I’m not mistaken I’d have to make scripts for each one of the level_template, value_template, turn_on, turn_off, set_level and so on…

I’m thinking perhaps the best way forward would be to put all this logic inside a python script and try to create an even better and improved light group altogether :nerd_face: One day perhaps…

Thank you for doing this!

I’m currently getting the following error when trying to set a brightness for the group when all lights are off. I can’t see where to add the default value to fix this?

Error rendering service target template: ValueError: Template error: int got invalid input 'None' when rendering template '{% set brightness_from = state_attr('light.living_room_lights_smart', 'brightness')|int %} {% set brightness_change = brightness - brightness_from %} {{ expand("group.living_room_dimmable") | selectattr('attributes.brightness') | selectattr('attributes.brightness', '>', -brightness_change) | map(attribute='entity_id', default=0) | list or expand('group.living_room_dimmable') | selectattr('domain', '==', 'light') | map(attribute='entity_id', default=0) | list if brightness != 255 else [0] }}' but no default was specified

Looking through your error message and comparing your code to mine, I think the only thing that differs that might cause this could be the very last line…

It seems the last thing you’re doing is

list if brightness != 255 else [0]

while I on the other hand am doing

list if brightness != 255 else []

Try changing from [0] to just [] and see if that helps.

I would like to have this solution applied to different media player volumes as I have smart speakers spread within my home. Similar to the lights, I would like to have each room to be set accordingly so that when the master slider is moved, the individual rooms adjust, not equal to the master slider.

I am not versed in YAML. Could someone point me in the right direction for this volume solution? Thanks!

Hello,
I would first like to thank you for this template, it was exactly what I was looking for, you saved me quite a bit of time.
However, I have the exact same problem as neilnose, the same error message happens when using the slider while all lights are off.
I’ve set the “else []”, the problem isn’t coming from this part.

Do you have any idea what “but no default was specified” is refering to ?

Assign a default to each |int —> |int(0)

That was it !
Thanks

Thanks for asking this - I’ve been looking for the same thing.
I wish HA would incorporate a similiar feature.
Google Home will present a group of lights and relative-dim them all similar to how your template does.
‘Areas’ seems a logical place for it - extend what an Area can do by imbuing it with control abilities and offer them as ‘fixed’ or ‘relative’ to achieve what you have here, but without having to do all the coding for each one.

I have the same error message

I have with no zero.

My entity does nothing other than error regardless of if the lights are on or not.

The entity is off even though the lights are on.

I have created a group light.office but this script uses group.office and i have no entity of that name which maybe the issue?

It is a great template so any help appreciated

Given all lights are on the defult value does not seem to be the issue - but i dont know how to set the default in any event

What errors have been logged?

Sorry i thought i had replied to the above post which had the identcal error.

Failed to call service light/turn_on. Error rendering service target template: ValueError: Template error: int got invalid input ‘None’ when rendering template ‘{% set brightness_from = state_attr(‘light.office_lights_smart’, ‘brightness’)|int %} {% set brightness_change = brightness - brightness_from %} {{ expand(“group.office”) | selectattr(‘attributes.brightness’) | selectattr(‘attributes.brightness’, ‘>’, -brightness_change) | map(attribute=‘entity_id’) | list or expand(‘group.office’) | selectattr(‘domain’, ‘==’, ‘light’) | map(attribute=‘entity_id’) | list if brightness != 255 else }}’ but no default was specified

It’s the same I answered above: Give int a default value.
That is the original code with default values for brightness and int, using a UI generated light group:


light:
  - platform: template
    lights:
      test_lights_smart:
        friendly_name: "Test lights smart"
        # Average of all lights currently on
        level_template: >
          {{ ( expand('light.test') 
            | map(attribute='attributes.brightness', default=0)
            |map('int', -1)
            | select("greaterthan", 0)
            | list or [0])
            | average 
            | round }}
        # True if any light has a brightness larger than 0
        value_template: >
          {{ expand('light.test') 
            | map(attribute='attributes.brightness', default=0)
            |map('int', -1)
            | sum > 0 }}
        turn_on:
          service: light.turn_on
          target:
            entity_id: light.test
        turn_off:
          service: light.turn_off
          target:
            entity_id: light.test
        set_level:
          # Set brightness by amount to all lights (when all are off), OR only the lights that would remain on by the brightness change
          - service: light.turn_on
            data:
              brightness_step: "{{ brightness |int(0) - state_attr('light.test_lights_smart', 'brightness')|int(0) }}"
            target:
              entity_id: >
                {% set brightness_from = state_attr('light.test_lights_smart', 'brightness')|int(0) %}
                {% set brightness_change = brightness |int(0) - brightness_from %}
                {{ expand('light.test') 
                  | selectattr('attributes.brightness')
                  | selectattr('attributes.brightness', '>', -brightness_change)
                  | map(attribute='entity_id') 
                  | list or 
                  expand('light.test') 
                  | selectattr('domain', '==', 'light')
                  | map(attribute='entity_id') 
                  | list if brightness != 255 else [] }}
          # Set brightness to 1 for all lights that would turn off, keeping them on
          - service: light.turn_on
            data:
              brightness_pct: 1
            target:
              entity_id: >
                {% set brightness_from = state_attr('light.test_lights_smart', 'brightness')|int(0) %}
                {% set brightness_change = brightness |int(0) - brightness_from %}
                {{ expand('light.test') 
                | selectattr('attributes.brightness')
                | selectattr('attributes.brightness', '<=', -brightness_change)
                | map(attribute='entity_id') 
                | list 
                }}
          # Set brightness to 100 for all lights currently on when slider moves to 100%, or all the lights if all are currently off
          - service: light.turn_on
            data:
              brightness_pct: 100
            target:
              entity_id: >
                {{ expand('light.test') 
                  | selectattr('attributes.brightness')
                  | map(attribute='entity_id') 
                  | list or 
                    expand('light.test') 
                  | selectattr('domain', '==', 'light')
                  | map(attribute='entity_id') 
                  | list if brightness == 255 else [] }}


1 Like

Thank you. I am on my phone so can’t read it in detail.

This is a revised code with default values in it ?

Thanks

Jeremy

Works perfectly - thank you !!

First off, thanks to everyone helping each other out here and sorry for not being more active.

I have since I created this solution tried to see if I could turn it into a custom component… and now it’s done :tada:

Introducing the Relative Brightness Light Group, a custom component that does basically the same as above but in way fewer lines of code:

light:
  - platform: relative_brightness_light_group
    name: The Office
    entities:
      - light.office_desk
      - light.office_spotlights

So please try it out, as I’ll most likely focus my effort on that one over the very convoluted light template approach I created above :sweat_smile:

Also, a small heads-up that the custom component acts a little bit different - whenever the group brightness is adjusted the individual lights are adjusted proportionally to the group change while the solution above simply increased/decreased each individual light brightness equally as much as the group brightness changed. This should make the behavior more similar to what the Hue app and others do as well. Give it a spin will ya!

1 Like

This is brilliant. I’ve just got it working (those having problems, don’t forget to use light.NAME where NAME is what you called the room in the configuration.yaml).

Thank you for developing this!

1 Like