#:########################################################################################:#
# TITLE: MAIN BEDROOM MAIN LIGHTS SOUTH
# zorruno.com layout v1.1 2026
#:########################################################################################:#
# REPO:
# https://home.fox.co.nz/gitea/zorruno/zorruno-homeassistant/src/branch/master/esphome/esp-masterbdrmsouth.yaml
#:########################################################################################:#
# VERSIONS:
# v1.6 2026-07-26 Suppressed command-producing mirror callbacks during startup
# v1.5 2026-07-25 Standardized MQTT payloads: "ON"/"OFF" → "On"/"Off"
# v1.4 2026-07-20 Flattened retained North and South MQTT status topics to the device roots.
# v1.3 2026-06-12 Reassigned logical controls after live wiring checks.
#                 Relay 1 on both switches = North Lights virtual/Tasmota control.
#                 Relay 2 on both switches = South Lights control; WEST Relay 2 is physical load.
#                 Button 1 now controls North Lights; Button 2 now controls South Lights.
# v1.2 2026-06-12 Reworked after live wiring checks.
#                 Confirmed North Lights are controlled by Tasmota/iFan02 Power1.
#                 Confirmed South Lights physical load is on WEST switch Relay 2.
# v1.1 2026-06-12 North Lights controlled via Tasmota iFan02 MQTT cmnd/stat topics.
# v1.0 2026-06-11 Initial YAML for Master Bedroom South 3-gang switch.
#:########################################################################################:#
# HARDWARE:
# - Zemismart KS-811 Triple push button (ESP8266)
# - Pinout/Schematic: https://community.home-assistant.io/t/zemismart-ks-811-working-with-esphome/
#:########################################################################################:#
# OPERATION NOTES:
# - 3-gang KS-811 triple switch.
# - Relay/Button 1 = Master Bedroom North Lights.
#     * Actual North Lights are controlled by Tasmota/iFan02 light.tasmo_ifan02_3793_bedrm1_1.
#     * Button 1 sends MQTT TOGGLE to the Tasmota POWER1 command topic.
#     * Relay 1 is virtual/mirror only and follows Tasmota POWER1 feedback.
#     * Relay 1 can also be switched from HA; ON/OFF sends explicit Tasmota ON/OFF.
# - Relay/Button 2 = Master Bedroom South Lights.
#     * On this SOUTH switch, Relay 2 is virtual/mirror/control only.
#     * Actual South Lights physical load is WEST switch Relay 2.
#     * Button 2 toggles Relay 2, which sends the South Lights command to the WEST switch.
# - Button 3 = Bedside Lamp Jump/Fade via Home Assistant action.
#     * Requires ESPHome integration permission: "Allow the device to perform Home Assistant actions".
# - Relay 3 = unused output. Forced off/internal; Button 3 does NOT toggle Relay 3.
# - GPIO16 does not support interrupts; polling is used for Button 1 (suppresses warning).
#:########################################################################################:#
# MQTT COMMANDS:
# - North Lights Tasmota command topic:
#   ${north_lights_tasmota_command_topic}
#     Payload "TOGGLE" -> iFan02 Light toggle
#     Payload "ON"     -> iFan02 Light ON
#     Payload "OFF"    -> iFan02 Light OFF
#     Empty payload    -> Query current POWER1 state
#
# - North Lights Tasmota status topic:
#   ${north_lights_tasmota_status_topic}
#     Payload "ON" / "OFF" updates Relay 1 mirror state on both switches.
#
# - North Lights local command topic:
#   ${mqtt_local_command_topic1}/set
#     Payload "On" / "ON"   -> North Lights ON via Tasmota POWER1
#     Payload "Off" / "OFF" -> North Lights OFF via Tasmota POWER1
#     NOTE: TOGGLE is intentionally not handled here to avoid double-toggle if both switches receive it.
#
# - North Lights retained status/sync topic:
#   ${mqtt_local_status_topic1}
#     Payload "On" / "Off" mirrors the actual North Lights state.
#
# - South Lights local command topic:
#   ${mqtt_local_command_topic2}/set
#     Payload "On" / "ON"   -> South Lights ON via WEST Relay 2
#     Payload "Off" / "OFF" -> South Lights OFF via WEST Relay 2
#     NOTE: TOGGLE is intentionally not handled here to avoid ambiguous multi-device toggles.
#
# - South Lights retained status/sync topic:
#   ${mqtt_local_status_topic2}
#     Payload "On" / "Off" mirrors the actual South Lights state.
#:########################################################################################:#
# OFFLINE NOTES:
# a) Home Assistant OFFLINE, WiFi OK, MQTT broker OK
# - Button 1 controls the North Lights through direct Tasmota MQTT command.
# - Button 2 controls the South Lights through MQTT command to the WEST switch.
# - Tasmota feedback updates Relay 1 mirror via stat POWER1.
# - WEST Relay 2 status updates Relay 2 mirror via the South Lights retained status topic.
# - Button 3 cannot toggle the bedside lamp because it requires Home Assistant/API.
#
# b) MQTT OFFLINE (or broker unreachable), Home Assistant/API OK
# - Button 1 attempts HA action fallback to light.toggle the Tasmota/iFan02 HA entity.
# - Button 2 attempts HA action fallback to the WEST physical South Lights relay entity.
# - Button 3 can still toggle the bedside lamp via Home Assistant/API.
#
# c) Home Assistant and MQTT OFFLINE, WiFi OK or entire network OFFLINE
# - This SOUTH switch cannot control the remote loads.
# - Button 3 cannot control the bedside lamp.
# - Accurate time is NOT needed (SNTP not needed).
#:########################################################################################:#

#:########################################################################################:#
# 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-masterbdrmsouth"
  friendly_name: "Master Bedroom Main Lights South"
  description_comment: "Master Bedroom Main Lights South using a Zemismart KS-811 Triple Push Button. Relay/Button 1 North Lights Tasmota/iFan02 virtual mirror, Relay/Button 2 South Lights virtual control, Button 3 toggles bedside lamp jump/fade. (Layout V1.1)"
  device_area: "Master Bedroom"

  # Project Naming
  project_name: "Zemismart Technologies.KS-811 Triple"
  project_version: "v1.6"

  # Passwords & Secrets
  api_key: !secret esp-api_key
  ota_pass: !secret esp-ota_pass
  static_ip_address: !secret esp-masterbdrmsouth_ip
  mqtt_command_main_topic: !secret mqtt_command_main_topic
  mqtt_status_main_topic: !secret mqtt_status_main_topic

  # 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"
  update_interval: "60s"

  # MQTT LOCAL Controls
  # NOTE: device_name1 is North Lights and maps to Relay 1 / Tasmota/iFan02 light.
  mqtt_device_name1: "masterbrdm-northlights"
  mqtt_local_command_topic1: "${mqtt_command_main_topic}/${mqtt_device_name1}"
  mqtt_local_status_topic1: "${mqtt_status_main_topic}/${mqtt_device_name1}"

  # NOTE: device_name2 is South Lights and maps to Relay 2 / WEST switch physical load.
  mqtt_device_name2: "masterbrdm-southlights"
  mqtt_local_command_topic2: "${mqtt_command_main_topic}/${mqtt_device_name2}"
  mqtt_local_status_topic2: "${mqtt_status_main_topic}/${mqtt_device_name2}"

  # Tasmota/iFan02 North Lights MQTT control and feedback
  # NOTE: Tasmota status topic observed as uppercase POWER1. MQTT topics are case-sensitive.
  north_lights_tasmota_command_topic: "cmnd/tasmo-ifan02-3793-bedrm1-1/POWER1"
  north_lights_tasmota_status_topic: "stat/tasmo-ifan02-3793-bedrm1-1/POWER1"

  # Home Assistant fallback entities
  north_lights_ha_entity: "light.tasmo_ifan02_3793_bedrm1_1"
  # NOTE: If HA renames WEST Relay 2 after this YAML name correction, update this entity_id.
  south_lights_physical_entity: "switch.master_bedroom_master_bedroom_main_lights_west_relay_2_master_bedroom_south_lights"

  # Switch/Relay/Button Naming
  switch_1_name: "Master Bedroom North Lights"
  switch_2_name: "Master Bedroom South Lights"
  switch_3_name: "Bedside Lamp Jump/Fade" # NOTE: Relay 3 output is unused and forced off

  # Home Assistant bedside lamp entity controlled by Button 3
  bedside_lamp_entity: "switch.master_bedroom_bedside_lamp_arlec_bulb_master_bedroom_bedside_lamp_arlec_bulb_jump_fade_up_down"

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

  #### 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}"
  area: "${device_area}"
  project:
    name: "${project_name}"
    version: "${project_version}"
  on_boot:
    priority: -100
    then:
      - delay: 5s
      - lambda: "id(command_callbacks_ready) = true;"

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

#:########################################################################################:#
# LOGGING
# https://esphome.io/components/logger.html
#:########################################################################################:#
logger:
  level: "${log_level}"
  #baud_rate: 0

#:########################################################################################:#
# GLOBALS
# Used to suppress MQTT/HA feedback loops and hold the last known remote states.
#:########################################################################################:#
globals:
  # Prevent restored/default mirror states from issuing commands during startup.
  - id: command_callbacks_ready
    type: bool
    restore_value: false
    initial_value: "false"

  - id: north_update_from_sync
    type: bool
    restore_value: false
    initial_value: "false"

  - id: south_update_from_sync
    type: bool
    restore_value: false
    initial_value: "false"

  - id: north_lights_state
    type: bool
    restore_value: false
    initial_value: "false"

  - id: south_lights_state
    type: bool
    restore_value: false
    initial_value: "false"

#:########################################################################################:#
# MQTT: Device-specific MQTT command + retained status/sync triggers
# This adds device-specific MQTT command triggers to the common MQTT configuration.
#:########################################################################################:#
mqtt:
  # Query the iFan02/Tasmota light state whenever this ESP reconnects to MQTT.
  # Tasmota treats an empty command payload as a status query for that command.
  on_connect:
    then:
      - delay: 2s
      - mqtt.publish:
          topic: "${north_lights_tasmota_command_topic}"
          payload: ""

  on_message:
    # -------------------------------------------------------------------------
    # North Lights local command topic -> Tasmota/iFan02 command topic
    # NOTE: TOGGLE is intentionally ignored here to avoid double-toggle if both
    # switches receive the same MQTT command. Buttons publish TOGGLE directly.
    # -------------------------------------------------------------------------
    - topic: "${mqtt_local_command_topic1}/set"
      then:
        - lambda: |-
            if (x == "On" || x == "ON" || x == "on") {
              id(request_north_lights_on).execute();
            } else if (x == "Off" || x == "OFF" || x == "off") {
              id(request_north_lights_off).execute();
            }

    # Tasmota/iFan02 POWER1 feedback is the source of truth for North Lights state.
    - topic: "${north_lights_tasmota_status_topic}"
      then:
        - lambda: |-
            if (x == "ON" || x == "On" || x == "on") {
              id(set_north_on_from_tasmota).execute();
            } else if (x == "OFF" || x == "Off" || x == "off") {
              id(set_north_off_from_tasmota).execute();
            }

    # Retained North status is a secondary mirror/sync path.
    - topic: "${mqtt_local_status_topic1}"
      then:
        - lambda: |-
            if (x == "On" || x == "ON" || x == "on") {
              id(set_north_on_from_sync).execute();
            } else if (x == "Off" || x == "OFF" || x == "off") {
              id(set_north_off_from_sync).execute();
            }

    # -------------------------------------------------------------------------
    # South Lights status. Actual physical South load is WEST Relay 2.
    # This SOUTH switch publishes commands but does NOT subscribe to its own
    # command topic, to avoid command echo/chatter. The WEST switch publishes
    # retained status after the physical Relay 2 changes, and this switch mirrors
    # that status onto its local Relay 2.
    # -------------------------------------------------------------------------
    - topic: "${mqtt_local_status_topic2}"
      then:
        - lambda: |-
            if (x == "On" || x == "ON" || x == "on") {
              id(set_south_on_from_sync).execute();
            } else if (x == "Off" || x == "OFF" || x == "off") {
              id(set_south_off_from_sync).execute();
            }

#:########################################################################################:#
# STATUS LED
# https://esphome.io/components/status_led.html
#:########################################################################################:#
status_led:
  pin:
    number: GPIO2
    inverted: true

#:########################################################################################:#
# BINARY SENSORS
# https://esphome.io/components/binary_sensor/
#:########################################################################################:#
binary_sensor:
  # Button 1 (GPIO16) - polling (GPIO16 has no interrupts)
  # North Lights: direct TOGGLE to Tasmota/iFan02. Relay 1 follows Tasmota feedback only.
  - platform: gpio
    pin:
      number: GPIO16
      mode: INPUT
      inverted: true
    use_interrupt: false
    name: "Button 1: ${switch_1_name}"
    on_press:
      - script.execute: request_north_lights_toggle

  # Button 2 (GPIO05)
  # South Lights: toggles virtual Relay 2, which sends command to WEST physical Relay 2.
  - platform: gpio
    pin:
      number: GPIO05
      mode: INPUT
      inverted: true
    name: "Button 2: ${switch_2_name}"
    on_press:
      - switch.toggle: relay_2

  # Button 3 (GPIO04)
  # NOTE: This does not toggle Relay 3. It checks the HA entity state and calls the opposite HA action.
  - platform: gpio
    pin:
      number: GPIO4
      mode: INPUT
      inverted: true
    name: "Button 3: ${switch_3_name}"
    on_press:
      - script.execute: toggle_bedside_lamp_jump_fade

  # Home Assistant state import used by Button 3 so it can choose ON vs OFF explicitly.
  - platform: homeassistant
    id: bedside_lamp_jump_fade_state
    entity_id: "${bedside_lamp_entity}"
    internal: true

#:########################################################################################:#
# SCRIPT
# https://esphome.io/components/script.html
#:########################################################################################:#
script:
  # ---------------------------------------------------------------------------
  # North Lights helpers
  # Actual North Lights are Tasmota/iFan02 POWER1. Relay 1 is the local mirror/control.
  # ---------------------------------------------------------------------------
  - id: request_north_lights_toggle
    mode: restart
    then:
      - if:
          condition:
            mqtt.connected:
          then:
            - mqtt.publish:
                topic: "${north_lights_tasmota_command_topic}"
                payload: "TOGGLE"
          else:
            - if:
                condition:
                  api.connected:
                    state_subscription_only: true
                then:
                  - homeassistant.action:
                      action: light.toggle
                      data:
                        entity_id: "${north_lights_ha_entity}"
                else:
                  - logger.log:
                      level: WARN
                      format: "Button 1 pressed, but MQTT and HA/API are not connected. North Lights not changed."

  - id: request_north_lights_on
    mode: restart
    then:
      - mqtt.publish:
          topic: "${north_lights_tasmota_command_topic}"
          payload: "On"

  - id: request_north_lights_off
    mode: restart
    then:
      - mqtt.publish:
          topic: "${north_lights_tasmota_command_topic}"
          payload: "Off"

  - id: set_north_on_from_tasmota
    mode: restart
    then:
      - lambda: |-
          id(north_lights_state) = true;
          id(north_update_from_sync) = true;
      - switch.turn_on: relay_1
      - mqtt.publish:
          topic: "${mqtt_local_status_topic1}"
          payload: "On"
          retain: true
      - lambda: |-
          id(north_update_from_sync) = false;

  - id: set_north_off_from_tasmota
    mode: restart
    then:
      - lambda: |-
          id(north_lights_state) = false;
          id(north_update_from_sync) = true;
      - switch.turn_off: relay_1
      - mqtt.publish:
          topic: "${mqtt_local_status_topic1}"
          payload: "Off"
          retain: true
      - lambda: |-
          id(north_update_from_sync) = false;

  - id: set_north_on_from_sync
    mode: restart
    then:
      - lambda: |-
          id(north_lights_state) = true;
          id(north_update_from_sync) = true;
      - switch.turn_on: relay_1
      - lambda: |-
          id(north_update_from_sync) = false;

  - id: set_north_off_from_sync
    mode: restart
    then:
      - lambda: |-
          id(north_lights_state) = false;
          id(north_update_from_sync) = true;
      - switch.turn_off: relay_1
      - lambda: |-
          id(north_update_from_sync) = false;

  # ---------------------------------------------------------------------------
  # South Lights helpers
  # Actual physical South Lights relay is WEST switch Relay 2. Relay 2 is the local mirror/control.
  # ---------------------------------------------------------------------------
  - id: request_south_lights_on
    mode: restart
    then:
      - if:
          condition:
            mqtt.connected:
          then:
            - mqtt.publish:
                topic: "${mqtt_local_command_topic2}/set"
                payload: "On"
          else:
            - if:
                condition:
                  api.connected:
                    state_subscription_only: true
                then:
                  - homeassistant.action:
                      action: switch.turn_on
                      data:
                        entity_id: "${south_lights_physical_entity}"
                else:
                  - logger.log:
                      level: WARN
                      format: "Button 2 pressed, but MQTT and HA/API are not connected. South Lights not changed."

  - id: request_south_lights_off
    mode: restart
    then:
      - if:
          condition:
            mqtt.connected:
          then:
            - mqtt.publish:
                topic: "${mqtt_local_command_topic2}/set"
                payload: "Off"
          else:
            - if:
                condition:
                  api.connected:
                    state_subscription_only: true
                then:
                  - homeassistant.action:
                      action: switch.turn_off
                      data:
                        entity_id: "${south_lights_physical_entity}"
                else:
                  - logger.log:
                      level: WARN
                      format: "Button 2 pressed, but MQTT and HA/API are not connected. South Lights not changed."

  - id: set_south_on_from_sync
    mode: restart
    then:
      - lambda: |-
          id(south_lights_state) = true;
          id(south_update_from_sync) = true;
      - switch.turn_on: relay_2
      - lambda: |-
          id(south_update_from_sync) = false;

  - id: set_south_off_from_sync
    mode: restart
    then:
      - lambda: |-
          id(south_lights_state) = false;
          id(south_update_from_sync) = true;
      - switch.turn_off: relay_2
      - lambda: |-
          id(south_update_from_sync) = false;

  # ---------------------------------------------------------------------------
  # Bedside lamp helper
  # ---------------------------------------------------------------------------
  - id: toggle_bedside_lamp_jump_fade
    mode: restart
    then:
      - if:
          condition:
            api.connected:
              state_subscription_only: true
          then:
            - if:
                condition:
                  binary_sensor.is_on: bedside_lamp_jump_fade_state
                then:
                  - homeassistant.action:
                      action: switch.turn_off
                      data:
                        entity_id: "${bedside_lamp_entity}"
                else:
                  - homeassistant.action:
                      action: switch.turn_on
                      data:
                        entity_id: "${bedside_lamp_entity}"
          else:
            - logger.log:
                level: WARN
                format: "Button 3 pressed, but Home Assistant/API is not connected. Bedside lamp not toggled."

#:########################################################################################:#
# SWITCH
# https://esphome.io/components/switch/
#:########################################################################################:#
switch:
  # Relay 1 (GPIO13) - Master Bedroom North Lights - VIRTUAL/MIRROR ONLY ON THIS SWITCH
  - platform: gpio
    name: "Relay 1: ${switch_1_name}"
    pin: GPIO13
    id: relay_1
    restore_mode: RESTORE_DEFAULT_OFF
    on_turn_on:
      - lambda: |-
          if (id(command_callbacks_ready) && !id(north_update_from_sync)) {
            id(request_north_lights_on).execute();
          }
    on_turn_off:
      - lambda: |-
          if (id(command_callbacks_ready) && !id(north_update_from_sync)) {
            id(request_north_lights_off).execute();
          }

  # Relay 2 (GPIO12) - Master Bedroom South Lights - VIRTUAL/MIRROR ONLY ON THIS SWITCH
  - platform: gpio
    name: "Relay 2: ${switch_2_name}"
    pin: GPIO12
    id: relay_2
    restore_mode: RESTORE_DEFAULT_OFF
    on_turn_on:
      - lambda: |-
          if (id(command_callbacks_ready) && !id(south_update_from_sync)) {
            id(request_south_lights_on).execute();
          }
    on_turn_off:
      - lambda: |-
          if (id(command_callbacks_ready) && !id(south_update_from_sync)) {
            id(request_south_lights_off).execute();
          }

  # Relay 3 (GPIO14) - unused output, intentionally not exposed to Home Assistant.
  # Button 3 controls the bedside lamp via Home Assistant/API instead.
  - platform: gpio
    name: "Relay 3: Unused Output Forced Off"
    pin: GPIO14
    id: relay_3
    restore_mode: ALWAYS_OFF
    internal: true
