Last active
December 30, 2025 21:13
-
-
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
| 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