blueprint: name: Outdoor Lighting with Manual Override ("Is Dark" mode) description: > Turns lights on when an occupancy group sensor indicates presence AND a UniFi Protect camera reports darkness (its built-in "Is Dark" binary sensor). 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 occupancy sensors (e.g. camera motion or a dedicated PIR), and one timer helper per light/zone. If you have multiple outdoor cameras whose "Is Dark" readings might disagree (e.g. one is aimed near a porch light or a neighbor's floodlight), create a Binary Sensor Group with "all" logic over their isDark sensors instead of pointing at just one camera, so a single glare-affected camera can't force lights on. 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 (camera motion, PIR, etc.) covering this zone. selector: entity: domain: binary_sensor is_dark_sensor: name: Darkness Sensor / Group description: Select a UniFi Protect camera's "Is Dark" binary sensor, or a Binary Sensor Group helper (all-logic) combining several if their readings might disagree. selector: entity: domain: binary_sensor 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 is_dark_entity: !input is_dark_sensor 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/something is already # triggering occupancy (e.g. dusk falls while a car is still parked # in view of a camera-based occupancy sensor). - id: darkness_change platform: state entity_id: !input is_dark_sensor for: seconds: 10 action: - variables: override_active: "{{ is_state(timer_entity, 'active') }}" occupied: "{{ is_state(occupancy_entity, 'on') }}" is_dark: "{{ is_state(is_dark_entity, 'on') }}" - 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/darkness 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 darkness changed, but manual override is currently active (Do Nothing) - conditions: - condition: template value_template: "{{ (trigger.id in ['occupancy_change', 'darkness_change']) and override_active }}" sequence: [] # Case 3: Occupancy detected, override NOT active, and it's dark out - 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-daylight, 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 not is_state(is_dark_entity, 'on')) }} - action: light.turn_off target: !input light_target