Files
zorruno-homeassistant/packages/outdoor_lighting_control.yaml
2026-07-21 13:40:06 +12:00

1246 lines
50 KiB
YAML

###############################################################################
# PACKAGE: Outdoor lighting controls
###############################################################################
#
# Version:
# 2026-07-20 v0.3
# - Synchronized the documented Number 16 schedule with the implementation.
#
# 2026-05-09 v0.2
# - Refactored scheduled control to use editable schedule lists.
# - Switched lights now use explicit ON/OFF schedule entries instead of
# ON plus duration.
# - Added dimmer schedule event tracking so long ramps are not restarted by
# the 5 minute scheduler.
# - Added manual-on start tracking for switched lights so scheduled events
# can override a temporary manual-on period.
#
# 2026-05-09 v0.1
# - Initial package for outdoor switched and dimmable lighting.
#
# Notes:
# - Edit schedules inside the per-light reconcile/apply scripts below.
# - The schedule rows are deliberately near the top of each script.
# - Dimmable schedule format:
# trigger: sunset, offset_minutes, target_brightness, ramp_minutes
# trigger: time, at, target_brightness, ramp_minutes
# - Switched schedule format:
# trigger: sunset, offset_minutes, target_state
# trigger: time, at, target_state
# - For dimmable schedules, fixed time entries override sun based entries if
# the real sunset offset would occur after the following fixed time.
# - The scheduler runs every 5 minutes. Dimming ramps also step every 5 minutes
# by default.
# - Manual-off suppression lasts until the next day's first scheduled ON.
#
###############################################################################
input_boolean:
outdoor_lighting_automation_enable:
name: Outdoor Lighting Automation - Master Enable
icon: mdi:outdoor-lamp
initial: true
outdoor_lighting_deck_coloured_automation_enable:
name: Outdoor Lighting Automation - Deck Coloured Lights
icon: mdi:string-lights
initial: true
outdoor_lighting_patio_automation_enable:
name: Outdoor Lighting Automation - Patio Lights
icon: mdi:coach-lamp
initial: true
outdoor_lighting_number_16_automation_enable:
name: Outdoor Lighting Automation - View Road Number 16
icon: mdi:numeric-16-box
initial: true
outdoor_lighting_main_entrance_automation_enable:
name: Outdoor Lighting Automation - Main Entrance Outside LEDs
icon: mdi:led-strip-variant
initial: true
input_datetime:
outdoor_lighting_deck_suppress_until:
name: Outdoor Lighting Deck Suppress Until
has_date: true
has_time: true
outdoor_lighting_deck_manual_on_started:
name: Outdoor Lighting Deck Manual On Started
has_date: true
has_time: true
outdoor_lighting_patio_suppress_until:
name: Outdoor Lighting Patio Suppress Until
has_date: true
has_time: true
outdoor_lighting_patio_manual_on_started:
name: Outdoor Lighting Patio Manual On Started
has_date: true
has_time: true
outdoor_lighting_number_16_last_schedule_event:
name: Outdoor Lighting Number 16 Last Schedule Event
has_date: true
has_time: true
outdoor_lighting_main_entrance_suppress_until:
name: Outdoor Lighting Main Entrance Suppress Until
has_date: true
has_time: true
outdoor_lighting_main_entrance_last_schedule_event:
name: Outdoor Lighting Main Entrance Last Schedule Event
has_date: true
has_time: true
outdoor_lighting_main_entrance_last_override_press:
name: Outdoor Lighting Main Entrance Last Override Press
has_date: true
has_time: true
input_select:
outdoor_lighting_main_entrance_override_mode:
name: Outdoor Lighting Main Entrance Override Mode
options:
- none
- return_to_schedule
- turn_off
initial: none
icon: mdi:gesture-tap-button
timer:
outdoor_lighting_deck_manual_on:
name: Outdoor Lighting Deck Manual On Timer
duration: "01:00:00"
outdoor_lighting_patio_manual_on:
name: Outdoor Lighting Patio Manual On Timer
duration: "01:00:00"
outdoor_lighting_main_entrance_override:
name: Outdoor Lighting Main Entrance Override Timer
duration: "01:00:00"
script:
#############################################################################
# Shared dimmable ramp scripts
#############################################################################
outdoor_lighting_ramp_number_16:
alias: Outdoor Lighting - Ramp View Road Number 16
mode: restart
fields:
target_brightness:
description: Target brightness from 0 to 100. Zero means off.
ramp_seconds:
description: Ramp duration in seconds.
sequence:
- variables:
brightness_entity: number.front_entranceway_led_light_controls_number_16_led_light_brightness
switch_entity: switch.front_entranceway_led_light_controls_number_16_led_light
ramp_update_seconds: 300
target: >
{% set raw = target_brightness | int(0) %}
{{ [[raw, 0] | max, 100] | min }}
ramp_s: "{{ ramp_seconds | int(0) }}"
current: >
{% if is_state(switch_entity, 'on') %}
{{ states(brightness_entity) | float(0) }}
{% else %}
0
{% endif %}
raw_steps: >
{% if ramp_s | int(0) <= 0 %}
0
{% else %}
{{ ((ramp_s | int(0)) / (ramp_update_seconds | int(300))) | round(0, 'ceil') | int }}
{% endif %}
steps: >
{% if ramp_s | int(0) <= 0 %}
1
{% else %}
{{ (raw_steps | int(0)) + 1 }}
{% endif %}
step_delay_seconds: >
{% if (steps | int(1)) <= 1 %}
0
{% else %}
{{ ((ramp_s | int(0)) / ((steps | int(1)) - 1)) | round(0, 'ceil') | int }}
{% endif %}
- repeat:
count: "{{ steps | int(1) }}"
sequence:
- variables:
next_value: >
{% set val = (current | float(0)) + (((target | float(0)) - (current | float(0))) * (repeat.index | int) / (steps | int(1))) %}
{{ [[val | round(0) | int, 0] | max, 100] | min }}
- action: number.set_value
target:
entity_id: number.front_entranceway_led_light_controls_number_16_led_light_brightness
data:
value: "{{ next_value | int(0) }}"
- if:
- condition: template
value_template: "{{ (target | int(0)) > 0 and is_state('switch.front_entranceway_led_light_controls_number_16_led_light', 'off') }}"
then:
- action: switch.turn_on
target:
entity_id: switch.front_entranceway_led_light_controls_number_16_led_light
- if:
- condition: template
value_template: "{{ repeat.index < (steps | int(1)) }}"
then:
- delay:
seconds: "{{ step_delay_seconds | int(0) }}"
- if:
- condition: template
value_template: "{{ (target | int(0)) <= 0 }}"
then:
- action: switch.turn_off
target:
entity_id: switch.front_entranceway_led_light_controls_number_16_led_light
outdoor_lighting_ramp_main_entrance:
alias: Outdoor Lighting - Ramp Main Entrance Outside LEDs
mode: restart
fields:
target_brightness:
description: Target brightness from 0 to 100. Zero means off.
ramp_seconds:
description: Ramp duration in seconds.
sequence:
- variables:
brightness_entity: number.front_entranceway_led_light_controls_main_entrance_outside_led_lights_brightness
switch_entity: switch.front_entranceway_led_light_controls_main_entrance_outside_led_lights
ramp_update_seconds: 300
target: >
{% set raw = target_brightness | int(0) %}
{{ [[raw, 0] | max, 100] | min }}
ramp_s: "{{ ramp_seconds | int(0) }}"
current: >
{% if is_state(switch_entity, 'on') %}
{{ states(brightness_entity) | float(0) }}
{% else %}
0
{% endif %}
raw_steps: >
{% if ramp_s | int(0) <= 0 %}
0
{% else %}
{{ ((ramp_s | int(0)) / (ramp_update_seconds | int(300))) | round(0, 'ceil') | int }}
{% endif %}
steps: >
{% if ramp_s | int(0) <= 0 %}
1
{% else %}
{{ (raw_steps | int(0)) + 1 }}
{% endif %}
step_delay_seconds: >
{% if (steps | int(1)) <= 1 %}
0
{% else %}
{{ ((ramp_s | int(0)) / ((steps | int(1)) - 1)) | round(0, 'ceil') | int }}
{% endif %}
- repeat:
count: "{{ steps | int(1) }}"
sequence:
- variables:
next_value: >
{% set val = (current | float(0)) + (((target | float(0)) - (current | float(0))) * (repeat.index | int) / (steps | int(1))) %}
{{ [[val | round(0) | int, 0] | max, 100] | min }}
- action: number.set_value
target:
entity_id: number.front_entranceway_led_light_controls_main_entrance_outside_led_lights_brightness
data:
value: "{{ next_value | int(0) }}"
- if:
- condition: template
value_template: "{{ (target | int(0)) > 0 and is_state('switch.front_entranceway_led_light_controls_main_entrance_outside_led_lights', 'off') }}"
then:
- action: switch.turn_on
target:
entity_id: switch.front_entranceway_led_light_controls_main_entrance_outside_led_lights
- if:
- condition: template
value_template: "{{ repeat.index < (steps | int(1)) }}"
then:
- delay:
seconds: "{{ step_delay_seconds | int(0) }}"
- if:
- condition: template
value_template: "{{ (target | int(0)) <= 0 }}"
then:
- action: switch.turn_off
target:
entity_id: switch.front_entranceway_led_light_controls_main_entrance_outside_led_lights
#############################################################################
# 1) Outdoor Deck Coloured Lights
# Switch: switch.esp_downstloungeoutside_relay_1_outside_lights
# Override: binary_sensor.esp_downstloungeoutside_button_1_outside_lights
#############################################################################
outdoor_lighting_reconcile_deck_coloured:
alias: Outdoor Lighting - Reconcile Deck Coloured Lights
mode: restart
sequence:
- condition: state
entity_id: input_boolean.outdoor_lighting_automation_enable
state: "on"
- condition: state
entity_id: input_boolean.outdoor_lighting_deck_coloured_automation_enable
state: "on"
###########################################################################
# EDIT SCHEDULE HERE
# Format: trigger, offset_minutes, target_state
# Current schedule:
# - sunset +60, ON
# - sunset +240, OFF
###########################################################################
- variables:
schedule:
- trigger: sunset
offset_minutes: 60
target_state: "on"
- trigger: sunset
offset_minutes: 240
target_state: "off"
- variables:
selected_schedule: >
{% set now_ts = as_timestamp(now()) %}
{% set next_setting_raw = state_attr('sun.sun', 'next_setting') %}
{% if next_setting_raw %}
{% set next_setting = as_datetime(next_setting_raw) | as_local %}
{% if next_setting.date() == now().date() %}
{% set today_sunset = next_setting %}
{% else %}
{% set today_sunset = next_setting - timedelta(days=1) %}
{% endif %}
{% else %}
{% set today_sunset = none %}
{% endif %}
{% set ns = namespace(best_ts=0, best_state='off') %}
{% for day_offset in [-1, 0] %}
{% for item in schedule %}
{% if item.trigger == 'sunset' and today_sunset %}
{% set event_dt = today_sunset + timedelta(days=day_offset) + timedelta(minutes=item.offset_minutes | int(0)) %}
{% elif item.trigger == 'time' %}
{% set event_dt = today_at(item.at) + timedelta(days=day_offset) %}
{% else %}
{% set event_dt = none %}
{% endif %}
{% if event_dt %}
{% set event_ts = as_timestamp(event_dt, 0) %}
{% if event_ts <= now_ts and event_ts > ns.best_ts %}
{% set ns.best_ts = event_ts %}
{% set ns.best_state = item.target_state %}
{% endif %}
{% endif %}
{% endfor %}
{% endfor %}
{{ ns.best_state }}|{{ ns.best_ts | int(0) }}
selected_target_state: "{{ selected_schedule.split('|')[0] }}"
selected_event_ts: "{{ selected_schedule.split('|')[1] | int(0) }}"
suppress_active: >
{% set ts = as_timestamp(states('input_datetime.outdoor_lighting_deck_suppress_until'), 0) %}
{{ ts > as_timestamp(now()) }}
manual_on_active: "{{ is_state('timer.outdoor_lighting_deck_manual_on', 'active') }}"
manual_on_started_ts: "{{ as_timestamp(states('input_datetime.outdoor_lighting_deck_manual_on_started'), 0) }}"
manual_on_still_valid: >
{{ manual_on_active | bool and (selected_event_ts | int(0)) <= (manual_on_started_ts | int(0)) }}
should_be_on: >
{% if suppress_active | bool %}
false
{% elif manual_on_still_valid | bool %}
true
{% else %}
{{ selected_target_state == 'on' }}
{% endif %}
schedule_has_overridden_manual: >
{{ manual_on_active | bool and not (manual_on_still_valid | bool) }}
- if:
- condition: template
value_template: "{{ schedule_has_overridden_manual | bool }}"
then:
- action: timer.cancel
target:
entity_id: timer.outdoor_lighting_deck_manual_on
- choose:
- conditions:
- condition: template
value_template: "{{ should_be_on | bool }}"
sequence:
- action: switch.turn_on
target:
entity_id: switch.esp_downstloungeoutside_relay_1_outside_lights
default:
- action: switch.turn_off
target:
entity_id: switch.esp_downstloungeoutside_relay_1_outside_lights
#############################################################################
# 2) Patio Lights
# Switch: switch.esp_garageentrylights_relay_2_patio_lights
# Override: binary_sensor.esp_garageentrylights_button_2_patio_lights
#############################################################################
outdoor_lighting_reconcile_patio:
alias: Outdoor Lighting - Reconcile Patio Lights
mode: restart
sequence:
- condition: state
entity_id: input_boolean.outdoor_lighting_automation_enable
state: "on"
- condition: state
entity_id: input_boolean.outdoor_lighting_patio_automation_enable
state: "on"
###########################################################################
# EDIT SCHEDULE HERE
# Format: trigger, offset_minutes, target_state
# Current schedule:
# - sunset -30, ON
# - sunset +150, OFF
###########################################################################
- variables:
schedule:
- trigger: sunset
offset_minutes: -30
target_state: "on"
- trigger: sunset
offset_minutes: 150
target_state: "off"
- variables:
selected_schedule: >
{% set now_ts = as_timestamp(now()) %}
{% set next_setting_raw = state_attr('sun.sun', 'next_setting') %}
{% if next_setting_raw %}
{% set next_setting = as_datetime(next_setting_raw) | as_local %}
{% if next_setting.date() == now().date() %}
{% set today_sunset = next_setting %}
{% else %}
{% set today_sunset = next_setting - timedelta(days=1) %}
{% endif %}
{% else %}
{% set today_sunset = none %}
{% endif %}
{% set ns = namespace(best_ts=0, best_state='off') %}
{% for day_offset in [-1, 0] %}
{% for item in schedule %}
{% if item.trigger == 'sunset' and today_sunset %}
{% set event_dt = today_sunset + timedelta(days=day_offset) + timedelta(minutes=item.offset_minutes | int(0)) %}
{% elif item.trigger == 'time' %}
{% set event_dt = today_at(item.at) + timedelta(days=day_offset) %}
{% else %}
{% set event_dt = none %}
{% endif %}
{% if event_dt %}
{% set event_ts = as_timestamp(event_dt, 0) %}
{% if event_ts <= now_ts and event_ts > ns.best_ts %}
{% set ns.best_ts = event_ts %}
{% set ns.best_state = item.target_state %}
{% endif %}
{% endif %}
{% endfor %}
{% endfor %}
{{ ns.best_state }}|{{ ns.best_ts | int(0) }}
selected_target_state: "{{ selected_schedule.split('|')[0] }}"
selected_event_ts: "{{ selected_schedule.split('|')[1] | int(0) }}"
suppress_active: >
{% set ts = as_timestamp(states('input_datetime.outdoor_lighting_patio_suppress_until'), 0) %}
{{ ts > as_timestamp(now()) }}
manual_on_active: "{{ is_state('timer.outdoor_lighting_patio_manual_on', 'active') }}"
manual_on_started_ts: "{{ as_timestamp(states('input_datetime.outdoor_lighting_patio_manual_on_started'), 0) }}"
manual_on_still_valid: >
{{ manual_on_active | bool and (selected_event_ts | int(0)) <= (manual_on_started_ts | int(0)) }}
should_be_on: >
{% if suppress_active | bool %}
false
{% elif manual_on_still_valid | bool %}
true
{% else %}
{{ selected_target_state == 'on' }}
{% endif %}
schedule_has_overridden_manual: >
{{ manual_on_active | bool and not (manual_on_still_valid | bool) }}
- if:
- condition: template
value_template: "{{ schedule_has_overridden_manual | bool }}"
then:
- action: timer.cancel
target:
entity_id: timer.outdoor_lighting_patio_manual_on
- choose:
- conditions:
- condition: template
value_template: "{{ should_be_on | bool }}"
sequence:
- action: switch.turn_on
target:
entity_id: switch.esp_garageentrylights_relay_2_patio_lights
default:
- action: switch.turn_off
target:
entity_id: switch.esp_garageentrylights_relay_2_patio_lights
#############################################################################
# 3) View Road Street Number 16
# Brightness: number.front_entranceway_led_light_controls_number_16_led_light_brightness
# Switch: switch.front_entranceway_led_light_controls_number_16_led_light
#############################################################################
outdoor_lighting_apply_number_16_current_target:
alias: Outdoor Lighting - Apply Current Target - View Road Number 16
mode: restart
fields:
force_reconcile:
description: Force immediate reconciliation even if the schedule event has already been applied.
ramp_seconds:
description: Optional ramp seconds override. Use -1 or omit to use the schedule ramp.
sequence:
- condition: state
entity_id: input_boolean.outdoor_lighting_automation_enable
state: "on"
- condition: state
entity_id: input_boolean.outdoor_lighting_number_16_automation_enable
state: "on"
###########################################################################
# EDIT SCHEDULE HERE
# Format: trigger, offset_minutes/at, target_brightness, ramp_minutes
# Current schedule:
# - sunset -30, 60, 30
# - 20:00, 50, 120
# - 01:00, 20, 120
# - 06:00, 0, 120
###########################################################################
- variables:
schedule:
- trigger: sunset
offset_minutes: -30
target_brightness: 60
ramp_minutes: 30
- trigger: time
at: "20:00:00"
target_brightness: 50
ramp_minutes: 120
- trigger: time
at: "01:00:00"
target_brightness: 20
ramp_minutes: 120
- trigger: time
at: "06:00:00"
target_brightness: 0
ramp_minutes: 120
- variables:
selected_schedule: >
{% set now_ts = as_timestamp(now()) %}
{% set next_setting_raw = state_attr('sun.sun', 'next_setting') %}
{% if next_setting_raw %}
{% set next_setting = as_datetime(next_setting_raw) | as_local %}
{% if next_setting.date() == now().date() %}
{% set today_sunset = next_setting %}
{% else %}
{% set today_sunset = next_setting - timedelta(days=1) %}
{% endif %}
{% else %}
{% set today_sunset = none %}
{% endif %}
{% set ns = namespace(best_ts=0, best_target=0, best_ramp=0) %}
{% for day_offset in [-1, 0] %}
{% for item in schedule %}
{% if item.trigger == 'sunset' and today_sunset %}
{% set event_dt = today_sunset + timedelta(days=day_offset) + timedelta(minutes=item.offset_minutes | int(0)) %}
{% elif item.trigger == 'time' %}
{% set event_dt = today_at(item.at) + timedelta(days=day_offset) %}
{% else %}
{% set event_dt = none %}
{% endif %}
{% if event_dt %}
{% set valid = namespace(ok=true) %}
{% if item.trigger == 'sunset' %}
{% set idx = loop.index0 %}
{% set nxt = namespace(found=false, dt=none) %}
{% for step in range(1, schedule | length) %}
{% set j = (idx + step) % (schedule | length) %}
{% set next_item = schedule[j] %}
{% if not nxt.found and next_item.trigger == 'time' %}
{% set next_dt = today_at(next_item.at) + timedelta(days=day_offset) %}
{% if next_dt <= event_dt %}
{% set next_dt = next_dt + timedelta(days=1) %}
{% endif %}
{% set nxt.dt = next_dt %}
{% set nxt.found = true %}
{% endif %}
{% endfor %}
{% if nxt.found and event_dt >= nxt.dt %}
{% set valid.ok = false %}
{% endif %}
{% endif %}
{% set event_ts = as_timestamp(event_dt, 0) %}
{% if valid.ok and event_ts <= now_ts and event_ts > ns.best_ts %}
{% set ns.best_ts = event_ts %}
{% set ns.best_target = item.target_brightness | int(0) %}
{% set ns.best_ramp = (item.ramp_minutes | int(0) * 60) %}
{% endif %}
{% endif %}
{% endfor %}
{% endfor %}
{{ ns.best_target }}|{{ ns.best_ramp }}|{{ ns.best_ts | int(0) }}
selected_target: "{{ selected_schedule.split('|')[0] | int(0) }}"
selected_ramp_seconds: "{{ selected_schedule.split('|')[1] | int(0) }}"
selected_event_ts: "{{ selected_schedule.split('|')[2] | int(0) }}"
last_event_ts: "{{ as_timestamp(states('input_datetime.outdoor_lighting_number_16_last_schedule_event'), 0) | int(0) }}"
do_force: "{{ force_reconcile | default(false) | bool }}"
requested_ramp_seconds: "{{ ramp_seconds | default(-1) | int(-1) }}"
ramp_to_use: >
{% if requested_ramp_seconds | int(-1) >= 0 %}
{{ requested_ramp_seconds | int(0) }}
{% elif do_force | bool %}
0
{% else %}
{{ selected_ramp_seconds | int(0) }}
{% endif %}
should_apply: >
{{ do_force | bool or (selected_event_ts | int(0)) != (last_event_ts | int(0)) }}
- if:
- condition: template
value_template: "{{ should_apply | bool }}"
then:
- action: input_datetime.set_datetime
target:
entity_id: input_datetime.outdoor_lighting_number_16_last_schedule_event
data:
datetime: "{{ (selected_event_ts | int(0)) | timestamp_custom('%Y-%m-%d %H:%M:%S', true) }}"
- action: script.outdoor_lighting_ramp_number_16
data:
target_brightness: "{{ selected_target | int(0) }}"
ramp_seconds: "{{ ramp_to_use | int(0) }}"
#############################################################################
# 4) Main Entrance Outside LED Lights
# Brightness: number.front_entranceway_led_light_controls_main_entrance_outside_led_lights_brightness
# Switch: switch.front_entranceway_led_light_controls_main_entrance_outside_led_lights
# Override: binary_sensor.esp_entrancelightswitch_button_2_external_light_controller
#############################################################################
outdoor_lighting_apply_main_entrance_current_target:
alias: Outdoor Lighting - Apply Current Target - Main Entrance Outside LEDs
mode: restart
fields:
force_reconcile:
description: Force immediate reconciliation even if the schedule event has already been applied.
ramp_seconds:
description: Optional ramp seconds override. Use -1 or omit to use the schedule ramp.
sequence:
- condition: state
entity_id: input_boolean.outdoor_lighting_automation_enable
state: "on"
- condition: state
entity_id: input_boolean.outdoor_lighting_main_entrance_automation_enable
state: "on"
- condition: state
entity_id: input_select.outdoor_lighting_main_entrance_override_mode
state: "none"
###########################################################################
# EDIT SCHEDULE HERE
# Format: trigger, offset_minutes/at, target_brightness, ramp_minutes
# Current schedule:
# - sunset -30, 30, 30
# - 20:00, 20, 30
# - 23:00, 0, 30
###########################################################################
- variables:
schedule:
- trigger: sunset
offset_minutes: -30
target_brightness: 30
ramp_minutes: 30
- trigger: time
at: "20:00:00"
target_brightness: 20
ramp_minutes: 30
- trigger: time
at: "23:00:00"
target_brightness: 0
ramp_minutes: 30
- variables:
suppress_active: >
{% set ts = as_timestamp(states('input_datetime.outdoor_lighting_main_entrance_suppress_until'), 0) %}
{{ ts > as_timestamp(now()) }}
selected_schedule: >
{% set now_ts = as_timestamp(now()) %}
{% set next_setting_raw = state_attr('sun.sun', 'next_setting') %}
{% if next_setting_raw %}
{% set next_setting = as_datetime(next_setting_raw) | as_local %}
{% if next_setting.date() == now().date() %}
{% set today_sunset = next_setting %}
{% else %}
{% set today_sunset = next_setting - timedelta(days=1) %}
{% endif %}
{% else %}
{% set today_sunset = none %}
{% endif %}
{% set ns = namespace(best_ts=0, best_target=0, best_ramp=0) %}
{% for day_offset in [-1, 0] %}
{% for item in schedule %}
{% if item.trigger == 'sunset' and today_sunset %}
{% set event_dt = today_sunset + timedelta(days=day_offset) + timedelta(minutes=item.offset_minutes | int(0)) %}
{% elif item.trigger == 'time' %}
{% set event_dt = today_at(item.at) + timedelta(days=day_offset) %}
{% else %}
{% set event_dt = none %}
{% endif %}
{% if event_dt %}
{% set valid = namespace(ok=true) %}
{% if item.trigger == 'sunset' %}
{% set idx = loop.index0 %}
{% set nxt = namespace(found=false, dt=none) %}
{% for step in range(1, schedule | length) %}
{% set j = (idx + step) % (schedule | length) %}
{% set next_item = schedule[j] %}
{% if not nxt.found and next_item.trigger == 'time' %}
{% set next_dt = today_at(next_item.at) + timedelta(days=day_offset) %}
{% if next_dt <= event_dt %}
{% set next_dt = next_dt + timedelta(days=1) %}
{% endif %}
{% set nxt.dt = next_dt %}
{% set nxt.found = true %}
{% endif %}
{% endfor %}
{% if nxt.found and event_dt >= nxt.dt %}
{% set valid.ok = false %}
{% endif %}
{% endif %}
{% set event_ts = as_timestamp(event_dt, 0) %}
{% if valid.ok and event_ts <= now_ts and event_ts > ns.best_ts %}
{% set ns.best_ts = event_ts %}
{% set ns.best_target = item.target_brightness | int(0) %}
{% set ns.best_ramp = (item.ramp_minutes | int(0) * 60) %}
{% endif %}
{% endif %}
{% endfor %}
{% endfor %}
{{ ns.best_target }}|{{ ns.best_ramp }}|{{ ns.best_ts | int(0) }}
selected_target: >
{% if suppress_active | bool %}
0
{% else %}
{{ selected_schedule.split('|')[0] | int(0) }}
{% endif %}
selected_ramp_seconds: "{{ selected_schedule.split('|')[1] | int(0) }}"
selected_event_ts: "{{ selected_schedule.split('|')[2] | int(0) }}"
last_event_ts: "{{ as_timestamp(states('input_datetime.outdoor_lighting_main_entrance_last_schedule_event'), 0) | int(0) }}"
do_force: "{{ force_reconcile | default(false) | bool }}"
requested_ramp_seconds: "{{ ramp_seconds | default(-1) | int(-1) }}"
ramp_to_use: >
{% if requested_ramp_seconds | int(-1) >= 0 %}
{{ requested_ramp_seconds | int(0) }}
{% elif do_force | bool %}
0
{% else %}
{{ selected_ramp_seconds | int(0) }}
{% endif %}
ramp_script_active: "{{ is_state('script.outdoor_lighting_ramp_main_entrance', 'on') }}"
should_apply: >
{% if suppress_active | bool and ramp_script_active | bool %}
false
{% else %}
{{ do_force | bool or suppress_active | bool or (selected_event_ts | int(0)) != (last_event_ts | int(0)) }}
{% endif %}
- if:
- condition: template
value_template: "{{ should_apply | bool }}"
then:
- if:
- condition: template
value_template: "{{ not (suppress_active | bool) }}"
then:
- action: input_datetime.set_datetime
target:
entity_id: input_datetime.outdoor_lighting_main_entrance_last_schedule_event
data:
datetime: "{{ (selected_event_ts | int(0)) | timestamp_custom('%Y-%m-%d %H:%M:%S', true) }}"
- action: script.outdoor_lighting_ramp_main_entrance
data:
target_brightness: "{{ selected_target | int(0) }}"
ramp_seconds: "{{ ramp_to_use | int(0) }}"
#############################################################################
# Reconcile all
#############################################################################
outdoor_lighting_reconcile_all:
alias: Outdoor Lighting - Reconcile All Outdoor Lights
mode: restart
fields:
force_reconcile:
description: Force dimmable lights to reconcile immediately.
sequence:
- action: script.outdoor_lighting_reconcile_deck_coloured
- action: script.outdoor_lighting_reconcile_patio
- action: script.outdoor_lighting_apply_number_16_current_target
data:
force_reconcile: "{{ force_reconcile | default(false) | bool }}"
ramp_seconds: >
{% if force_reconcile | default(false) | bool %}
0
{% else %}
-1
{% endif %}
- action: script.outdoor_lighting_apply_main_entrance_current_target
data:
force_reconcile: "{{ force_reconcile | default(false) | bool }}"
ramp_seconds: >
{% if force_reconcile | default(false) | bool %}
0
{% else %}
-1
{% endif %}
automation:
#############################################################################
# Startup, scheduler, and enable switch cleanup
#############################################################################
- id: outdoor_lighting_reconcile_on_start
alias: Outdoor Lighting - Reconcile On Home Assistant Start
mode: single
trigger:
- platform: homeassistant
event: start
action:
- delay: "00:00:30"
- action: input_select.select_option
target:
entity_id: input_select.outdoor_lighting_main_entrance_override_mode
data:
option: none
- action: timer.cancel
target:
entity_id:
- timer.outdoor_lighting_deck_manual_on
- timer.outdoor_lighting_patio_manual_on
- timer.outdoor_lighting_main_entrance_override
- action: script.outdoor_lighting_reconcile_all
data:
force_reconcile: true
- id: outdoor_lighting_scheduler_tick
alias: Outdoor Lighting - Scheduler Tick
mode: single
trigger:
- platform: time_pattern
minutes: "/5"
condition:
- condition: state
entity_id: input_boolean.outdoor_lighting_automation_enable
state: "on"
action:
- action: script.outdoor_lighting_reconcile_all
data:
force_reconcile: false
- id: outdoor_lighting_reconcile_when_enable_changes
alias: Outdoor Lighting - Reconcile When Automation Enable Changes
mode: queued
trigger:
- platform: state
entity_id:
- input_boolean.outdoor_lighting_automation_enable
- input_boolean.outdoor_lighting_deck_coloured_automation_enable
- input_boolean.outdoor_lighting_patio_automation_enable
- input_boolean.outdoor_lighting_number_16_automation_enable
- input_boolean.outdoor_lighting_main_entrance_automation_enable
to: "on"
action:
- action: script.outdoor_lighting_reconcile_all
data:
force_reconcile: true
- id: outdoor_lighting_cleanup_when_master_disabled
alias: Outdoor Lighting - Cleanup When Master Automation Disabled
mode: single
trigger:
- platform: state
entity_id: input_boolean.outdoor_lighting_automation_enable
to: "off"
action:
- action: script.turn_off
target:
entity_id:
- script.outdoor_lighting_ramp_number_16
- script.outdoor_lighting_ramp_main_entrance
- script.outdoor_lighting_apply_number_16_current_target
- script.outdoor_lighting_apply_main_entrance_current_target
- action: timer.cancel
target:
entity_id:
- timer.outdoor_lighting_deck_manual_on
- timer.outdoor_lighting_patio_manual_on
- timer.outdoor_lighting_main_entrance_override
- action: input_select.select_option
target:
entity_id: input_select.outdoor_lighting_main_entrance_override_mode
data:
option: none
#############################################################################
# Switched light override buttons
#############################################################################
- id: outdoor_lighting_deck_coloured_override_button
alias: Outdoor Lighting - Deck Coloured Lights - Override Button
mode: restart
trigger:
- platform: state
entity_id: binary_sensor.esp_downstloungeoutside_button_1_outside_lights
to: "on"
condition:
- condition: state
entity_id: input_boolean.outdoor_lighting_automation_enable
state: "on"
- condition: state
entity_id: input_boolean.outdoor_lighting_deck_coloured_automation_enable
state: "on"
action:
- variables:
was_on_at_press: "{{ is_state('switch.esp_downstloungeoutside_relay_1_outside_lights', 'on') }}"
- delay: "00:00:01"
- choose:
- conditions:
- condition: template
value_template: "{{ was_on_at_press | bool }}"
sequence:
- action: input_datetime.set_datetime
target:
entity_id: input_datetime.outdoor_lighting_deck_suppress_until
data:
datetime: >-
{%- set next_setting_raw = state_attr('sun.sun', 'next_setting') -%}
{%- set next_setting = as_datetime(next_setting_raw) | as_local -%}
{%- if next_setting.date() == now().date() -%}
{%- set today_sunset = next_setting -%}
{%- else -%}
{%- set today_sunset = next_setting - timedelta(days=1) -%}
{%- endif -%}
{{ (today_sunset + timedelta(days=1, minutes=60)).strftime('%Y-%m-%d %H:%M:%S') }}
- action: timer.cancel
target:
entity_id: timer.outdoor_lighting_deck_manual_on
- action: switch.turn_off
target:
entity_id: switch.esp_downstloungeoutside_relay_1_outside_lights
default:
- action: input_datetime.set_datetime
target:
entity_id: input_datetime.outdoor_lighting_deck_suppress_until
data:
datetime: "1970-01-01 00:00:00"
- action: input_datetime.set_datetime
target:
entity_id: input_datetime.outdoor_lighting_deck_manual_on_started
data:
datetime: "{{ now().strftime('%Y-%m-%d %H:%M:%S') }}"
- action: switch.turn_on
target:
entity_id: switch.esp_downstloungeoutside_relay_1_outside_lights
- action: timer.start
target:
entity_id: timer.outdoor_lighting_deck_manual_on
data:
duration: "01:00:00"
- id: outdoor_lighting_patio_override_button
alias: Outdoor Lighting - Patio Lights - Override Button
mode: restart
trigger:
- platform: state
entity_id: binary_sensor.esp_garageentrylights_button_2_patio_lights
to: "on"
condition:
- condition: state
entity_id: input_boolean.outdoor_lighting_automation_enable
state: "on"
- condition: state
entity_id: input_boolean.outdoor_lighting_patio_automation_enable
state: "on"
action:
- variables:
was_on_at_press: "{{ is_state('switch.esp_garageentrylights_relay_2_patio_lights', 'on') }}"
- delay: "00:00:01"
- choose:
- conditions:
- condition: template
value_template: "{{ was_on_at_press | bool }}"
sequence:
- action: input_datetime.set_datetime
target:
entity_id: input_datetime.outdoor_lighting_patio_suppress_until
data:
datetime: >-
{%- set next_setting_raw = state_attr('sun.sun', 'next_setting') -%}
{%- set next_setting = as_datetime(next_setting_raw) | as_local -%}
{%- if next_setting.date() == now().date() -%}
{%- set today_sunset = next_setting -%}
{%- else -%}
{%- set today_sunset = next_setting - timedelta(days=1) -%}
{%- endif -%}
{{ (today_sunset + timedelta(days=1, minutes=-30)).strftime('%Y-%m-%d %H:%M:%S') }}
- action: timer.cancel
target:
entity_id: timer.outdoor_lighting_patio_manual_on
- action: switch.turn_off
target:
entity_id: switch.esp_garageentrylights_relay_2_patio_lights
default:
- action: input_datetime.set_datetime
target:
entity_id: input_datetime.outdoor_lighting_patio_suppress_until
data:
datetime: "1970-01-01 00:00:00"
- action: input_datetime.set_datetime
target:
entity_id: input_datetime.outdoor_lighting_patio_manual_on_started
data:
datetime: "{{ now().strftime('%Y-%m-%d %H:%M:%S') }}"
- action: switch.turn_on
target:
entity_id: switch.esp_garageentrylights_relay_2_patio_lights
- action: timer.start
target:
entity_id: timer.outdoor_lighting_patio_manual_on
data:
duration: "01:00:00"
- id: outdoor_lighting_deck_coloured_manual_on_timer_finished
alias: Outdoor Lighting - Deck Coloured Lights - Manual On Timer Finished
mode: single
trigger:
- platform: event
event_type: timer.finished
event_data:
entity_id: timer.outdoor_lighting_deck_manual_on
action:
- action: input_datetime.set_datetime
target:
entity_id: input_datetime.outdoor_lighting_deck_manual_on_started
data:
datetime: "1970-01-01 00:00:00"
- action: script.outdoor_lighting_reconcile_deck_coloured
- id: outdoor_lighting_patio_manual_on_timer_finished
alias: Outdoor Lighting - Patio Lights - Manual On Timer Finished
mode: single
trigger:
- platform: event
event_type: timer.finished
event_data:
entity_id: timer.outdoor_lighting_patio_manual_on
action:
- action: input_datetime.set_datetime
target:
entity_id: input_datetime.outdoor_lighting_patio_manual_on_started
data:
datetime: "1970-01-01 00:00:00"
- action: script.outdoor_lighting_reconcile_patio
#############################################################################
# Main entrance dimmable override button
#############################################################################
- id: outdoor_lighting_main_entrance_override_button
alias: Outdoor Lighting - Main Entrance Outside LEDs - Override Button
mode: restart
trigger:
- platform: state
entity_id: binary_sensor.esp_entrancelightswitch_button_2_external_light_controller
to: "on"
condition:
- condition: state
entity_id: input_boolean.outdoor_lighting_automation_enable
state: "on"
- condition: state
entity_id: input_boolean.outdoor_lighting_main_entrance_automation_enable
state: "on"
action:
- variables:
override_ramp_seconds: 60
was_on_at_press: "{{ is_state('switch.front_entranceway_led_light_controls_main_entrance_outside_led_lights', 'on') }}"
- delay: "00:00:01"
- variables:
last_press_age_seconds: >
{% set last_ts = as_timestamp(states('input_datetime.outdoor_lighting_main_entrance_last_override_press'), 0) %}
{% if last_ts <= 0 %}
999999
{% else %}
{{ (as_timestamp(now()) - last_ts) | int(999999) }}
{% endif %}
- choose:
- conditions:
- condition: template
value_template: >
{{ not is_state('input_select.outdoor_lighting_main_entrance_override_mode', 'none')
and (last_press_age_seconds | int(999999)) <= 60 }}
sequence:
- action: timer.cancel
target:
entity_id: timer.outdoor_lighting_main_entrance_override
- action: input_select.select_option
target:
entity_id: input_select.outdoor_lighting_main_entrance_override_mode
data:
option: none
- action: input_datetime.set_datetime
target:
entity_id: input_datetime.outdoor_lighting_main_entrance_suppress_until
data:
datetime: >-
{%- set next_setting_raw = state_attr('sun.sun', 'next_setting') -%}
{%- set next_setting = as_datetime(next_setting_raw) | as_local -%}
{%- if next_setting.date() == now().date() -%}
{%- set today_sunset = next_setting -%}
{%- else -%}
{%- set today_sunset = next_setting - timedelta(days=1) -%}
{%- endif -%}
{%- set tomorrow_sun_on = today_sunset + timedelta(days=1, minutes=-30) -%}
{%- set tomorrow_2000 = today_at('20:00:00') + timedelta(days=1) -%}
{%- if tomorrow_sun_on < tomorrow_2000 -%}
{{ tomorrow_sun_on.strftime('%Y-%m-%d %H:%M:%S') }}
{%- else -%}
{{ tomorrow_2000.strftime('%Y-%m-%d %H:%M:%S') }}
{%- endif -%}
- action: script.outdoor_lighting_ramp_main_entrance
data:
target_brightness: 0
ramp_seconds: "{{ override_ramp_seconds }}"
- conditions:
- condition: template
value_template: >
{{ not is_state('input_select.outdoor_lighting_main_entrance_override_mode', 'none')
and (last_press_age_seconds | int(999999)) > 60
and (last_press_age_seconds | int(999999)) <= 3600 }}
sequence:
- action: timer.cancel
target:
entity_id: timer.outdoor_lighting_main_entrance_override
- action: input_select.select_option
target:
entity_id: input_select.outdoor_lighting_main_entrance_override_mode
data:
option: none
- action: script.outdoor_lighting_apply_main_entrance_current_target
data:
force_reconcile: true
ramp_seconds: "{{ override_ramp_seconds }}"
default:
- action: input_datetime.set_datetime
target:
entity_id: input_datetime.outdoor_lighting_main_entrance_last_override_press
data:
datetime: "{{ now().strftime('%Y-%m-%d %H:%M:%S') }}"
- choose:
- conditions:
- condition: template
value_template: "{{ was_on_at_press | bool }}"
sequence:
- action: input_select.select_option
target:
entity_id: input_select.outdoor_lighting_main_entrance_override_mode
data:
option: return_to_schedule
default:
- action: input_select.select_option
target:
entity_id: input_select.outdoor_lighting_main_entrance_override_mode
data:
option: turn_off
- action: script.outdoor_lighting_ramp_main_entrance
data:
target_brightness: 100
ramp_seconds: "{{ override_ramp_seconds }}"
- action: timer.start
target:
entity_id: timer.outdoor_lighting_main_entrance_override
data:
duration: "01:00:00"
- id: outdoor_lighting_main_entrance_override_timer_finished
alias: Outdoor Lighting - Main Entrance Outside LEDs - Override Timer Finished
mode: single
trigger:
- platform: event
event_type: timer.finished
event_data:
entity_id: timer.outdoor_lighting_main_entrance_override
condition:
- condition: state
entity_id: input_boolean.outdoor_lighting_automation_enable
state: "on"
- condition: state
entity_id: input_boolean.outdoor_lighting_main_entrance_automation_enable
state: "on"
action:
- variables:
override_ramp_seconds: 60
mode_at_finish: "{{ states('input_select.outdoor_lighting_main_entrance_override_mode') }}"
- action: input_select.select_option
target:
entity_id: input_select.outdoor_lighting_main_entrance_override_mode
data:
option: none
- choose:
- conditions:
- condition: template
value_template: "{{ mode_at_finish == 'return_to_schedule' }}"
sequence:
- action: script.outdoor_lighting_apply_main_entrance_current_target
data:
force_reconcile: true
ramp_seconds: "{{ override_ramp_seconds }}"
- conditions:
- condition: template
value_template: "{{ mode_at_finish == 'turn_off' }}"
sequence:
- action: script.outdoor_lighting_ramp_main_entrance
data:
target_brightness: 0
ramp_seconds: "{{ override_ramp_seconds }}"