##########################################################################################
##########################################################################################
# PMB Electronics 6 Button ESP32 Switch
#
# V1.1 2025-08-06 Various function updates, Ext V calibrated (for my unit)
# V1.0 2025-08-04 Initial Version
##########################################################################################
# PMB Product Details Page https://rcbeacon.com/blog/?p=5488
#
# DEVICE GPIO
# ------------------------------------------
# GPIO36 ADC1 (knob)  GPIO39 ADC2 (Supply V)
# GPIO19 Button1      GPIO21 LED1
# GPIO35 Button2      GPIO25 LED2
# GPIO05 Button3      GPIO18 LED3
# GPIO33 Button4      GPIO26 LED4
# GPIO16 Button5      GPIO17 LED5
# GPIO12 Button6      GPIO13 LED6 (6 button/led may not be Populated)
#
# OPERATION (as at V1.1)
# 1. Buttons 1-3: These toggle a virtual relay (v_relay1 to 3) and also turn on the 
#    associated LED when pressed.  Inputs are debounced.
# 2. Button 4: As with buttons 1-3,but also if double pressed, the v_relay only 
#    stays on for a short period (default 30s) then switches off.  The LED fast flashes.
#    If it is held down, it stays on for longer (1hr default) and the LED slow flashes.
# 3. Button 5: As with Buttons 1-3 BUT if double pressed, a different v_relay switches on 
#    (5B) and the LED slowly pulses.  If held down for 2 seconds, a 3rd v_relay switches
#    on (5C) and the LED fast pulses. The 3 v_relays are exclusive, only one can be 
#    on at a time.
# 4. The knob gives a reported value of 0-100% when turned (as well as the adc value).
#    this can be calibrated in the substitutions section
# 5. The system supply voltage is reported (~10-20V). 
# 6. Power Loss: LEDs and v_relays are restored on power loss (restore settings saved 
#    every 10s). If v_relay4 was turned on with a timer (double press or hold), the 
#    state isn't restored on power loss.
# 7. There is an overall brightness slider/variable.  This allows the LEDs (except when 
#    flashing) to have a maximum brightness (eg for night mode)
#
# NOTES
# 1. To Flash via ESPHome, you likely have to connect to the computer, start the flash 
#    then hold down I00 button.  I think GPIO12 connection is preventing the flash process.
# 2. OTA flash to an existing Tasmota device is likely a bit hard with and ESP32 as they
#    expect a signed Tasmota binary... and partition layout needs to be fixed anyway
# 3. ESPHome warns on compiling: "legacy adc driver is deprecated, please migrate to use 
#    esp_adc/adc_oneshot.h and esp_adc/adc_continuous.h for oneshot mode and continuous mode 
#    drivers respectively" [-Wcpp]" (I'm not bothered with this)
# 4. Normally I'd include SNTP for timing/scheduling functions, and uptime diagnostics etc.  
#    I have commented this out in the packages section as not sure it is really needed here.
#
##########################################################################################
##########################################################################################

##########################################################################################
# SPECIFIC DEVICE VARIABLE SUBSTITUTIONS
# USUALLY YOU WILL ONLY NEED TO CHANGE VALUES UNDER THIS SECTION
# If NOT using a secrets file, just replace "!secret my_key" with the password etc (in quotes)
##########################################################################################
substitutions:
  # Device Naming
  device_name: "esp-6buttontest-pmb"  # the filename should be the device_name.yaml
  friendly_name: "PMB Light Switch Test"
  description_comment: "Multi Button Wallswitch in standard AU/NZ flush size. In this case 5 Buttons with LEDs and one Knob"
  device_area: "Lounge" # Allows ESP device to be automatically linked to an 'Area' in Home Assistant.  

  # Wifi details
  wifi_ssid: !secret ha_wifi_ssid
  wifi_password: !secret ha_wifi_password
  fallback_ap_password: !secret fallback_ap_password  # if it doesn't connect, it will create its own AP

  # Passwords
  api_key: !secret esp-api_key # unfortunately you can't use substitutions inside secrets names
  ota_pass: !secret esp-ota_pass # unfortunately you can't use substitutions inside secrets names
  static_ip_address: !secret esp-6buttontest-pmb_ip
  
  # Add these if we are giving it a static ip, or remove them in the Wifi section
  static_ip_subnet: !secret ha_wifi_subnet
  static_ip_gateway: !secret ha_wifi_gateway
  static_ip_dns1: !secret ha_wifi_dns1
  static_ip_dns2: !secret ha_wifi_dns2

  # MQTT Settings
  mqtt_server: !secret ha_mqtt_server
  mqtt_username: !secret ha_mqtt_username
  mqtt_password: !secret ha_mqtt_password
  mqtt_topic: "esphome" # main topic for the mqtt server, call it what you like

  # Web Server Settings
  web_server_username: !secret web_server_username
  web_server_password: !secret web_server_password

  # Switch Naming
  switch_1_name: "Red"
  switch_2_name: "White"
  switch_3_name: "Green" 
  switch_4_name: "Orange" 
  switch_5_name: "Blue"
  switch_5b_name: "Blue (Double)"
  switch_5c_name: "Blue (Hold)"
  # switch_6_name: "Spare"
  variable_1_name: "Dimmer"
  variable_1_full_scale: "3.14"  # ADC reading when at 100% (adjust after calibration)
  variable_1_min_scale:  "0.14"   # volts at ADC for 0% (adjust after calibration)

  # Device Settings
  log_level: "INFO" # Define logging level: NONE, ERROR, WARN, INFO, DEBUG (Default), VERBOSE, VERY_VERBOSE
  update_interval: "60s" # update time for for general sensors etc
  pwm_freq: "1000Hz" # ESPHome default is 1000Hz, but will go much higher... (not much point for a switch LED)

  # Network reconnect every x hours to ensure best access point
  # This is my own script.  No need to use if only one AP or it is always fixed.
  base_interval_hours: "23"    # Base interval in hours
  random_offset_max_minutes: "59"   # Max random offset in minutes

  # Enables faster network connections, with last connected SSID being connected to and 
  # no full scan for SSID being undertaken
  wifi_fast_connect: "false"

  # Define a domain for this device to use. i.e. iot.home.lan (so device will appear as 
  # athom-smart-plug-v2.iot.home.lan in DNS/DHCP logs)
  dns_domain: ".local"

  # Automatically add the mac address to the name eg so you can use a single firmware for all devices
  # I don't tend to like doing this, but good for many identical devices
  add_mac_suffix: "false"

  # Enable or disable the use of IPv6 networking on the device
  ipv6_enable: "false"

##########################################################################################
# PACKAGES
# Included Common Packages from other files.
# https://esphome.io/components/packages.html
##########################################################################################
# NOTE: ALL COMMENTED AS FOR THIS TEST EVERYTHING IS IN ONE YAML FILE (except no SNTP)
##########################################################################################
#packages:
  #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}"
  #common_api: !include
  #  file: common/api_common.yaml
  #  vars:
  #    local_api_key: "${api_key}"
  #common_webportal: !include
  #  file: common/webportal_common.yaml
  #common_mqtt: !include
  #  file: common/mqtt_common.yaml
  #  vars:
  #    local_device_name: "${device_name}"
  #common_sntp: !include
  #  file: common/sntp_common.yaml
  #common_general_sensors: !include
  #  file: common/sensors_common.yaml
  #  vars:
  #    local_friendly_name: "${friendly_name}"
  #    local_update_interval: "${update_interval}"

##########################################################################################
# WIFI COMPONENT
# https://esphome.io/components/wifi.html
#
# Power Save mode (can reduce wifi reliability)
# NONE (least power saving, Default for ESP8266)
# LIGHT (Default for ESP32)
# HIGH (most power saving)
##########################################################################################
wifi:
  ssid: ${wifi_ssid}
  password: ${wifi_password}
  #enable_rrm: true           # (ESP32 only) enable 802.11k Radio Resource Management
  #enable_btm: true           # (ESP32 only) enable 802.11v BSS Transition Management
  #power_save_mode: LIGHT   # https://esphome.io/components/wifi.html#wifi-power-save-mode
  manual_ip: # optional static IP address
    static_ip: ${static_ip_address}
    gateway: ${static_ip_gateway}
    subnet: ${static_ip_subnet}
    dns1: ${static_ip_dns1}
    dns2: ${static_ip_dns2}
  ap: # Details for fallback hotspot in case wifi connection fails https://esphome.io/components/wifi.html#access-point-mode
    ssid: ${device_name} AP
    password: ${fallback_ap_password}
    ap_timeout: 10min # Time until it brings up fallback AP. default is 1min
  # Allow rapid re-connection to previously connect WiFi SSID, skipping scan of all SSID
  fast_connect: ${wifi_fast_connect}
  # Define dns domain / suffix to add to hostname
  domain: ${dns_domain}

#captive_portal: # extra fallback mechanism for when connecting if the configured WiFi fails

##########################################################################################
# ESPHome OTA UPDATES
# https://esphome.io/components/ota/esphome.html
##########################################################################################
ota:
  - platform: esphome
    password: ${ota_pass}
    version: 2

##########################################################################################
# SAFE MODE
# Safe mode will detect boot loops
# https://esphome.io/components/safe_mode
##########################################################################################
safe_mode:

##########################################################################################
# NETWORK COMPONENT
# global configuration for all types of networks
# https://esphome.io/components/network.html
##########################################################################################
network:
  enable_ipv6: ${ipv6_enable}

##########################################################################################
# MQTT CLIENT COMPONENT
# https://esphome.io/components/mqtt.html?highlight=mqtt
# MUST also have api enabled if you enable MQTT!
##########################################################################################
mqtt:
  broker: ${mqtt_server}
  topic_prefix: ${mqtt_topic}/${device_name}
  username: ${mqtt_username}
  password: ${mqtt_password}
  discovery: false # enable entity discovery (true is default)
  discover_ip: false # enable device discovery (true is default)
  id: mqtt_client
  reboot_timeout: 0s  # same for MQTT

##########################################################################################
# WEB SERVER COMPONENT
# https://esphome.io/components/web_server.html
# Web Portal for display and monitoring
# Turning this off is maybe a good idea to save resources, especially on an esp8266.
##########################################################################################   
web_server:
  port: 80
  auth:
    username: ${web_server_username} # probably a good idea to secure it
    password: ${web_server_password}

##########################################################################################
# NATIVE API COMPONENT
# Enable the Home Assistant API
# https://esphome.io/components/api.html
##########################################################################################
api:
  encryption:
    key: ${api_key}
  reboot_timeout: 0s  # disables watchdog reboot on API failure  

##########################################################################################
# ESPHome CORE CONFIGURATION
# https://esphome.io/components/esphome.html
##########################################################################################
esphome:
  name: ${device_name}
  friendly_name: ${friendly_name}
  comment: ${description_comment} # Appears on the ESPHome page in HA
  area: ${device_area}
  on_boot:
    then:
      - lambda: |-
          // if the user had manually left it ON, re-apply it now
          if (id(vrelay4_manual_state)) {
            id(vrelay_4).turn_on();
          }
          // now restore all your _other_ relays as you already had them
          if (id(vrelay_1_state))   id(vrelay_1).turn_on();
          if (id(vrelay_2_state))   id(vrelay_2).turn_on();
          if (id(vrelay_3_state))   id(vrelay_3).turn_on();
          if (id(vrelay_5_state))   id(vrelay_5).turn_on();
          if (id(vrelay_5b_state))  id(vrelay_5b).turn_on();
          if (id(vrelay_5c_state))  id(vrelay_5c).turn_on();

##########################################################################################
# ESP PLATFORM AND FRAMEWORK
# https://esphome.io/components/esp32.html
##########################################################################################
# Suggest using ESP-IDF Framework. Changes might need need to be cable flashed here to 
# reset the partitioning
##########################################################################################
esp32:
  board: esp32dev
  framework:
    type: esp-idf  # "esp-idf" OR "arduino". 
    version: recommended # recommended, latest or dev

preferences:
  flash_write_interval: 10s  # not too important but anything written for reboot mem will wear the flash

##########################################################################################
# GLOBAL VARIABLES
# https://esphome.io/components/globals.html
##########################################################################################
# We are using globals here to store the states of the virtual relays
# They will also be restored on power loss.
##########################################################################################
globals:
  - id: vrelay_1_state
    type: bool
    restore_value: yes
    initial_value: 'false'
  - id: vrelay_2_state
    type: bool
    restore_value: yes
    initial_value: 'false'
  - id: vrelay_3_state
    type: bool
    restore_value: yes
    initial_value: 'false'
  - id: vrelay_4_state
    type: bool
    restore_value: yes
    initial_value: 'false'
  - id: vrelay4_manual_state
    type: bool
    restore_value: yes
    initial_value: 'false'
  - id: vrelay4_timer_state
    type: bool
    restore_value: no
    initial_value: 'false'
  - id: vrelay_5_state
    type: bool
    restore_value: yes
    initial_value: 'false'
  - id: vrelay_5b_state
    type: bool
    restore_value: yes
    initial_value: 'false'
  - id: vrelay_5c_state
    type: bool
    restore_value: yes
    initial_value: 'false'

  - id: max_led_brightness
    type: float
    restore_value: yes
    initial_value: '1.0'   # default to 100% on first run

##########################################################################################    
# LOGGER COMPONENT
# https://esphome.io/components/logger.html
# Logs all log messages through the serial port and through MQTT topics.
########################################################################################## 
logger:
  level: ${log_level}  #INFO Level suggested, or DEBUG for testing

##########################################################################################
# STATUS LED
# https://esphome.io/components/status_led.html
##########################################################################################
# This is usually the blue LED on the ESP32 Dev board
# Status_LED can show ESPHome errors and warnings
##########################################################################################
status_led:
  pin:
    number: GPIO2
    inverted: yes
    ignore_strapping_warning: true # GPIO2: This just supresses a warning, we know what we are doing

##########################################################################################
# SCRIPT COMPONENT
# https://esphome.io/components/script.html
# Scripts can be executed nearly anywhere in your device configuration with a single call.
##########################################################################################
script:
  # Restart Networking every x hours + rand mins.  Starts on reboot and always runs
  # This ensures that the device is connected to the best AP, but no need for it
  # if one AP and it is always reliable.
  - id: random_reconnect
    mode: restart
    then:
      - lambda: |-
          // Compute total delay: base hours + random offset minutes
          uint32_t extra;
          #if defined(ESP32)
          // ESP32 (both Arduino & IDF builds) uses esp_random()
          extra = esp_random() % (${random_offset_max_minutes} + 1);
          #elif defined(ESP8266)
          // ESP8266 Arduino core
          extra = os_random() % (${random_offset_max_minutes} + 1);
          #else
          // Fallback to esp_random() on other platforms
          extra = esp_random() % (${random_offset_max_minutes} + 1);
          #endif
          uint32_t total_s = ${base_interval_hours} * 3600 + extra * 60;
          ESP_LOGI("random_reconnect", "Next reconnect in %u seconds", total_s);
          // Delay inside lambda (blocks script execution but OK for reconnect timing)
          delay(total_s * 1000);
      - logger.log: "network_check: performing reconnect"
      - wifi.disable: {}
      - delay: 1s
      - wifi.enable: {}
      - script.execute: random_reconnect
  # Button 4: Double-press short timer (should NOT restore after power loss)
  - id: button4_short
    then:
      - lambda: |-
          // Timed mode (not manual) → will NOT be restored on reboot
          id(vrelay4_manual_state) = false;
      - switch.turn_off: vrelay_4     # keep the virtual relay OFF during timer
      - lambda: |-
          // Start 30 s fast-flash timer
          id(vrelay4_timer_state) = true;
          {
            auto call = id(led4).turn_on();
            call.set_brightness(id(max_led_brightness));
            call.set_effect("Flash Fast");
            call.perform();
          }
      - delay: 30s
      - lambda: |-
          // Timer expired → clear
          id(vrelay4_timer_state) = false;
      - light.turn_off: led4

  # Button 4: Hold long timer (should NOT restore after power loss)
  - id: button4_long
    then:
      - lambda: |-
          // Timed mode (not manual) → will NOT be restored on reboot
          id(vrelay4_manual_state) = false;
      - switch.turn_off: vrelay_4     # keep the virtual relay OFF during timer
      - lambda: |-
          // Start 60 min slow-flash timer
          id(vrelay4_timer_state) = true;
          {
            auto call = id(led4).turn_on();
            call.set_brightness(id(max_led_brightness));
            call.set_effect("Flash Slow");
            call.perform();
          }
      - delay: 60min
      - lambda: |-
          // Timer expired → clear
          id(vrelay4_timer_state) = false;
      - light.turn_off: led4

##########################################################################################
# BINARY SENSORS
# https://esphome.io/components/binary_sensor/
##########################################################################################
# These are the input buttons on GPIO.
# No "mode: INPUT_PULLUP" needed as they have external pullups
# Switches pull to GND so "inverted:true" for ESPHome to report ON when pressed.
# They will toggle the virtual relays when pressed (the vrelays will toggle the LEDs)
##########################################################################################
binary_sensor:
  - platform: gpio
    pin:
      number: GPIO19
      mode: INPUT
      inverted: true
    name: "Button 1: ${switch_1_name}"
    filters:
      - delayed_on: 50ms
      - delayed_off: 50ms
    on_press:
      - switch.toggle: vrelay_1

  - platform: gpio
    pin:
      number: GPIO35
      mode: INPUT
      inverted: true
    name: "Button 2: ${switch_2_name}"
    filters:
      - delayed_on: 50ms
      - delayed_off: 50ms
    on_press:
      - switch.toggle: vrelay_2

  - platform: gpio
    pin:
      number: GPIO05
      mode: INPUT
      inverted: true
      ignore_strapping_warning: true # GPIO05: This just supresses a warning, we know what we are doing
    name: "Button 3: ${switch_3_name}"
    filters:
      - delayed_on: 50ms
      - delayed_off: 50ms
    on_press:
      - switch.toggle: vrelay_3

  - platform: gpio
    pin:
      number: GPIO33
      mode: INPUT
      inverted: true
    name: "Button 4: ${switch_4_name}"
    filters:
      - delayed_on: 50ms
      - delayed_off: 50ms
    on_multi_click:
      # Single click: cancel timers, then toggle relay+LED
      - timing:
          - ON for 50ms to 400ms
          - OFF for at least 250ms
        then:
          - script.stop: button4_short
          - script.stop: button4_long
          - switch.toggle: vrelay_4
      # Double click → short sequence
      - timing:
          - ON for 50ms to 400ms
          - OFF for 50ms to 300ms
          - ON for 50ms to 400ms
        then:
          - script.stop: button4_long
          - script.execute: button4_short
      # Hold → long sequence
      - timing:
          - ON for at least 2s
        then:
          - script.stop: button4_short
          - script.execute: button4_long

  - platform: gpio
    pin:
      number: GPIO16
      mode: INPUT
      inverted: true
    name: "Button 5: ${switch_5_name}"
    filters:
      - delayed_on: 30ms
      - delayed_off: 30ms
    on_multi_click:
      # Single click: if any of 5/5B/5C is on → turn them all off; else turn on 5
      - timing:
          - ON for 50ms to 400ms
          - OFF for at least 250ms
        then:
          - lambda: |-
              if (id(vrelay_5_state) || id(vrelay_5b_state) || id(vrelay_5c_state)) {
                id(vrelay_5).turn_off();
                id(vrelay_5b).turn_off();
                id(vrelay_5c).turn_off();
              } else {
                id(vrelay_5).turn_on();
              }

      # Double click → vrelay_5b
      - timing:
          - ON for 50ms to 400ms
          - OFF for 50ms to 300ms
          - ON for 50ms to 400ms
        then:
          - switch.toggle: vrelay_5b

      # Hold → vrelay_5c
      - timing:
          - ON for at least 2s
        then:
          - switch.toggle: vrelay_5c

  # FUTURE: Board allows for 6 buttons
  #- platform: gpio
  #  pin:
  #    number: GPIO12
  #    mode: INPUT
  #    inverted: true
  #  name: "Button 6: ${switch_6_name}"
  #  filters:
  #    - delayed_on: 50ms
  #    - delayed_off: 50ms
  #  on_press:
  #    - switch.toggle: vrelay_6

    ## DIAGNOSTIC ONLY SENSORS BELOW ##
  - platform: status
    name: "Network Status"
    icon: mdi:check-network-outline
    entity_category: "diagnostic"    

##########################################################################################
# OUTPUT COMPONENT
# https://esphome.io/components/light/index.html
##########################################################################################
# An OUTPUT can be binary (0,1) or float, which is any value between 0 and 1.
# PWM Outputs such as "ledc" are float. https://esphome.io/components/output/ledc.html
# We could change frequency: "100Hz" and/or resolution: "12", but these are the default.
##########################################################################################
output:
  - platform: ledc
    id: led1_output
    pin: GPIO21
    frequency: ${pwm_freq}
  - platform: ledc
    id: led2_output
    pin: GPIO25
    frequency: ${pwm_freq}
  - platform: ledc
    id: led3_output
    pin: GPIO18
    frequency: ${pwm_freq}
  - platform: ledc
    id: led4_output
    pin: GPIO26
    frequency: ${pwm_freq}
  - platform: ledc
    id: led5_output
    pin: GPIO17
    frequency: ${pwm_freq}

  # FUTURE: Board allows for 6 buttons
  #- platform: ledc
  #  id: led6_output
  #  pin: GPIO13
  #  frequency: ${pwm_freq}

##########################################################################################
# LIGHT COMPONENT
# https://esphome.io/components/light/index.html
##########################################################################################
light:
  # We are just going to use simple monochromatic component as only one colour to PWM control
  # This doesn't actually do anything by itself, you need to tied it to an OUTPUT component.
  - platform: monochromatic
    id: led1
    output: led1_output
    name: "LED 1: ${switch_1_name}"
  - platform: monochromatic
    id: led2
    output: led2_output
    name: "LED 2: ${switch_2_name}"
  - platform: monochromatic
    id: led3
    output: led3_output
    name: "LED 3: ${switch_3_name}"
  - platform: monochromatic
    id: led4
    output: led4_output
    name: "LED 4: ${switch_4_name}"
    effects:
      - strobe:
          name: "Flash Slow"
          colors:
            - state: ON
              duration: 1s
            - state: OFF
              duration: 0.5s
      - strobe:
          name: "Flash Fast"
          colors:
            - state: ON
              duration: 500ms
            - state: OFF
              duration: 100ms
  - platform: monochromatic
    id: led5
    output: led5_output
    name: "LED 5: ${switch_5_name}"
    effects:
      # Pulse 0.5s: smooth ramp between 0% and current brightness
      - pulse:
          name: "Pulse 0.5s"
          transition_length:
            on_length: 0.5s
            off_length: 0.5s
          update_interval: 1s
      # Pulse 0.1s: ramp up 0.5s / down 0.1s
      - pulse:
          name: "Pulse 0.1s"
          transition_length:
            on_length: 0.5s
            off_length: 0.1s
          update_interval: 600ms

  # FUTURE: Board allows for 6 buttons
  #- platform: monochromatic
  #  id: led6
  #  output: led6_output
  #  name: "LED 6: ${switch_6_name}"

##########################################################################################
# SWITCH COMPONENT
# https://esphome.io/components/switch/
##########################################################################################
switch:
  # Normally SWITCH would be for things like a relay output and turning that on and off.
  # We are using it for virtual relays, that show up in Home Assistant etc, and toggling
  # them along with the LEDs when the buttons are pressed.
  # Separating them from the LEDs as outputs means we can do thinks like light blinking etc
  # but still leave the switch outputs toggled on or off. 
  - platform: template
    name: "Output 1: ${switch_1_name}"
    id: vrelay_1
    optimistic: true
    restore_mode: RESTORE_DEFAULT_OFF
    lambda: |-
      return id(vrelay_1_state);
    turn_on_action:
      - lambda: |-
          auto call = id(led1).turn_on();
          call.set_brightness(id(max_led_brightness));
          call.perform();
      - lambda: |-
          id(vrelay_1_state) = true;
    turn_off_action:
      - light.turn_off: led1
      - lambda: |-
          id(vrelay_1_state) = false;

  - platform: template
    name: "Output 2: ${switch_2_name}"
    id: vrelay_2
    optimistic: true
    restore_mode: RESTORE_DEFAULT_OFF
    lambda: |-
      return id(vrelay_2_state);
    turn_on_action:
      - lambda: |-
          auto call = id(led2).turn_on();
          call.set_brightness(id(max_led_brightness));
          call.perform();
      - lambda: |-
          id(vrelay_2_state) = true;
    turn_off_action:
      - light.turn_off: led2
      - lambda: |-
          id(vrelay_2_state) = false;

  - platform: template
    name: "Output 3: ${switch_3_name}"
    id: vrelay_3
    optimistic: true
    restore_mode: RESTORE_DEFAULT_OFF
    lambda: |-
      return id(vrelay_3_state);
    turn_on_action:
      - lambda: |-
          auto call = id(led3).turn_on();
          call.set_brightness(id(max_led_brightness));
          call.perform();
      - lambda: |-
          id(vrelay_3_state) = true;
    turn_off_action:
      - light.turn_off: led3
      - lambda: |-
          id(vrelay_3_state) = false;

  - platform: template
    name: "Output 4: ${switch_4_name}"
    id: vrelay_4
    optimistic: true
    restore_mode: RESTORE_DEFAULT_OFF
    lambda: |-
      // our single "source of truth" is vrelay_4_state
      return id(vrelay_4_state);

    turn_on_action:
      - lambda: |-
          // a true manual toggle
          id(vrelay4_manual_state) = true;
          // clear any leftover timer (shouldn't happen)
          id(vrelay4_timer_state)  = false;
          id(vrelay_4_state)       = true;
          {
            auto call = id(led4).turn_on();
            call.set_brightness(id(max_led_brightness));
            call.perform();
          }

    turn_off_action:
      - lambda: |-
          // manual switch-off
          id(vrelay4_manual_state) = false;
          id(vrelay4_timer_state)  = false;
          id(vrelay_4_state)       = false;
      - light.turn_off: led4

  - platform: template
    name: "Output 5: ${switch_5_name}"
    id: vrelay_5
    optimistic: true
    restore_mode: RESTORE_DEFAULT_OFF
    lambda: |-
      return id(vrelay_5_state);
    turn_on_action:
      - switch.turn_off: vrelay_5b
      - switch.turn_off: vrelay_5c
      - lambda: |-
          auto call = id(led5).turn_on();
          call.set_brightness(id(max_led_brightness));
          call.perform();
      - lambda: |-
          id(vrelay_5_state) = true;
    turn_off_action:
      - light.turn_off: led5
      - lambda: |-
          id(vrelay_5_state) = false;

  - platform: template
    name: "Output 5B: ${switch_5b_name}"
    id: vrelay_5b
    optimistic: true
    restore_mode: RESTORE_DEFAULT_OFF
    lambda: |-
      return id(vrelay_5b_state);
    turn_on_action:
      - switch.turn_off: vrelay_5
      - switch.turn_off: vrelay_5c
      - lambda: |-
          auto call = id(led5).turn_on();
          call.set_brightness(id(max_led_brightness));
          call.set_effect("Pulse 0.5s");
          call.perform();
      - lambda: |-
          id(vrelay_5b_state) = true;
    turn_off_action:
      - light.turn_off: led5
      - lambda: |-
          id(vrelay_5b_state) = false;

  - platform: template
    name: "Output 5C: ${switch_5c_name}"
    id: vrelay_5c
    optimistic: true
    restore_mode: RESTORE_DEFAULT_OFF
    lambda: |-
      return id(vrelay_5c_state);
    turn_on_action:
      - switch.turn_off: vrelay_5
      - switch.turn_off: vrelay_5b
      - lambda: |-
          auto call = id(led5).turn_on();
          call.set_brightness(id(max_led_brightness));
          call.set_effect("Pulse 0.1s");
          call.perform();
      - lambda: |-
          id(vrelay_5c_state) = true;
    turn_off_action:
      - light.turn_off: led5
      - lambda: |-
          id(vrelay_5c_state) = false;

  
  # FUTURE: Board allows for 6 buttons
  #- platform: template
  #  name: "Output 6: ${switch_6_name}"
  #  id: vrelay_6
  #  lambda: |-
  #    return id(vrelay_6_state);
  #  turn_on_action:
  #    - light.turn_on: led6
  #    - lambda: |-
  #        id(vrelay_6_state) = true;
  #  turn_off_action:
  #    - light.turn_off: led6
  #    - lambda: |-
  #        id(vrelay_6_state) = false;

##########################################################################################
# SENSOR COMPONENT
# https://esphome.io/components/sensor/
##########################################################################################
sensor:
  # Raw ADC reading from GPIO36
  # ADC Component https://esphome.io/components/sensor/adc.html
  - platform: adc
    pin: GPIO36
    id: dimmer_raw
    name: "${variable_1_name} Raw"
    attenuation: 12db
    update_interval: 0.5s
    on_value:
      then:
        - lambda: |-
            // Convert raw ADC (x) to volts
            float volts = x;
            // Apply min/max calibration
            float min_v = ${variable_1_min_scale};
            float max_v = ${variable_1_full_scale};
            // Calculate percentage between min and max
            float pct = ((volts - min_v) / (max_v - min_v)) * 100.0;
            // Clamp result between 0 and 100
            if (pct > 100.0) pct = 100.0;
            if (pct < 0.0) pct = 0.0;
            // Publish to percentage sensor
            id(dimmer_percent).publish_state(pct);
  # Percentage sensor derived from raw ADC
  - platform: template
    id: dimmer_percent
    name: "${variable_1_name}"
    unit_of_measurement: "%"
    accuracy_decimals: 0
  # External supply voltage from GPIO39
  - platform: adc
    pin: GPIO39
    name: "External Supply Voltage"
    id: supply_voltage
    attenuation: 12db 
    update_interval: 2s
    filters:
      - calibrate_linear:  # https://esphome.io/components/sensor/#calibrate-linear
          # Format: raw_value -> real_voltage_at_pin
          - 1.80 -> 20.00    # Example: ADC reads X when pin is -> actually Y
          - 1.35 -> 15.00    
          - 0.89 -> 10.00    
      #- multiply: 4.00      # Voltage divider ratio
    unit_of_measurement: "V"


    ## DIAGNOSTIC ONLY SENSORS BELOW ##
  - platform: uptime # Uptime for this device in seconds
    name: "Uptime (s):"
    update_interval: ${update_interval}
    id: uptime_sensor
    entity_category: "diagnostic"
  - platform: wifi_signal # Wifi Strength
    name: "Wifi (dB):"
    id: wifi_signal_db
    update_interval: ${update_interval}
    entity_category: "diagnostic"
  - platform: copy # Reports the WiFi signal strength in %
    source_id: wifi_signal_db
    name: "WiFi (%):"
    filters:
      - lambda: return min(max(2 * (x + 100.0), 0.0), 100.0);
    unit_of_measurement: "%"
    entity_category: "diagnostic"

##########################################################################################
# TEXT SENSOR COMPONENT
# https://esphome.io/components/text_sensor/index.html
##########################################################################################
text_sensor:
    ## DIAGNOSTIC ONLY SENSORS BELOW ##
  - platform: version
    name: "Version:"
    entity_category: "diagnostic"
  - platform: wifi_info
    ip_address:
      icon: mdi:ip-network
      entity_category: diagnostic
      name: "IP Address:"
    ssid:
      name: "Connected SSID"
      icon: mdi:wifi-strength-2
      entity_category: diagnostic
    mac_address:
      name: "MAC Address:"
      icon: mdi:network-pos
      entity_category: diagnostic
  - platform: uptime # Uptime for this device human readable
    name: "Uptime:"
    icon: mdi:clock-start
    update_interval: ${update_interval}
    entity_category: "diagnostic"

##########################################################################################
# BUTTON COMPONENT
# https://esphome.io/components/button/
##########################################################################################
# Diagnostic buttons ued here and non-active by default, activated if needed in HA
##########################################################################################
button:
    ## DIAGNOSTIC ONLY BUTTONS BELOW ##
  - platform: safe_mode
    name: "Safe Mode Restart:"
    entity_category: "diagnostic"
    disabled_by_default: true  # Need to activate them in HA
  - platform: restart
    name: "Restart:"
    entity_category: "diagnostic"
    disabled_by_default: true
  - platform: factory_reset
    name: "FACTORY RESET:"
    entity_category: "diagnostic"
    disabled_by_default: true

##########################################################################################
# NUMBER COMPONENT
# https://esphome.io/components/number/
##########################################################################################
number:
  - platform: template
    name: "LED Max Brightness"
    id: led_max_brightness_number
    optimistic: true
    restore_value: true
    initial_value: "1.0"      # show 100% on first boot
    min_value: 0.2
    max_value: 1.0
    step: 0.05
    unit_of_measurement: "%"
    set_action:
      - lambda: |-
          // store the new max
          id(max_led_brightness) = x;
          // if any LED is currently on, re-apply at new brightness
          if (id(vrelay_1_state)) {
            auto call = id(led1).turn_on();
            call.set_brightness(x);
            call.perform();
          }
          if (id(vrelay_2_state)) {
            auto call = id(led2).turn_on();
            call.set_brightness(x);
            call.perform();
          }
          if (id(vrelay_3_state)) {
            auto call = id(led3).turn_on();
            call.set_brightness(x);
            call.perform();
          }
          if (id(vrelay_4_state)) {
            auto call = id(led4).turn_on();
            call.set_brightness(x);
            call.perform();
          }
          if (id(vrelay_5_state)) {
            auto call = id(led5).turn_on();
            call.set_brightness(x);
            call.perform();
          }