Daylight harvesting: electric light follows the window
The ceiling circuit in a south-facing office trims itself down as daylight rises, holding a constant 500 lx on the work plane. Measured saving on a clear spring day was 41 % of the lighting energy against a fixed-100 % baseline.
# HearthNodes recipe 07 - daylight harvesting
alias: "Light - daylight harvesting on the work plane"
id: hn_light_daylight_harvest
description: "Hold 500 lx on the work plane, clamp the ceiling circuit to 12-100 percent"
mode: single
max_exceeded: silent
trigger:
- platform: numeric_state
entity_id: sensor.office_workplane_lux
below: "{{ state_attr('sensor.office_workplane_lux','last_setpoint') | float(500) - 60 }}"
- platform: numeric_state
entity_id: sensor.office_workplane_lux
above: "{{ state_attr('sensor.office_workplane_lux','last_setpoint') | float(500) + 60 }}"
variables:
target_lx: 500
daylight_share: >-
{{ (states('sensor.office_daylight_only_lux') | float(0) / target_lx * 100) | round(0) }}
harvested_pct: >-
{{ [[(100 - daylight_share | float(0)) | round(0), 12] | max, 100] | min }}
condition:
- condition: state
entity_id: binary_sensor.office_occupied
state: "on"
- condition: state
entity_id: input_boolean.harvest_hold
state: "off"
- condition: template
value_template: >-
{{ (now() - states.sensor.office_workplane_lux.last_updated).total_seconds() < 600 }}
action:
- service: light.turn_on
target:
entity_id: light.office_ceiling
data:
brightness_pct: "{{ harvested_pct }}"
transition: 8
- service: input_number.set_value
target:
entity_id: input_number.ceiling_last_level
data:
value: "{{ harvested_pct }}"
# Photosensor failure -> fixed 70 percent plus a maintenance flag. Daylight is
# never inferred from a weather API. Manual dimmer presses always suspend the
# loop for 15 minutes through input_boolean.harvest_hold.
Trigger · numeric_state
sensor.office_workplane_lux
Work-plane illuminance changes by more than 60 lx from the value that produced the current dim level.
Step-by-step
- light.office_ceilingSet the ceiling circuit to the computed level, clamped between 12 % and 100 %, with an 8 s transition so nobody sees a step.
turn_on · brightness_pct {{ harvested }} · transition 8s - sensor.office_lighting_powerLog the circuit's mean power every 5 minutes so the saving can be reported honestly against a baseline week.
log · 5min mean - input_boolean.harvest_holdSuspend harvesting for 15 minutes after anyone touches the physical dimmer.
on for 15 min after any manual dimmer press
Calibrate the ceiling sensor against a real meter
At commissioning, hold a calibrated lux meter at the work plane and adjust the ceiling sensor's gain until the two agree within 10 %. Do this at two daylight levels — one overcast and one bright — because the sensor's cosine response is wrong at low angles and the error is not linear.
Set the floor at 12 %, not at zero
Dimming to zero removes the flicker-free band and makes the transition back visible. 12 % also keeps the drivers warm and above their minimum conduction angle. If the room genuinely needs no electric light, turn the circuit off explicitly rather than dimming to nothing.
Measure the baseline for a full week first
Run the circuit at a fixed 100 % for one working week with power logging on, and only then enable harvesting. Without that baseline any saving number you quote is an estimate, and estimates get discounted by everyone who reads them.
Hardware
| Model | Protocol | Link | Role in this chain |
|---|---|---|---|
| DALI or 0-10 V dimmable driver with a Matter bridge | matter |
wired | Wired control is required here: Zigbee dimming at 12 % tends to flicker on cheap drivers and cannot hold a stable low level. |
| Ceiling-mounted closed-loop photosensor (0-10 V) | matter |
wired | Reads the combined daylight plus electric light on the work plane, which is the value the loop must hold constant. |
| Desk-level lux reference (Zigbee, for calibration only) | zigbee |
wireless | Used at commissioning to calibrate the ceiling sensor's reading against a real work-plane measurement. |
When it fails
If the photosensor fails, the circuit falls back to a fixed 70 % and the automation raises a maintenance flag; it never guesses daylight from a weather API, because a wrong guess leaves people working in the dark. Manual dimmer presses always win and always suspend the loop.