Last active
August 23, 2026 15:01
-
-
Save PurpleBooth/da1b12d5ef99abbeb9edf5aee304db34 to your computer and use it in GitHub Desktop.
Home Assistant Constant Lux Light Controller
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Home Assistant Constant Lux Light Controller | |
| blueprint: | |
| name: "Constant Lux Light Controller" | |
| description: > | |
| Maintains a constant lux level by adjusting light brightness. | |
| Features: | |
| - Prioritized light groups (ceiling vs accent) | |
| - Sun-based color temperature for ceiling lights | |
| - Presence detection with delay | |
| - State restoration when returning to room | |
| - Smooth transitions | |
| - Initial brightness boost when lights are off (avoids starting from 0) | |
| - Aggressive turn-off when ambient lux is well above target | |
| domain: automation | |
| input: | |
| lux_sensor: | |
| name: Lux Sensor | |
| description: "The light sensor to monitor lux levels" | |
| selector: | |
| entity: | |
| domain: sensor | |
| device_class: illuminance | |
| presence_sensor: | |
| name: Presence Sensor | |
| description: "Binary sensor for room presence detection" | |
| selector: | |
| entity: | |
| domain: binary_sensor | |
| awake_sensor: | |
| name: Awake Sensor | |
| description: "Binary sensor that must be ON for automation to run" | |
| selector: | |
| entity: | |
| domain: binary_sensor | |
| bypass_switch: | |
| name: Bypass Switch | |
| description: "Input boolean that disables the automation when ON" | |
| selector: | |
| entity: | |
| domain: input_boolean | |
| ceiling_lights: | |
| name: Ceiling Lights (Primary) | |
| description: "Primary lights - dimmed first when too bright, brightened last when too dark. Color temperature follows sun." | |
| selector: | |
| entity: | |
| domain: light | |
| multiple: true | |
| accent_lights: | |
| name: Accent Lights (Secondary) | |
| description: "Secondary lights - dimmed after ceiling lights are off, brightened first when too dark. Maintains existing colors." | |
| selector: | |
| entity: | |
| domain: light | |
| multiple: true | |
| target_lux: | |
| name: Target Lux Helper | |
| description: "Input number helper that contains the target lux level to maintain" | |
| selector: | |
| entity: | |
| domain: input_number | |
| presence_delay: | |
| name: Presence Off Delay | |
| description: "Minutes to wait after presence is lost before turning off lights" | |
| default: 3 | |
| selector: | |
| number: | |
| min: 0 | |
| max: 30 | |
| unit_of_measurement: minutes | |
| mode: slider | |
| transition_time: | |
| name: Adjustment Transition Time | |
| description: "Transition time in seconds for brightness adjustments" | |
| default: 30 | |
| selector: | |
| number: | |
| min: 0 | |
| max: 120 | |
| unit_of_measurement: seconds | |
| mode: slider | |
| turn_off_transition: | |
| name: Turn Off Transition Time | |
| description: "Transition time in seconds when turning off lights (no presence)" | |
| default: 5 | |
| selector: | |
| number: | |
| min: 0 | |
| max: 30 | |
| unit_of_measurement: seconds | |
| mode: slider | |
| restore_transition: | |
| name: Restore Transition Time | |
| description: "Transition time in seconds when restoring lights on return" | |
| default: 2 | |
| selector: | |
| number: | |
| min: 0 | |
| max: 30 | |
| unit_of_measurement: seconds | |
| mode: slider | |
| min_color_temp: | |
| name: Minimum Color Temperature | |
| description: "Color temperature at sunrise/sunset (warm)" | |
| default: 2000 | |
| selector: | |
| color_temp: | |
| min_mireds: 153 | |
| max_mireds: 500 | |
| unit: kelvin | |
| max_color_temp: | |
| name: Maximum Color Temperature | |
| description: "Color temperature at solar noon (cool)" | |
| default: 6500 | |
| selector: | |
| color_temp: | |
| min_mireds: 153 | |
| max_mireds: 500 | |
| unit: kelvin | |
| scene_name: | |
| name: Snapshot Scene Name | |
| description: "Unique name for the scene used to save/restore light states (no spaces, use underscores)" | |
| default: "lux_controller_snapshot" | |
| selector: | |
| text: | |
| overshoot_threshold: | |
| name: Overshoot Turn-Off Threshold | |
| description: "When ambient lux exceeds target by this amount, turn all lights off immediately (0 = disabled)" | |
| default: 100 | |
| selector: | |
| number: | |
| min: 0 | |
| max: 500 | |
| unit_of_measurement: lux | |
| mode: slider | |
| min_initial_brightness: | |
| name: Minimum Initial Brightness | |
| description: "When turning on lights from off, start at at least this brightness (0-255) instead of incrementing from 0" | |
| default: 50 | |
| selector: | |
| number: | |
| min: 0 | |
| max: 255 | |
| unit_of_measurement: brightness | |
| mode: slider | |
| mode: single | |
| trigger: | |
| - platform: time_pattern | |
| minutes: "/1" | |
| id: "time_trigger" | |
| - platform: state | |
| entity_id: !input presence_sensor | |
| from: "off" | |
| to: "on" | |
| id: "presence_restored" | |
| condition: | |
| - condition: state | |
| entity_id: !input awake_sensor | |
| state: "on" | |
| - condition: state | |
| entity_id: !input bypass_switch | |
| state: "off" | |
| action: | |
| - variables: | |
| lux_sensor_entity: !input lux_sensor | |
| target_lux_entity: !input target_lux | |
| ceiling_light_entities: !input ceiling_lights | |
| accent_light_entities: !input accent_lights | |
| presence_delay_minutes: !input presence_delay | |
| transition_seconds: !input transition_time | |
| turn_off_transition_seconds: !input turn_off_transition | |
| restore_transition_seconds: !input restore_transition | |
| min_color_temp_kelvin: !input min_color_temp | |
| max_color_temp_kelvin: !input max_color_temp | |
| snapshot_scene_id: !input scene_name | |
| overshoot_lux: !input overshoot_threshold | |
| min_init_brightness: !input min_initial_brightness | |
| all_light_entities: "{{ ceiling_light_entities + accent_light_entities }}" | |
| snapshot_scene_entity: "scene.{{ snapshot_scene_id }}" | |
| target_lux_value: "{{ states(target_lux_entity) | float(60) }}" | |
| scene_exists: "{{ states(snapshot_scene_entity) not in ['unknown', 'unavailable'] }}" | |
| - choose: | |
| - conditions: | |
| - condition: trigger | |
| id: "presence_restored" | |
| - condition: template | |
| value_template: "{{ scene_exists }}" | |
| sequence: | |
| - service: scene.turn_on | |
| target: | |
| entity_id: "{{ snapshot_scene_entity }}" | |
| data: | |
| transition: "{{ restore_transition_seconds }}" | |
| - delay: | |
| seconds: "{{ restore_transition_seconds + 2 }}" | |
| - service: scene.delete | |
| data: | |
| entity_id: "{{ snapshot_scene_entity }}" | |
| - conditions: | |
| - condition: state | |
| entity_id: !input presence_sensor | |
| state: "off" | |
| for: | |
| minutes: "{{ presence_delay_minutes }}" | |
| - condition: template | |
| value_template: "{{ not scene_exists }}" | |
| sequence: | |
| - service: scene.create | |
| data: | |
| scene_id: "{{ snapshot_scene_id }}" | |
| snapshot_entities: "{{ all_light_entities }}" | |
| - delay: | |
| seconds: 2 | |
| - service: light.turn_off | |
| target: | |
| entity_id: "{{ all_light_entities }}" | |
| data: | |
| transition: "{{ turn_off_transition_seconds }}" | |
| - conditions: | |
| - condition: state | |
| entity_id: !input presence_sensor | |
| state: "on" | |
| - condition: template | |
| value_template: "{{ not scene_exists }}" | |
| sequence: | |
| - variables: | |
| current_lux: "{{ states(lux_sensor_entity) | float(0) }}" | |
| lux_diff: "{{ current_lux - target_lux_value }}" | |
| sun_elevation: "{{ state_attr('sun.sun', 'elevation') | float(0) }}" | |
| color_temp_range: "{{ max_color_temp_kelvin - min_color_temp_kelvin }}" | |
| sun_color_temp_kelvin: >- | |
| {% if sun_elevation <= 0 %} | |
| {{ min_color_temp_kelvin }} | |
| {% elif sun_elevation >= 90 %} | |
| {{ max_color_temp_kelvin }} | |
| {% else %} | |
| {{ (min_color_temp_kelvin + (sun_elevation / 90) * color_temp_range) | int }} | |
| {% endif %} | |
| ceiling_brightnesses: >- | |
| {% set ns = namespace(brightnesses=[]) %} | |
| {% for light in ceiling_light_entities %} | |
| {% set brightness = state_attr(light, 'brightness') | int(0) if is_state(light, 'on') else 0 %} | |
| {% set ns.brightnesses = ns.brightnesses + [{'entity': light, 'brightness': brightness}] %} | |
| {% endfor %} | |
| {{ ns.brightnesses }} | |
| accent_brightnesses: >- | |
| {% set ns = namespace(brightnesses=[]) %} | |
| {% for light in accent_light_entities %} | |
| {% set brightness = state_attr(light, 'brightness') | int(0) if is_state(light, 'on') else 0 %} | |
| {% set ns.brightnesses = ns.brightnesses + [{'entity': light, 'brightness': brightness}] %} | |
| {% endfor %} | |
| {{ ns.brightnesses }} | |
| any_ceiling_on: "{{ ceiling_brightnesses | selectattr('brightness', 'gt', 0) | list | count > 0 }}" | |
| any_accent_on: "{{ accent_brightnesses | selectattr('brightness', 'gt', 0) | list | count > 0 }}" | |
| any_lights_on: "{{ any_ceiling_on or any_accent_on }}" | |
| all_accent_at_max: "{{ accent_brightnesses | selectattr('brightness', 'lt', 255) | list | count == 0 and accent_brightnesses | length > 0 }}" | |
| all_ceiling_at_max: "{{ ceiling_brightnesses | selectattr('brightness', 'lt', 255) | list | count == 0 and ceiling_brightnesses | length > 0 }}" | |
| brightest_ceiling: >- | |
| {% set sorted = ceiling_brightnesses | sort(attribute='brightness', reverse=true) %} | |
| {% if sorted | length > 0 and sorted[0].brightness > 0 %} | |
| {{ sorted[0] }} | |
| {% else %} | |
| {{ {'entity': 'none', 'brightness': 0} }} | |
| {% endif %} | |
| dimmest_ceiling: >- | |
| {% set sorted = ceiling_brightnesses | sort(attribute='brightness') %} | |
| {% if sorted | length > 0 and sorted[0].brightness < 255 %} | |
| {{ sorted[0] }} | |
| {% else %} | |
| {{ {'entity': 'none', 'brightness': 255} }} | |
| {% endif %} | |
| brightest_accent: >- | |
| {% set sorted = accent_brightnesses | sort(attribute='brightness', reverse=true) %} | |
| {% if sorted | length > 0 and sorted[0].brightness > 0 %} | |
| {{ sorted[0] }} | |
| {% else %} | |
| {{ {'entity': 'none', 'brightness': 0} }} | |
| {% endif %} | |
| dimmest_accent: >- | |
| {% set sorted = accent_brightnesses | sort(attribute='brightness') %} | |
| {% if sorted | length > 0 and sorted[0].brightness < 255 %} | |
| {{ sorted[0] }} | |
| {% else %} | |
| {{ {'entity': 'none', 'brightness': 255} }} | |
| {% endif %} | |
| brightness_step: "{{ [5, (lux_diff | abs / 2) | int] | max }}" | |
| - choose: | |
| # ========== WAY TOO BRIGHT - TURN OFF ALL LIGHTS ========== | |
| - conditions: | |
| - condition: template | |
| value_template: "{{ overshoot_lux > 0 and lux_diff > overshoot_lux and any_lights_on }}" | |
| sequence: | |
| - service: light.turn_off | |
| target: | |
| entity_id: "{{ all_light_entities }}" | |
| data: | |
| transition: "{{ transition_seconds }}" | |
| # ========== TOO BRIGHT - REDUCE LIGHTS ========== | |
| - conditions: | |
| - condition: template | |
| value_template: "{{ lux_diff > 0 }}" | |
| sequence: | |
| - choose: | |
| - conditions: | |
| - condition: template | |
| value_template: "{{ any_ceiling_on and brightest_ceiling.entity != 'none' }}" | |
| sequence: | |
| - variables: | |
| new_brightness: "{{ [0, brightest_ceiling.brightness - brightness_step] | max }}" | |
| - choose: | |
| - conditions: | |
| - condition: template | |
| value_template: "{{ new_brightness <= 0 }}" | |
| sequence: | |
| - service: light.turn_off | |
| target: | |
| entity_id: "{{ brightest_ceiling.entity }}" | |
| data: | |
| transition: "{{ transition_seconds }}" | |
| default: | |
| - service: light.turn_on | |
| target: | |
| entity_id: "{{ brightest_ceiling.entity }}" | |
| data: | |
| brightness: "{{ new_brightness }}" | |
| color_temp_kelvin: "{{ sun_color_temp_kelvin }}" | |
| transition: "{{ transition_seconds }}" | |
| - conditions: | |
| - condition: template | |
| value_template: "{{ not any_ceiling_on and brightest_accent.entity != 'none' }}" | |
| sequence: | |
| - variables: | |
| new_brightness: "{{ [0, brightest_accent.brightness - brightness_step] | max }}" | |
| - choose: | |
| - conditions: | |
| - condition: template | |
| value_template: "{{ new_brightness <= 0 }}" | |
| sequence: | |
| - service: light.turn_off | |
| target: | |
| entity_id: "{{ brightest_accent.entity }}" | |
| data: | |
| transition: "{{ transition_seconds }}" | |
| default: | |
| - service: light.turn_on | |
| target: | |
| entity_id: "{{ brightest_accent.entity }}" | |
| data: | |
| brightness: "{{ new_brightness }}" | |
| transition: "{{ transition_seconds }}" | |
| # ========== TOO DARK - INCREASE LIGHTS ========== | |
| - conditions: | |
| - condition: template | |
| value_template: "{{ lux_diff < 0 }}" | |
| sequence: | |
| - choose: | |
| # All lights off — turn on with useful initial brightness | |
| - conditions: | |
| - condition: template | |
| value_template: "{{ not any_lights_on }}" | |
| sequence: | |
| - variables: | |
| initial_brightness: >- | |
| {{ [255, [min_init_brightness, (lux_diff | abs / 2) | int + min_init_brightness] | max] | min }} | |
| - service: light.turn_on | |
| target: | |
| entity_id: "{{ accent_light_entities }}" | |
| data: | |
| brightness: "{{ initial_brightness }}" | |
| transition: "{{ transition_seconds }}" | |
| - choose: | |
| - conditions: | |
| - condition: template | |
| value_template: "{{ lux_diff | abs > 100 }}" | |
| sequence: | |
| - service: light.turn_on | |
| target: | |
| entity_id: "{{ ceiling_light_entities }}" | |
| data: | |
| brightness: "{{ initial_brightness }}" | |
| color_temp_kelvin: "{{ sun_color_temp_kelvin }}" | |
| transition: "{{ transition_seconds }}" | |
| # Accent lights on — increase incrementally | |
| - conditions: | |
| - condition: template | |
| value_template: "{{ any_accent_on and not all_accent_at_max and dimmest_accent.entity != 'none' }}" | |
| sequence: | |
| - variables: | |
| new_brightness: "{{ [255, dimmest_accent.brightness + brightness_step] | min }}" | |
| - service: light.turn_on | |
| target: | |
| entity_id: "{{ dimmest_accent.entity }}" | |
| data: | |
| brightness: "{{ new_brightness }}" | |
| transition: "{{ transition_seconds }}" | |
| # All accent at max — increase ceiling lights | |
| - conditions: | |
| - condition: template | |
| value_template: "{{ all_accent_at_max and dimmest_ceiling.entity != 'none' }}" | |
| sequence: | |
| - variables: | |
| new_brightness: "{{ [255, dimmest_ceiling.brightness + brightness_step] | min }}" | |
| - choose: | |
| - conditions: | |
| - condition: template | |
| value_template: "{{ dimmest_ceiling.brightness == 0 }}" | |
| sequence: | |
| - service: light.turn_on | |
| target: | |
| entity_id: "{{ dimmest_ceiling.entity }}" | |
| data: | |
| brightness: "{{ [min_init_brightness, new_brightness] | max }}" | |
| color_temp_kelvin: "{{ sun_color_temp_kelvin }}" | |
| transition: "{{ transition_seconds }}" | |
| default: | |
| - service: light.turn_on | |
| target: | |
| entity_id: "{{ dimmest_ceiling.entity }}" | |
| data: | |
| brightness: "{{ new_brightness }}" | |
| color_temp_kelvin: "{{ sun_color_temp_kelvin }}" | |
| transition: "{{ transition_seconds }}" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment