Lux-adaptive blinds that hold 300 lx on the desk, not a fixed percentage
A closed-loop controller reads a desk lux sensor every 90 seconds and moves the blind to the position that keeps the desk between 250 and 400 lx. On an overcast day it opens fully; on a bright February noon it sits at 22 % open.
# HearthNodes recipe 06 - lux adaptive blinds
alias: "Light - closed loop blinds holding 300 lx"
id: hn_light_lux_adaptive_blinds
description: "Desk illuminance loop, 90 s cycle, 12 point maximum step"
mode: single
max_exceeded: silent
trigger:
- platform: numeric_state
entity_id: sensor.desk_illuminance
below: 250
for: "00:01:30"
- platform: numeric_state
entity_id: sensor.desk_illuminance
above: 400
for: "00:01:30"
variables:
target_lx: 300
deadband_lx: 60
measured: "{{ states('sensor.desk_illuminance') | float(300) }}"
current: "{{ state_attr('cover.study_blind','current_position') | int(50) }}"
error: "{{ measured - target_lx }}"
raw_step: "{{ (-error / 18) | round(0) }}"
clamped_step: "{{ [[raw_step, -12] | max, 12] | min }}"
computed_position: "{{ [[current + clamped_step, 0] | max, 100] | min }}"
condition:
- condition: numeric_state
entity_id: sun.sun
attribute: elevation
above: 5
- condition: state
entity_id: binary_sensor.blind_manual_override
state: "off"
- condition: state
entity_id: binary_sensor.study_window_contact
state: "off"
- condition: template
value_template: "{{ (measured - target_lx) | abs > deadband_lx }}"
action:
- service: cover.set_cover_position
target:
entity_id: cover.study_blind
data:
position: "{{ computed_position }}"
- service: counter.increment
target:
entity_id: counter.blind_moves_today
# Stale lux (>20 min without a report) hands the loop to a sun-position model.
# A manual wall-switch press suppresses the loop for 45 minutes via
# binary_sensor.blind_manual_override, which matters more than any tuning value.
Trigger · numeric_state
sensor.desk_illuminance
Desk illuminance leaves the 250–400 lx band, sustained for 90 seconds.
Step-by-step
- cover.study_blindMove the blind to the computed position in 4 % steps, capped at 12 % change per cycle so the slats do not visibly hunt.
set_cover_position · {{ computed_position }} - input_number.blind_deadbandKeep a 60 lx deadband around the 300 lx target so small cloud edges do not move the blind.
hold at 60 lx - counter.blind_moves_todayCount moves per day; anything above 25 means the deadband or the cycle time is wrong.
increment
Aim the lux sensor at what you actually read on
Mount the sensor at the height of your eyes when seated, angled slightly up toward the ceiling. A sensor lying flat on the desk reads direct sun in winter and reports 900 lx when the paper in front of you is at 200 lx. Match the sensor's field of view to the task, not to the window.
Sample every 90 s and cap the step
Faster sampling makes the blind hunt on every cloud edge; slower sampling lets the room go dark for minutes. 90 s with a 12 % maximum step is the compromise that keeps the slats visually still while still tracking a moving sun. Log the move count for a week before deciding it is tuned.
Give the wall switch 45 minutes of authority
Anyone who physically moves the blind has expressed a clear preference. Suppressing the loop for 45 minutes after a manual press is what makes the automation feel helpful rather than argumentative.
Hardware
| Model | Protocol | Link | Role in this chain |
|---|---|---|---|
| Somfy Sonesse 30 with a Matter bridge | matter |
wired | Mains-powered blind motor; Matter gives a real position percentage rather than the timed-press approximation that RF-only motors use. |
| Aqara T1 illuminance sensor | zigbee |
wireless | Desk-height lux sensor aimed at the ceiling to read reflected light rather than direct sun. |
| Window contact (any reed sensor) | zigbee |
wireless | Blocks the loop while the window is open, when the blind would hit the frame. |
When it fails
If the lux sensor goes stale for more than 20 minutes, the loop hands control to a sun-position model (elevation and azimuth) that approximates the same target. If that also reports implausible values, the blind stays where it is and a notification names the stale sensor rather than opening fully at midday.