#:########################################################################################:#
# TITLE: CEILING FAN - BEDROOM 3
# zorruno.com layout v1.1 2026
#:########################################################################################:#
# REPO:
# https://home.fox.co.nz/gitea/zorruno/zorruno-homeassistant/src/branch/master/esphome/esp-bedrm3ceilingfan.yaml
#:########################################################################################:#
# VERSIONS:
# V1.1 2026-03-05 Updated yaml to zorruno layout V1.1
# V1.0 2025-07-21 First Setup (and replacement of Tasmota)
#:########################################################################################:#
# HARDWARE:
# - Sonoff iFan02 (ESP8266) (3-speed fan + 1x light relay)
#:########################################################################################:#
# OPERATION NOTES:
# - Fan and light can be controlled via MQTT (local topics below) and via Home Assistant API
# - Fan speed state is tracked in a restored global (speed_value)
# - On boot, the device publishes the restored speed_value to the MQTT speed/state topic for syncing
#:########################################################################################:#
# MQTT COMMANDS:
# Fan speed command:
#   ${mqtt_local_command_topic}/speed/set  ->  0,1,2,3,+,-
# Fan light command:
#   ${mqtt_local_command_topic}/light/set  ->  ON,OFF
#
# Fan speed status:
#   ${mqtt_local_status_topic}/speed/state ->  0,1,2,3
# Fan light status:
#   ${mqtt_local_status_topic}/light/state ->  ON,OFF
#:########################################################################################:#
# OFFLINE NOTES:
# a) Home Assistant OFFLINE, but Network and MQTT ONLINE
#    - Fan and light remain controllable via MQTT local command topics
#    - Status publishes to MQTT still occur (speed and light state)
#    - HA entities are unavailable until HA returns
# b) MQTT OFFLINE (but WiFi/Network and HA API ONLINE)
#    - Fan and light remain controllable from Home Assistant via API (fan/light/select)
#    - MQTT local commands will not work and status publishes will fail while MQTT is down
# c) Entire WiFi/Network OFFLINE
#    - No HA API and no MQTT control available
#    - If power remains on, relays keep their last driven state
#    - If the device reboots while offline, fan/light restore_mode defaults apply (RESTORE_DEFAULT_OFF)
#    - Accurate time is NOT needed (SNTP not required) for core 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-bedrm3ceilingfan"
  friendly_name: "Bedroom 3 Ceiling Fan"
  description_comment: "3 Speed Overhead Ceiling Fan Bedroom 3 :: Sonoff iFan02 (Layout V1.1)"
  device_area: "Bedroom 3" # Allows ESP device to be automatically linked to an 'Area' in Home Assistant.

  # Project Naming
  project_name: "Sonoff Technologies.Sonoff iFan02" # Project Details
  project_version: "v1.1" # Project V denotes release of yaml file, allowing checking of deployed vs latest version

  # Passwords and Secrets
  api_key: !secret esp-api_key
  ota_pass: !secret esp-ota_pass
  static_ip_address: !secret esp-bedrm3ceilingfan_ip # unfortunately you can't use substitutions inside secrets names
  mqtt_local_command_main_topic: !secret mqtt_command_main_topic
  mqtt_local_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" # Define logging level: NONE, ERROR, WARN, INFO, DEBUG (Default), VERBOSE, VERY_VERBOSE
  update_interval: "60s" # update time for general sensors etc

  # MQTT LOCAL Controls
  mqtt_device_name: "bedroom3-ceilingfan"
  mqtt_local_command_topic: "${mqtt_local_command_main_topic}/${mqtt_device_name}" # Topic used to command this locally without HA
  mqtt_local_status_topic: "${mqtt_local_status_main_topic}/${mqtt_device_name}" # Topic used to view status locally without HA

  # Switch/Relay Naming & Icons
  light_1_name: "Fan Light"
  switch_2_name: "Fan 2 Relay"
  switch_3_name: "Fan 3 Relay"
  switch_4_name: "Fan 4 Relay"


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

  #### WEB PORTAL ####
  #common_webportal: !include common/webportal_common.yaml

  #### 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
  area: "${device_area}"
  project:
    name: "${project_name}"
    version: "${project_version}"
  on_boot: # Publish restored speed on boot so other systems can sync
    priority: -100
    then:
      - delay: 200ms
      - mqtt.publish:
          topic: "${mqtt_local_status_topic}/speed/state"
          payload: !lambda "return to_string(id(speed_value));"


#:########################################################################################:#
# ESP PLATFORM AND FRAMEWORK:
# https://esphome.io/components/esp8266/
#:########################################################################################:#
esp8266:
  board: esp01_1m # Sonoff iFan02
  restore_from_flash: true # restore some values on reboot

preferences:
  flash_write_interval: 5min

mdns:
  disabled: true # Disabling will make the build file smaller (and it is still available via static IP)


#:########################################################################################:#
# GLOBAL VARIABLES:
# https://esphome.io/guides/automations.html#global-variables
#:########################################################################################:#
globals:
  - id: speed_value
    type: int
    restore_value: true
    initial_value: "0"


#:########################################################################################:#
# LOGGING: ESPHome Logging Enable
# https://esphome.io/components/logger.html
#:########################################################################################:#
logger:
  level: "${log_level}" # INFO suggested, or DEBUG for testing
  baud_rate: 0 # set to 0 for no logging via UART, needed if you are using it for other serial things


#:########################################################################################:#
# MQTT:
# This adds device-specific MQTT command triggers to the common MQTT configuration.
#:########################################################################################:#
mqtt:
  on_message:
    # Light control
    - topic: "${mqtt_local_command_topic}/light/set"
      payload: "ON"
      then:
        - light.turn_on: ifan02_light

    - topic: "${mqtt_local_command_topic}/light/set"
      payload: "OFF"
      then:
        - light.turn_off: ifan02_light

    # Fan speed control (0-3) plus ramp up/down (+ / -)
    - topic: "${mqtt_local_command_topic}/speed/set"
      then:
        # 1) Compute new speed_value based on payload
        - lambda: |-
            std::string s = x;
            int val = id(speed_value);
            if (s == "+") {
              val++;
            } else if (s == "-") {
              val--;
            } else if (s.size() && isdigit(s[0])) {
              val = atoi(s.c_str());
            } else {
              ESP_LOGE("ifan02", "Invalid speed payload: '%s'", x.c_str());
              return;
            }
            // Clamp between 0 and 3
            if (val < 0) val = 0;
            if (val > 3) val = 3;
            id(speed_value) = val;

        # 2) Drive the fan based on the new value
        - if:
            condition:
              lambda: "return id(speed_value) == 0;"
            then:
              - fan.turn_off: ifan02_fan
            else:
              - fan.turn_on:
                  id: ifan02_fan
                  speed: !lambda "return id(speed_value);"


#:########################################################################################:#
# SWITCH COMPONENT:
# https://esphome.io/components/switch/
#
# Sonoff iFan02 has relays, but they do not need to be shown in HA because
# the speed of the fan is the important part (and combinations of the relays give that)
#:########################################################################################:#
switch:
  - platform: gpio
    name: "${switch_3_name}"
    pin: GPIO4
    id: fan3sw
    restore_mode: RESTORE_DEFAULT_OFF
    internal: true

  - platform: gpio
    name: "${switch_2_name}"
    pin: GPIO5
    id: fan2sw
    restore_mode: RESTORE_DEFAULT_OFF
    internal: true

  - platform: gpio
    name: "${switch_4_name}"
    pin: GPIO15
    id: fan4sw
    restore_mode: RESTORE_DEFAULT_OFF
    internal: true


#:########################################################################################:#
# SELECT COMPONENT:
# https://esphome.io/components/select/index.html
#:########################################################################################:#
select:
  - platform: template
    name: "Bedroom 3 Fan Speed"
    id: bedroom3_fan_speed_select
    options:
      - "Off"
      - "Low"
      - "Medium"
      - "High"
    update_interval: 1s
    lambda: |-
      switch (id(speed_value)) {
        case 1:  return esphome::optional<std::string>("Low");
        case 2:  return esphome::optional<std::string>("Medium");
        case 3:  return esphome::optional<std::string>("High");
        default: return esphome::optional<std::string>("Off");
      }
    set_action:
      # OFF
      - if:
          condition:
            lambda: 'return x == "Off";'
          then:
            - lambda: |-
                id(speed_value) = 0;
            - fan.turn_off: ifan02_fan

      # LOW (1)
      - if:
          condition:
            lambda: 'return x == "Low";'
          then:
            - lambda: |-
                id(speed_value) = 1;
            - fan.turn_on:
                id: ifan02_fan
                speed: 1

      # MEDIUM (2)
      - if:
          condition:
            lambda: 'return x == "Medium";'
          then:
            - lambda: |-
                id(speed_value) = 2;
            - fan.turn_on:
                id: ifan02_fan
                speed: 2

      # HIGH (3)
      - if:
          condition:
            lambda: 'return x == "High";'
          then:
            - lambda: |-
                id(speed_value) = 3;
            - fan.turn_on:
                id: ifan02_fan
                speed: 3


#:########################################################################################:#
# OUTPUT COMPONENT:
# https://esphome.io/components/output/
#
# Template output drives the real relays when the fan output state changes
#:########################################################################################:#
output:
  - platform: gpio
    pin: GPIO12
    id: lightrelay

  - platform: template
    type: float
    id: fan_decode
    write_action:
      - lambda: |-
          if (state < 0.25) {
            id(fan2sw).turn_off();
            id(fan3sw).turn_off();
            id(fan4sw).turn_off();
          }
          else if (state < 0.5) {
            id(fan2sw).turn_on();
            id(fan3sw).turn_off();
            id(fan4sw).turn_off();
          }
          else if (state < 0.75) {
            id(fan2sw).turn_on();
            id(fan3sw).turn_on();
            id(fan4sw).turn_off();
          }
          else {
            id(fan2sw).turn_on();
            id(fan3sw).turn_off();
            id(fan4sw).turn_on();
          }


#:########################################################################################:#
# LIGHT COMPONENT:
# https://esphome.io/components/light/
#:########################################################################################:#
light:
  - platform: binary
    name: "${light_1_name}"
    output: lightrelay
    restore_mode: RESTORE_DEFAULT_OFF
    id: ifan02_light
    # Publish status updates when the light turns on/off
    on_turn_on:
      - mqtt.publish:
          topic: "${mqtt_local_status_topic}/light/state"
          payload: "ON"
    on_turn_off:
      - mqtt.publish:
          topic: "${mqtt_local_status_topic}/light/state"
          payload: "OFF"


#:########################################################################################:#
# FAN COMPONENT:
# https://esphome.io/components/fan/index.html
#:########################################################################################:#
fan:
  - platform: speed
    output: fan_decode
    name: "Fan"
    id: ifan02_fan
    speed_count: 3
    restore_mode: RESTORE_DEFAULT_OFF

    # Whenever you explicitly set a speed (1-3)
    on_speed_set:
      - lambda: |-
          id(speed_value) = x;
      - mqtt.publish:
          topic: "${mqtt_local_status_topic}/speed/state"
          payload: !lambda "return to_string(id(speed_value));"

    # Whenever the fan goes fully off
    on_turn_off:
      - lambda: |-
          id(speed_value) = 0;
      - mqtt.publish:
          topic: "${mqtt_local_status_topic}/speed/state"
          payload: "0"

    # Whenever the fan goes off->on (eg via HA fan switch)
    on_turn_on:
      - lambda: |-
          // bump 0->1 if we were off
          if (id(speed_value) == 0) id(speed_value) = 1;
      - fan.turn_on:
          id: ifan02_fan
          speed: !lambda "return id(speed_value);"
      - mqtt.publish:
          topic: "${mqtt_local_status_topic}/speed/state"
          payload: !lambda "return to_string(id(speed_value));"