blueprint: name: Occupancy Lighting with Manual Override (Window Rooms) description: > Turns lights on when an occupancy group sensor indicates presence AND the room is dark enough (lux below threshold). Turns lights off when the group becomes inactive for the configured delay. A physical switch toggle suppresses sensor-based automation for a configurable period using a timer helper. This automation never commands the light directly in response to the switch - it assumes your switch/relay (e.g. a Shelly in edge/detached input mode) already controls the light locally and instantly, independent of Home Assistant. HA only watches for the switch event to start the override window, so it never fights a manual on/off. Create a 'Binary Sensor Group' helper for your room's occupancy sensors, a 'Sensor Group' helper (mean or min) for your room's lux sensors if you have more than one, and one timer helper per room. domain: automation input: light_target: name: Light selector: target: entity: domain: light occupancy_group: name: Occupancy Sensor / Group description: Select a single binary sensor or a Binary Sensor Group helper containing this room's sensors. selector: entity: domain: binary_sensor illuminance_sensor: name: Illuminance Sensor / Group description: Select a lux sensor (e.g. Apollo R-Pro1 LTR390) or a Sensor Group helper averaging multiple lux sensors in this room. selector: entity: domain: sensor device_class: illuminance illuminance_threshold: name: Darkness threshold (lux) description: Lights will only turn on for occupancy if measured lux is below this value. Typical indoor daylight near a window is 300-1000+ lux; a dim room is under 50 lux. Tune to taste. default: 50 selector: number: min: 0 max: 2000 step: 1 mode: box unit_of_measurement: lux switch_input: name: Physical switch input description: > Select the switch/relay's INPUT entity (e.g. a Shelly's input binary_sensor reporting the physical toggle), NOT the light entity itself and not a mirror of the light's on/off state. If this is wired to something that also changes state whenever this automation calls light.turn_on/off, you'll get a feedback loop where the automation's own actions look like manual switch events. selector: entity: domain: binary_sensor override_timer: name: Override timer selector: entity: domain: timer override_duration: name: Override duration (minutes) default: 30 selector: number: min: 1 max: 1440 step: 1 mode: box unit_of_measurement: minutes off_delay: name: Off delay (seconds) default: 5 selector: number: min: 0 max: 300 step: 1 mode: box unit_of_measurement: seconds mode: restart variables: occupancy_entity: !input occupancy_group illuminance_entity: !input illuminance_sensor lux_threshold: !input illuminance_threshold timer_entity: !input override_timer duration_minutes: !input override_duration trigger: - id: occupancy_change platform: state entity_id: !input occupancy_group - id: manual_switch platform: state entity_id: !input switch_input - id: timer_finished platform: event event_type: timer.finished event_data: entity_id: !input override_timer # Re-evaluate if it gets dark while someone is already sitting in the room - id: illuminance_change platform: state entity_id: !input illuminance_sensor for: seconds: 10 action: - variables: override_active: "{{ is_state(timer_entity, 'active') }}" occupied: "{{ is_state(occupancy_entity, 'on') }}" is_dark: "{{ states(illuminance_entity) | float(9999) < lux_threshold }}" - choose: # Case 1: Manual physical switch flipped (either direction). # The Shelly's edge input mode has already flipped the relay locally - # HA must NOT issue a light.turn_on/off here, or it will fight the # person's manual action. We only start the override timer so that # occupancy/lux logic stays out of the way for the configured window. - conditions: - condition: template value_template: "{{ trigger.id == 'manual_switch' }}" sequence: - action: timer.start target: entity_id: !input override_timer data: # We multiply by 60 because timer.start data maps natively to HH:MM:SS or raw seconds duration: "{{ duration_minutes * 60 }}" # Case 2: Occupancy or lux changed, but manual override is currently active (Do Nothing) - conditions: - condition: template value_template: "{{ (trigger.id in ['occupancy_change', 'illuminance_change']) and override_active }}" sequence: [] # Case 3: Occupancy detected, override NOT active, and room is dark enough - conditions: - condition: template value_template: "{{ occupied and is_dark }}" sequence: - action: light.turn_on target: !input light_target # Default Case: Not occupied, or occupied-but-bright-enough, or timer finished -> sequence handles turning off default: - delay: seconds: !input off_delay - condition: template value_template: > {{ not is_state(timer_entity, 'active') and (not is_state(occupancy_entity, 'on') or states(illuminance_entity) | float(9999) >= lux_threshold) }} - action: light.turn_off target: !input light_target