Skip to content

Instantly share code, notes, and snippets.

@PurpleBooth
Last active August 23, 2026 15:01
Show Gist options
  • Select an option

  • Save PurpleBooth/da1b12d5ef99abbeb9edf5aee304db34 to your computer and use it in GitHub Desktop.

Select an option

Save PurpleBooth/da1b12d5ef99abbeb9edf5aee304db34 to your computer and use it in GitHub Desktop.
Home Assistant Constant Lux Light Controller
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