Skip to content

Instantly share code, notes, and snippets.

@PurpleBooth
Last active December 30, 2025 21:13
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
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
domain: automation
input:
# Sensors
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
# Conditions
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
# Light Groups
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
# Configuration
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:
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:
# Input references - define these first
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
# Derived variables
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) }}"
# Check if snapshot scene exists (means we need to restore, not do lux management)
scene_exists: "{{ states(snapshot_scene_entity) not in ['unknown', 'unavailable'] }}"
- choose:
# ========== PRESENCE RESTORED - Restore previous light states ==========
- conditions:
- condition: trigger
id: "presence_restored"
- condition: template
value_template: "{{ scene_exists }}"
sequence:
# Restore the scene
- service: scene.turn_on
target:
entity_id: "{{ snapshot_scene_entity }}"
data:
transition: "{{ restore_transition_seconds }}"
# Delete the scene so lux management can resume
- delay:
seconds: "{{ restore_transition_seconds + 2 }}"
- service: scene.delete
data:
entity_id: "{{ snapshot_scene_entity }}"
# ========== NO PRESENCE - Save state then turn off ==========
- conditions:
- condition: state
entity_id: !input presence_sensor
state: "off"
for:
minutes: "{{ presence_delay_minutes }}"
- condition: template
value_template: "{{ not scene_exists }}" # <-- ADD THIS LINE
sequence:
# Save current light states before turning off
- service: scene.create
data:
scene_id: "{{ snapshot_scene_id }}"
snapshot_entities: "{{ all_light_entities }}"
- delay:
seconds: 2
# Then turn off all lights
- service: light.turn_off
target:
entity_id: "{{ all_light_entities }}"
data:
transition: "{{ turn_off_transition_seconds }}"
# ========== PRESENCE DETECTED - Manage lux (only if no pending restore) ==========
- 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 }}"
# Calculate sun-based color temperature
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 %}
# Get brightness of each ceiling light
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 }}
# Get brightness of each accent light
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 }}
# Check if any lights are on
any_ceiling_on: "{{ ceiling_brightnesses | selectattr('brightness', 'gt', 0) | list | count > 0 }}"
any_accent_on: "{{ accent_brightnesses | selectattr('brightness', 'gt', 0) | list | count > 0 }}"
# Check if all lights at max
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 }}"
# Find brightest ceiling light (for dimming)
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 %}
# Find dimmest ceiling light (for brightening)
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 %}
# Find brightest accent light (for dimming)
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 %}
# Find dimmest accent light (for brightening)
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 %}
# Calculate brightness step
brightness_step: "{{ [1, (lux_diff | abs / 2) | int] | max }}"
- choose:
# ========== TOO BRIGHT - REDUCE LIGHTS ==========
- conditions:
- condition: template
value_template: "{{ lux_diff > 0 }}"
sequence:
- choose:
# First: Reduce ceiling lights if any are on
- 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 }}"
# Second: If all ceiling lights are off, reduce accent lights
- 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:
# First: Increase accent lights (if not all at max)
- conditions:
- condition: template
value_template: "{{ not all_accent_at_max and dimmest_accent.entity != 'none' }}"
sequence:
- variables:
new_brightness: "{{ [255, dimmest_accent.brightness + brightness_step] | min }}"
- choose:
- conditions:
- condition: template
value_template: "{{ dimmest_accent.brightness == 0 }}"
sequence:
- service: light.turn_on
target:
entity_id: "{{ dimmest_accent.entity }}"
data:
brightness: "{{ brightness_step }}"
transition: "{{ transition_seconds }}"
default:
- service: light.turn_on
target:
entity_id: "{{ dimmest_accent.entity }}"
data:
brightness: "{{ new_brightness }}"
transition: "{{ transition_seconds }}"
# Second: If all accent lights 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: "{{ brightness_step }}"
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