From 055ad1b1f8309a6aef6c84b7baaa94103bab14ad Mon Sep 17 00:00:00 2001 From: Bohdan Bahno Date: Sun, 26 Jul 2026 11:06:26 +0100 Subject: [PATCH] Add outdoor template --- ...r_Lighting_with_Manual_Override_UniFi.yaml | 168 ++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 Outdoor_Lighting_with_Manual_Override_UniFi.yaml diff --git a/Outdoor_Lighting_with_Manual_Override_UniFi.yaml b/Outdoor_Lighting_with_Manual_Override_UniFi.yaml new file mode 100644 index 0000000..0f94229 --- /dev/null +++ b/Outdoor_Lighting_with_Manual_Override_UniFi.yaml @@ -0,0 +1,168 @@ +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