###############################################################################
# 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 }}"
