#:########################################################################################:#
# TITLE: FRONT ENTRANCEWAY LED LIGHT CONTROLS
# zorruno.com layout v1.1 2026
#:########################################################################################:#
# REPO:
# https://home.fox.co.nz/gitea/zorruno/zorruno-homeassistant/src/branch/master/esphome/esp-entranceoutsideledlights.yaml
#:########################################################################################:#
# VERSIONS:
# V1.2 2026-03-10 Added per-channel maximum ON timers with auto-dim-to-off, restart-on-ON, and moved defaults into substitutions
# V1.1 2026-03-10 Updated yaml to zorruno layout V1.1
# V1.0 2026-02-23 Initial version (converted from Tasmota)
#:########################################################################################:#
# HARDWARE:
# - H801 (ESP8266EX) 5-channel PWM MOSFET LED controller
#:########################################################################################:#
# OPERATION NOTES:
# - H801 PWM outputs (typical / ESPHome Devices):
#     PWM1 / R   -> GPIO15
#     PWM2 / G   -> GPIO13
#     PWM3 / B   -> GPIO12
#     PWM4 / W1  -> GPIO14
#     PWM5 / W2  -> GPIO4
#   Some templates swap G/B labeling (GPIO12 vs GPIO13). If outputs appear swapped,
#   just swap out2_pwm_pin and out3_pwm_pin below.
#
# - We expose helper entities per channel:
#     * On/Off switch (ramps using per-channel ramp time)
#     * Brightness slider 0-100% normalized to the per-channel hard limit
#     * Ramp time (seconds) for a full 0 -> hard limit transition
#     * Hard limit (percent) absolute cap for that channel
#     * Maximum ON time (minutes) after which the channel dims to OFF
#
# - A fresh ON command restarts the countdown timer for that channel.
# - Setting channel brightness above 0 also restarts that channel timer.
#
# - Underlying PWM light entities are INTERNAL so HA cannot bypass the hard limit.
#:########################################################################################:#
# MQTT COMMANDS:
# - None (no device-specific MQTT commands in this YAML; common_mqtt is included for standardisation)
#:########################################################################################:#
# OFFLINE NOTES:
# a) Home Assistant OFFLINE, but Network and MQTT ONLINE
#    - Local PWM control continues via HA entities if HA is offline (entities unavailable until HA returns)
#    - Device continues running with last commanded PWM states
#    - Any already-running max ON timer continues locally on the device
# b) MQTT OFFLINE (but WiFi/Network and HA API ONLINE)
#    - No impact unless you rely on MQTT for diagnostics; local control via HA/API continues
#    - Any already-running max ON timer continues locally on the device
# c) Entire WiFi/Network OFFLINE
#    - No HA API and no MQTT connectivity
#    - Outputs remain in their last driven state (until power loss/reboot)
#    - Any already-running max ON timer continues locally on the device
#    - Accurate time is NOT needed (SNTP not required for operation)
#:########################################################################################:#


#:########################################################################################:#
# SUBSTITUTIONS: Specific device variable substitutions
# If NOT using a secrets file, just replace these with the passwords etc (in quotes)
#:########################################################################################:#
substitutions:
  # Device Naming
  device_name: "esp-entranceoutsideledlights"
  friendly_name: "Front Entranceway LED Light controls"
  description_comment: "H801 ESP8266 5-channel PWM LED controller (Entranceway) (Layout V1.1)"
  device_area: "Entranceway" # Allows ESP device to be automatically linked to an 'Area' in Home Assistant.

  # Project Naming
  project_name: "H801.5CH PWM LED Controller"
  project_version: "v1.2"

  # Passwords & Secrets
  api_key: !secret esp-api_key
  ota_pass: !secret esp-ota_pass
  static_ip_address: !secret esp-entranceoutsideledlights_ip # unfortunately you can't use substitutions inside secrets names

  # If we are changing IP addresses, you must update the current IP address here, otherwise it remains
  # Don't forget to switch it back when changed.
  current_ip_address: ${static_ip_address}

  # Device Settings
  log_level: "INFO" # NONE, ERROR, WARN, INFO, DEBUG, VERBOSE, VERY_VERBOSE
  update_interval: "60s"

  # OUTPUT NAMES
  out1_name: "Main Entrance Outside LED Lights"
  out2_name: "Number 16 LED Light"
  out3_name: "Spare 3"
  out4_name: "Doorbell Illumination"
  out5_name: "Spare 5"

  # OUTPUT DEFAULT SETTINGS
  out1_ramp_default_s: "2.0"
  out2_ramp_default_s: "2.0"
  out3_ramp_default_s: "2.0"
  out4_ramp_default_s: "2.0"
  out5_ramp_default_s: "2.0"

  out1_max_limit_default_pct: "100"
  out2_max_limit_default_pct: "100"
  out3_max_limit_default_pct: "100"
  out4_max_limit_default_pct: "100"
  out5_max_limit_default_pct: "100"

  out1_max_on_time_default_mins: "240"
  out2_max_on_time_default_mins: "240"
  out3_max_on_time_default_mins: "10"
  out4_max_on_time_default_mins: "4"
  out5_max_on_time_default_mins: "10"

  # H801 PWM PINS (easy swap here if needed)
  out1_pwm_pin: "GPIO15"
  out2_pwm_pin: "GPIO13"
  out3_pwm_pin: "GPIO12"
  out4_pwm_pin: "GPIO14"
  out5_pwm_pin: "GPIO4"
  pwm_frequency: "1000 Hz"


#:########################################################################################:#
# PACKAGES: Included Common Packages
# https://esphome.io/components/packages.html
#:########################################################################################:#
packages:
  #### WIFI, Network (Static/DHCP/IPV6 etc), Fallback AP, Safemode ####
  common_wifi: !include
    file: common/network_common.yaml
    vars:
      local_device_name: "${device_name}"
      local_static_ip_address: "${static_ip_address}"
      local_ota_pass: "${ota_pass}"
      local_current_ip_address: "${current_ip_address}"

  #### HOME ASSISTANT API (choose encryption or no encryption options) ####
  common_api: !include
    file: common/api_common.yaml
    #file: common/api_common_noencryption.yaml
    vars:
      local_api_key: "${api_key}"

  #### MQTT ####
  common_mqtt: !include
    file: common/mqtt_common.yaml
    vars:
      local_device_name: "${device_name}"

  #### SNTP (Only use if you want/need accurate timeclocks) ####
  common_sntp: !include common/sntp_common.yaml

  #### DIAGNOSTICS Sensors ####
  diag_basic: !include common/include_basic_diag_sensors.yaml
  diag_more: !include common/include_more_diag_sensors.yaml
  diag_debug: !include common/include_debug_diag_sensors.yaml
  #diag_resetcount: !include common/include_resetcount_diag_sensors.yaml


#:########################################################################################:#
# ESPHOME:
# https://esphome.io/components/esphome.html
#:########################################################################################:#
esphome:
  name: "${device_name}"
  friendly_name: "${friendly_name}"
  comment: "${description_comment}" # appears on the esphome page in HA
  min_version: 2025.11.0
  area: "${device_area}"
  project:
    name: "${project_name}"
    version: "${project_version}"


#:########################################################################################:#
# ESP PLATFORM AND FRAMEWORK:
# https://esphome.io/components/esp8266.html
#:########################################################################################:#
esp8266:
  board: esp01_1m


#:########################################################################################:#
# LOGGING:
# https://esphome.io/components/logger.html
#:########################################################################################:#
logger:
  level: "${log_level}"
  baud_rate: 0 # set to 0 for no logging via UART


#:########################################################################################:#
# OUTPUTS: H801 PWM MOSFET CHANNELS
# https://esphome.io/components/output/esp8266_pwm.html
#:########################################################################################:#
output:
  - platform: esp8266_pwm
    id: out1_pwm
    pin: ${out1_pwm_pin}
    frequency: ${pwm_frequency}

  - platform: esp8266_pwm
    id: out2_pwm
    pin: ${out2_pwm_pin}
    frequency: ${pwm_frequency}

  - platform: esp8266_pwm
    id: out3_pwm
    pin: ${out3_pwm_pin}
    frequency: ${pwm_frequency}

  - platform: esp8266_pwm
    id: out4_pwm
    pin: ${out4_pwm_pin}
    frequency: ${pwm_frequency}

  - platform: esp8266_pwm
    id: out5_pwm
    pin: ${out5_pwm_pin}
    frequency: ${pwm_frequency}


#:########################################################################################:#
# LIGHTS: Internal raw PWM lights (not exposed to HA)
# https://esphome.io/components/light/monochromatic.html
#:########################################################################################:#
light:
  - platform: monochromatic
    id: out1_light_raw
    name: "${out1_name} (RAW)"
    output: out1_pwm
    internal: true
    restore_mode: RESTORE_DEFAULT_OFF

  - platform: monochromatic
    id: out2_light_raw
    name: "${out2_name} (RAW)"
    output: out2_pwm
    internal: true
    restore_mode: RESTORE_DEFAULT_OFF

  - platform: monochromatic
    id: out3_light_raw
    name: "${out3_name} (RAW)"
    output: out3_pwm
    internal: true
    restore_mode: RESTORE_DEFAULT_OFF

  - platform: monochromatic
    id: out4_light_raw
    name: "${out4_name} (RAW)"
    output: out4_pwm
    internal: true
    restore_mode: RESTORE_DEFAULT_OFF

  - platform: monochromatic
    id: out5_light_raw
    name: "${out5_name} (RAW)"
    output: out5_pwm
    internal: true
    restore_mode: RESTORE_DEFAULT_OFF


#:########################################################################################:#
# GLOBALS: Store "last non-zero" brightness per channel
# https://esphome.io/components/globals.html
#:########################################################################################:#
globals:
  - id: out1_last_nonzero_pct
    type: float
    restore_value: true
    initial_value: "100.0"

  - id: out2_last_nonzero_pct
    type: float
    restore_value: true
    initial_value: "100.0"

  - id: out3_last_nonzero_pct
    type: float
    restore_value: true
    initial_value: "100.0"

  - id: out4_last_nonzero_pct
    type: float
    restore_value: true
    initial_value: "100.0"

  - id: out5_last_nonzero_pct
    type: float
    restore_value: true
    initial_value: "100.0"


#:########################################################################################:#
# SCRIPTS: Apply normalized brightness + proportional ramping + per-channel auto-off timers
# https://esphome.io/components/script.html
#:########################################################################################:#
script:
  #############################################
  # OUT1
  #############################################
  - id: out1_apply_target
    mode: restart
    then:
      - lambda: |-
          const float slider_pct = id(out1_brightness_pct).state;
          const float limit_pct  = id(out1_max_limit_pct).state;
          const float ramp_s     = id(out1_ramp_s).state;

          float limit_frac = limit_pct / 100.0f;
          if (limit_frac < 0.0f) limit_frac = 0.0f;
          if (limit_frac > 1.0f) limit_frac = 1.0f;

          float slider_frac = slider_pct / 100.0f;
          if (slider_frac < 0.0f) slider_frac = 0.0f;
          if (slider_frac > 1.0f) slider_frac = 1.0f;

          float target = slider_frac * limit_frac;

          float current = 0.0f;
          if (id(out1_light_raw).current_values.is_on()) {
            current = id(out1_light_raw).current_values.get_brightness();
          }

          float delta = target - current;
          if (delta < 0.0f) delta = -delta;

          float full_scale = limit_frac;
          if (current > full_scale) full_scale = current;
          if (full_scale < 0.0001f) full_scale = 0.0001f;

          uint32_t trans_ms = (uint32_t) (ramp_s * 1000.0f * (delta / full_scale));
          if (trans_ms > 600000) trans_ms = 600000;

          if (target <= 0.0001f) {
            auto call = id(out1_light_raw).turn_off();
            call.set_transition_length(trans_ms);
            call.perform();
          } else {
            auto call = id(out1_light_raw).turn_on();
            call.set_brightness(target);
            call.set_transition_length(trans_ms);
            call.perform();
          }

  - id: out1_turn_off
    mode: restart
    then:
      - lambda: |-
          const float limit_pct = id(out1_max_limit_pct).state;
          const float ramp_s    = id(out1_ramp_s).state;

          float limit_frac = limit_pct / 100.0f;
          if (limit_frac < 0.0f) limit_frac = 0.0f;
          if (limit_frac > 1.0f) limit_frac = 1.0f;

          float current = 0.0f;
          if (id(out1_light_raw).current_values.is_on()) {
            current = id(out1_light_raw).current_values.get_brightness();
          }

          float full_scale = limit_frac;
          if (current > full_scale) full_scale = current;
          if (full_scale < 0.0001f) full_scale = 0.0001f;

          uint32_t trans_ms = (uint32_t) (ramp_s * 1000.0f * (current / full_scale));
          if (trans_ms > 600000) trans_ms = 600000;

          auto call = id(out1_light_raw).turn_off();
          call.set_transition_length(trans_ms);
          call.perform();

  - id: out1_auto_off_timer
    mode: restart
    parameters:
      delay_ms: int
    then:
      - delay: !lambda "return delay_ms;"
      - script.execute: out1_turn_off

  #############################################
  # OUT2
  #############################################
  - id: out2_apply_target
    mode: restart
    then:
      - lambda: |-
          const float slider_pct = id(out2_brightness_pct).state;
          const float limit_pct  = id(out2_max_limit_pct).state;
          const float ramp_s     = id(out2_ramp_s).state;

          float limit_frac = limit_pct / 100.0f;
          if (limit_frac < 0.0f) limit_frac = 0.0f;
          if (limit_frac > 1.0f) limit_frac = 1.0f;

          float slider_frac = slider_pct / 100.0f;
          if (slider_frac < 0.0f) slider_frac = 0.0f;
          if (slider_frac > 1.0f) slider_frac = 1.0f;

          float target = slider_frac * limit_frac;

          float current = 0.0f;
          if (id(out2_light_raw).current_values.is_on()) {
            current = id(out2_light_raw).current_values.get_brightness();
          }

          float delta = target - current;
          if (delta < 0.0f) delta = -delta;

          float full_scale = limit_frac;
          if (current > full_scale) full_scale = current;
          if (full_scale < 0.0001f) full_scale = 0.0001f;

          uint32_t trans_ms = (uint32_t) (ramp_s * 1000.0f * (delta / full_scale));
          if (trans_ms > 600000) trans_ms = 600000;

          if (target <= 0.0001f) {
            auto call = id(out2_light_raw).turn_off();
            call.set_transition_length(trans_ms);
            call.perform();
          } else {
            auto call = id(out2_light_raw).turn_on();
            call.set_brightness(target);
            call.set_transition_length(trans_ms);
            call.perform();
          }

  - id: out2_turn_off
    mode: restart
    then:
      - lambda: |-
          const float limit_pct = id(out2_max_limit_pct).state;
          const float ramp_s    = id(out2_ramp_s).state;

          float limit_frac = limit_pct / 100.0f;
          if (limit_frac < 0.0f) limit_frac = 0.0f;
          if (limit_frac > 1.0f) limit_frac = 1.0f;

          float current = 0.0f;
          if (id(out2_light_raw).current_values.is_on()) {
            current = id(out2_light_raw).current_values.get_brightness();
          }

          float full_scale = limit_frac;
          if (current > full_scale) full_scale = current;
          if (full_scale < 0.0001f) full_scale = 0.0001f;

          uint32_t trans_ms = (uint32_t) (ramp_s * 1000.0f * (current / full_scale));
          if (trans_ms > 600000) trans_ms = 600000;

          auto call = id(out2_light_raw).turn_off();
          call.set_transition_length(trans_ms);
          call.perform();

  - id: out2_auto_off_timer
    mode: restart
    parameters:
      delay_ms: int
    then:
      - delay: !lambda "return delay_ms;"
      - script.execute: out2_turn_off

  #############################################
  # OUT3
  #############################################
  - id: out3_apply_target
    mode: restart
    then:
      - lambda: |-
          const float slider_pct = id(out3_brightness_pct).state;
          const float limit_pct  = id(out3_max_limit_pct).state;
          const float ramp_s     = id(out3_ramp_s).state;

          float limit_frac = limit_pct / 100.0f;
          if (limit_frac < 0.0f) limit_frac = 0.0f;
          if (limit_frac > 1.0f) limit_frac = 1.0f;

          float slider_frac = slider_pct / 100.0f;
          if (slider_frac < 0.0f) slider_frac = 0.0f;
          if (slider_frac > 1.0f) slider_frac = 1.0f;

          float target = slider_frac * limit_frac;

          float current = 0.0f;
          if (id(out3_light_raw).current_values.is_on()) {
            current = id(out3_light_raw).current_values.get_brightness();
          }

          float delta = target - current;
          if (delta < 0.0f) delta = -delta;

          float full_scale = limit_frac;
          if (current > full_scale) full_scale = current;
          if (full_scale < 0.0001f) full_scale = 0.0001f;

          uint32_t trans_ms = (uint32_t) (ramp_s * 1000.0f * (delta / full_scale));
          if (trans_ms > 600000) trans_ms = 600000;

          if (target <= 0.0001f) {
            auto call = id(out3_light_raw).turn_off();
            call.set_transition_length(trans_ms);
            call.perform();
          } else {
            auto call = id(out3_light_raw).turn_on();
            call.set_brightness(target);
            call.set_transition_length(trans_ms);
            call.perform();
          }

  - id: out3_turn_off
    mode: restart
    then:
      - lambda: |-
          const float limit_pct = id(out3_max_limit_pct).state;
          const float ramp_s    = id(out3_ramp_s).state;

          float limit_frac = limit_pct / 100.0f;
          if (limit_frac < 0.0f) limit_frac = 0.0f;
          if (limit_frac > 1.0f) limit_frac = 1.0f;

          float current = 0.0f;
          if (id(out3_light_raw).current_values.is_on()) {
            current = id(out3_light_raw).current_values.get_brightness();
          }

          float full_scale = limit_frac;
          if (current > full_scale) full_scale = current;
          if (full_scale < 0.0001f) full_scale = 0.0001f;

          uint32_t trans_ms = (uint32_t) (ramp_s * 1000.0f * (current / full_scale));
          if (trans_ms > 600000) trans_ms = 600000;

          auto call = id(out3_light_raw).turn_off();
          call.set_transition_length(trans_ms);
          call.perform();

  - id: out3_auto_off_timer
    mode: restart
    parameters:
      delay_ms: int
    then:
      - delay: !lambda "return delay_ms;"
      - script.execute: out3_turn_off

  #############################################
  # OUT4
  #############################################
  - id: out4_apply_target
    mode: restart
    then:
      - lambda: |-
          const float slider_pct = id(out4_brightness_pct).state;
          const float limit_pct  = id(out4_max_limit_pct).state;
          const float ramp_s     = id(out4_ramp_s).state;

          float limit_frac = limit_pct / 100.0f;
          if (limit_frac < 0.0f) limit_frac = 0.0f;
          if (limit_frac > 1.0f) limit_frac = 1.0f;

          float slider_frac = slider_pct / 100.0f;
          if (slider_frac < 0.0f) slider_frac = 0.0f;
          if (slider_frac > 1.0f) slider_frac = 1.0f;

          float target = slider_frac * limit_frac;

          float current = 0.0f;
          if (id(out4_light_raw).current_values.is_on()) {
            current = id(out4_light_raw).current_values.get_brightness();
          }

          float delta = target - current;
          if (delta < 0.0f) delta = -delta;

          float full_scale = limit_frac;
          if (current > full_scale) full_scale = current;
          if (full_scale < 0.0001f) full_scale = 0.0001f;

          uint32_t trans_ms = (uint32_t) (ramp_s * 1000.0f * (delta / full_scale));
          if (trans_ms > 600000) trans_ms = 600000;

          if (target <= 0.0001f) {
            auto call = id(out4_light_raw).turn_off();
            call.set_transition_length(trans_ms);
            call.perform();
          } else {
            auto call = id(out4_light_raw).turn_on();
            call.set_brightness(target);
            call.set_transition_length(trans_ms);
            call.perform();
          }

  - id: out4_turn_off
    mode: restart
    then:
      - lambda: |-
          const float limit_pct = id(out4_max_limit_pct).state;
          const float ramp_s    = id(out4_ramp_s).state;

          float limit_frac = limit_pct / 100.0f;
          if (limit_frac < 0.0f) limit_frac = 0.0f;
          if (limit_frac > 1.0f) limit_frac = 1.0f;

          float current = 0.0f;
          if (id(out4_light_raw).current_values.is_on()) {
            current = id(out4_light_raw).current_values.get_brightness();
          }

          float full_scale = limit_frac;
          if (current > full_scale) full_scale = current;
          if (full_scale < 0.0001f) full_scale = 0.0001f;

          uint32_t trans_ms = (uint32_t) (ramp_s * 1000.0f * (current / full_scale));
          if (trans_ms > 600000) trans_ms = 600000;

          auto call = id(out4_light_raw).turn_off();
          call.set_transition_length(trans_ms);
          call.perform();

  - id: out4_auto_off_timer
    mode: restart
    parameters:
      delay_ms: int
    then:
      - delay: !lambda "return delay_ms;"
      - script.execute: out4_turn_off

  #############################################
  # OUT5
  #############################################
  - id: out5_apply_target
    mode: restart
    then:
      - lambda: |-
          const float slider_pct = id(out5_brightness_pct).state;
          const float limit_pct  = id(out5_max_limit_pct).state;
          const float ramp_s     = id(out5_ramp_s).state;

          float limit_frac = limit_pct / 100.0f;
          if (limit_frac < 0.0f) limit_frac = 0.0f;
          if (limit_frac > 1.0f) limit_frac = 1.0f;

          float slider_frac = slider_pct / 100.0f;
          if (slider_frac < 0.0f) slider_frac = 0.0f;
          if (slider_frac > 1.0f) slider_frac = 1.0f;

          float target = slider_frac * limit_frac;

          float current = 0.0f;
          if (id(out5_light_raw).current_values.is_on()) {
            current = id(out5_light_raw).current_values.get_brightness();
          }

          float delta = target - current;
          if (delta < 0.0f) delta = -delta;

          float full_scale = limit_frac;
          if (current > full_scale) full_scale = current;
          if (full_scale < 0.0001f) full_scale = 0.0001f;

          uint32_t trans_ms = (uint32_t) (ramp_s * 1000.0f * (delta / full_scale));
          if (trans_ms > 600000) trans_ms = 600000;

          if (target <= 0.0001f) {
            auto call = id(out5_light_raw).turn_off();
            call.set_transition_length(trans_ms);
            call.perform();
          } else {
            auto call = id(out5_light_raw).turn_on();
            call.set_brightness(target);
            call.set_transition_length(trans_ms);
            call.perform();
          }

  - id: out5_turn_off
    mode: restart
    then:
      - lambda: |-
          const float limit_pct = id(out5_max_limit_pct).state;
          const float ramp_s    = id(out5_ramp_s).state;

          float limit_frac = limit_pct / 100.0f;
          if (limit_frac < 0.0f) limit_frac = 0.0f;
          if (limit_frac > 1.0f) limit_frac = 1.0f;

          float current = 0.0f;
          if (id(out5_light_raw).current_values.is_on()) {
            current = id(out5_light_raw).current_values.get_brightness();
          }

          float full_scale = limit_frac;
          if (current > full_scale) full_scale = current;
          if (full_scale < 0.0001f) full_scale = 0.0001f;

          uint32_t trans_ms = (uint32_t) (ramp_s * 1000.0f * (current / full_scale));
          if (trans_ms > 600000) trans_ms = 600000;

          auto call = id(out5_light_raw).turn_off();
          call.set_transition_length(trans_ms);
          call.perform();

  - id: out5_auto_off_timer
    mode: restart
    parameters:
      delay_ms: int
    then:
      - delay: !lambda "return delay_ms;"
      - script.execute: out5_turn_off


#:########################################################################################:#
# NUMBER: Brightness normalized to limit, plus per-channel ramp, hard limit, and max ON time
# https://esphome.io/components/number/template.html
#:########################################################################################:#
number:
  #############################################
  # OUT1
  #############################################
  - platform: template
    id: out1_ramp_s
    name: "${out1_name} Ramp"
    icon: mdi:timer-outline
    entity_category: config
    unit_of_measurement: "s"
    min_value: 0.0
    max_value: 30.0
    step: 0.1
    restore_value: true
    initial_value: ${out1_ramp_default_s}
    optimistic: true
    set_action:
      - script.execute: out1_apply_target

  - platform: template
    id: out1_max_limit_pct
    name: "${out1_name} Max Limit"
    icon: mdi:brightness-percent
    entity_category: config
    unit_of_measurement: "%"
    min_value: 0
    max_value: 100
    step: 1
    restore_value: true
    initial_value: ${out1_max_limit_default_pct}
    optimistic: true
    set_action:
      - script.execute: out1_apply_target

  - platform: template
    id: out1_auto_off_mins
    name: "${out1_name} Max ON Time"
    icon: mdi:timer-lock-outline
    entity_category: config
    unit_of_measurement: "min"
    min_value: 1
    max_value: 720
    step: 1
    restore_value: true
    initial_value: ${out1_max_on_time_default_mins}
    optimistic: true

  - platform: template
    id: out1_brightness_pct
    name: "${out1_name} Brightness"
    icon: mdi:lightbulb-on-outline
    unit_of_measurement: "%"
    min_value: 0
    max_value: 100
    step: 1
    restore_value: true
    initial_value: 100
    optimistic: true
    set_action:
      - lambda: |-
          if (x > 0.0f) id(out1_last_nonzero_pct) = x;
      - script.execute: out1_apply_target
      - if:
          condition:
            lambda: |-
              return x > 0.0f;
          then:
            - script.execute:
                id: out1_auto_off_timer
                delay_ms: !lambda "return (int) (id(out1_auto_off_mins).state * 60000.0f);"
          else:
            - script.stop: out1_auto_off_timer

  #############################################
  # OUT2
  #############################################
  - platform: template
    id: out2_ramp_s
    name: "${out2_name} Ramp"
    icon: mdi:timer-outline
    entity_category: config
    unit_of_measurement: "s"
    min_value: 0.0
    max_value: 30.0
    step: 0.1
    restore_value: true
    initial_value: ${out2_ramp_default_s}
    optimistic: true
    set_action:
      - script.execute: out2_apply_target

  - platform: template
    id: out2_max_limit_pct
    name: "${out2_name} Max Limit"
    icon: mdi:brightness-percent
    entity_category: config
    unit_of_measurement: "%"
    min_value: 0
    max_value: 100
    step: 1
    restore_value: true
    initial_value: ${out2_max_limit_default_pct}
    optimistic: true
    set_action:
      - script.execute: out2_apply_target

  - platform: template
    id: out2_auto_off_mins
    name: "${out2_name} Max ON Time"
    icon: mdi:timer-lock-outline
    entity_category: config
    unit_of_measurement: "min"
    min_value: 1
    max_value: 720
    step: 1
    restore_value: true
    initial_value: ${out2_max_on_time_default_mins}
    optimistic: true

  - platform: template
    id: out2_brightness_pct
    name: "${out2_name} Brightness"
    icon: mdi:lightbulb-on-outline
    unit_of_measurement: "%"
    min_value: 0
    max_value: 100
    step: 1
    restore_value: true
    initial_value: 100
    optimistic: true
    set_action:
      - lambda: |-
          if (x > 0.0f) id(out2_last_nonzero_pct) = x;
      - script.execute: out2_apply_target
      - if:
          condition:
            lambda: |-
              return x > 0.0f;
          then:
            - script.execute:
                id: out2_auto_off_timer
                delay_ms: !lambda "return (int) (id(out2_auto_off_mins).state * 60000.0f);"
          else:
            - script.stop: out2_auto_off_timer

  #############################################
  # OUT3
  #############################################
  - platform: template
    id: out3_ramp_s
    name: "${out3_name} Ramp"
    icon: mdi:timer-outline
    entity_category: config
    unit_of_measurement: "s"
    min_value: 0.0
    max_value: 30.0
    step: 0.1
    restore_value: true
    initial_value: ${out3_ramp_default_s}
    optimistic: true
    set_action:
      - script.execute: out3_apply_target

  - platform: template
    id: out3_max_limit_pct
    name: "${out3_name} Max Limit"
    icon: mdi:brightness-percent
    entity_category: config
    unit_of_measurement: "%"
    min_value: 0
    max_value: 100
    step: 1
    restore_value: true
    initial_value: ${out3_max_limit_default_pct}
    optimistic: true
    set_action:
      - script.execute: out3_apply_target

  - platform: template
    id: out3_auto_off_mins
    name: "${out3_name} Max ON Time"
    icon: mdi:timer-lock-outline
    entity_category: config
    unit_of_measurement: "min"
    min_value: 1
    max_value: 720
    step: 1
    restore_value: true
    initial_value: ${out3_max_on_time_default_mins}
    optimistic: true

  - platform: template
    id: out3_brightness_pct
    name: "${out3_name} Brightness"
    icon: mdi:lightbulb-on-outline
    unit_of_measurement: "%"
    min_value: 0
    max_value: 100
    step: 1
    restore_value: true
    initial_value: 100
    optimistic: true
    set_action:
      - lambda: |-
          if (x > 0.0f) id(out3_last_nonzero_pct) = x;
      - script.execute: out3_apply_target
      - if:
          condition:
            lambda: |-
              return x > 0.0f;
          then:
            - script.execute:
                id: out3_auto_off_timer
                delay_ms: !lambda "return (int) (id(out3_auto_off_mins).state * 60000.0f);"
          else:
            - script.stop: out3_auto_off_timer

  #############################################
  # OUT4
  #############################################
  - platform: template
    id: out4_ramp_s
    name: "${out4_name} Ramp"
    icon: mdi:timer-outline
    entity_category: config
    unit_of_measurement: "s"
    min_value: 0.0
    max_value: 30.0
    step: 0.1
    restore_value: true
    initial_value: ${out4_ramp_default_s}
    optimistic: true
    set_action:
      - script.execute: out4_apply_target

  - platform: template
    id: out4_max_limit_pct
    name: "${out4_name} Max Limit"
    icon: mdi:brightness-percent
    entity_category: config
    unit_of_measurement: "%"
    min_value: 0
    max_value: 100
    step: 1
    restore_value: true
    initial_value: ${out4_max_limit_default_pct}
    optimistic: true
    set_action:
      - script.execute: out4_apply_target

  - platform: template
    id: out4_auto_off_mins
    name: "${out4_name} Max ON Time"
    icon: mdi:timer-lock-outline
    entity_category: config
    unit_of_measurement: "min"
    min_value: 1
    max_value: 720
    step: 1
    restore_value: true
    initial_value: ${out4_max_on_time_default_mins}
    optimistic: true

  - platform: template
    id: out4_brightness_pct
    name: "${out4_name} Brightness"
    icon: mdi:lightbulb-on-outline
    unit_of_measurement: "%"
    min_value: 0
    max_value: 100
    step: 1
    restore_value: true
    initial_value: 100
    optimistic: true
    set_action:
      - lambda: |-
          if (x > 0.0f) id(out4_last_nonzero_pct) = x;
      - script.execute: out4_apply_target
      - if:
          condition:
            lambda: |-
              return x > 0.0f;
          then:
            - script.execute:
                id: out4_auto_off_timer
                delay_ms: !lambda "return (int) (id(out4_auto_off_mins).state * 60000.0f);"
          else:
            - script.stop: out4_auto_off_timer

  #############################################
  # OUT5
  #############################################
  - platform: template
    id: out5_ramp_s
    name: "${out5_name} Ramp"
    icon: mdi:timer-outline
    entity_category: config
    unit_of_measurement: "s"
    min_value: 0.0
    max_value: 30.0
    step: 0.1
    restore_value: true
    initial_value: ${out5_ramp_default_s}
    optimistic: true
    set_action:
      - script.execute: out5_apply_target

  - platform: template
    id: out5_max_limit_pct
    name: "${out5_name} Max Limit"
    icon: mdi:brightness-percent
    entity_category: config
    unit_of_measurement: "%"
    min_value: 0
    max_value: 100
    step: 1
    restore_value: true
    initial_value: ${out5_max_limit_default_pct}
    optimistic: true
    set_action:
      - script.execute: out5_apply_target

  - platform: template
    id: out5_auto_off_mins
    name: "${out5_name} Max ON Time"
    icon: mdi:timer-lock-outline
    entity_category: config
    unit_of_measurement: "min"
    min_value: 1
    max_value: 720
    step: 1
    restore_value: true
    initial_value: ${out5_max_on_time_default_mins}
    optimistic: true

  - platform: template
    id: out5_brightness_pct
    name: "${out5_name} Brightness"
    icon: mdi:lightbulb-on-outline
    unit_of_measurement: "%"
    min_value: 0
    max_value: 100
    step: 1
    restore_value: true
    initial_value: 100
    optimistic: true
    set_action:
      - lambda: |-
          if (x > 0.0f) id(out5_last_nonzero_pct) = x;
      - script.execute: out5_apply_target
      - if:
          condition:
            lambda: |-
              return x > 0.0f;
          then:
            - script.execute:
                id: out5_auto_off_timer
                delay_ms: !lambda "return (int) (id(out5_auto_off_mins).state * 60000.0f);"
          else:
            - script.stop: out5_auto_off_timer


#:########################################################################################:#
# SWITCH: On/Off that respects ramping; brightness slider remains unchanged when off
# https://esphome.io/components/switch/template.html
#:########################################################################################:#
switch:
  - platform: template
    id: out1_switch
    name: "${out1_name}"
    icon: mdi:lightbulb
    lambda: |-
      return id(out1_light_raw).current_values.is_on();
    turn_on_action:
      - if:
          condition:
            lambda: |-
              return id(out1_brightness_pct).state <= 0.0f;
          then:
            - number.set:
                id: out1_brightness_pct
                value: !lambda "return id(out1_last_nonzero_pct);"
          else:
            - script.execute: out1_apply_target
      - script.execute:
          id: out1_auto_off_timer
          delay_ms: !lambda "return (int) (id(out1_auto_off_mins).state * 60000.0f);"
    turn_off_action:
      - script.stop: out1_auto_off_timer
      - script.execute: out1_turn_off

  - platform: template
    id: out2_switch
    name: "${out2_name}"
    icon: mdi:lightbulb
    lambda: |-
      return id(out2_light_raw).current_values.is_on();
    turn_on_action:
      - if:
          condition:
            lambda: |-
              return id(out2_brightness_pct).state <= 0.0f;
          then:
            - number.set:
                id: out2_brightness_pct
                value: !lambda "return id(out2_last_nonzero_pct);"
          else:
            - script.execute: out2_apply_target
      - script.execute:
          id: out2_auto_off_timer
          delay_ms: !lambda "return (int) (id(out2_auto_off_mins).state * 60000.0f);"
    turn_off_action:
      - script.stop: out2_auto_off_timer
      - script.execute: out2_turn_off

  - platform: template
    id: out3_switch
    name: "${out3_name}"
    icon: mdi:lightbulb
    lambda: |-
      return id(out3_light_raw).current_values.is_on();
    turn_on_action:
      - if:
          condition:
            lambda: |-
              return id(out3_brightness_pct).state <= 0.0f;
          then:
            - number.set:
                id: out3_brightness_pct
                value: !lambda "return id(out3_last_nonzero_pct);"
          else:
            - script.execute: out3_apply_target
      - script.execute:
          id: out3_auto_off_timer
          delay_ms: !lambda "return (int) (id(out3_auto_off_mins).state * 60000.0f);"
    turn_off_action:
      - script.stop: out3_auto_off_timer
      - script.execute: out3_turn_off

  - platform: template
    id: out4_switch
    name: "${out4_name}"
    icon: mdi:lightbulb
    lambda: |-
      return id(out4_light_raw).current_values.is_on();
    turn_on_action:
      - if:
          condition:
            lambda: |-
              return id(out4_brightness_pct).state <= 0.0f;
          then:
            - number.set:
                id: out4_brightness_pct
                value: !lambda "return id(out4_last_nonzero_pct);"
          else:
            - script.execute: out4_apply_target
      - script.execute:
          id: out4_auto_off_timer
          delay_ms: !lambda "return (int) (id(out4_auto_off_mins).state * 60000.0f);"
    turn_off_action:
      - script.stop: out4_auto_off_timer
      - script.execute: out4_turn_off

  - platform: template
    id: out5_switch
    name: "${out5_name}"
    icon: mdi:lightbulb
    lambda: |-
      return id(out5_light_raw).current_values.is_on();
    turn_on_action:
      - if:
          condition:
            lambda: |-
              return id(out5_brightness_pct).state <= 0.0f;
          then:
            - number.set:
                id: out5_brightness_pct
                value: !lambda "return id(out5_last_nonzero_pct);"
          else:
            - script.execute: out5_apply_target
      - script.execute:
          id: out5_auto_off_timer
          delay_ms: !lambda "return (int) (id(out5_auto_off_mins).state * 60000.0f);"
    turn_off_action:
      - script.stop: out5_auto_off_timer
      - script.execute: out5_turn_off