#:########################################################################################:# # TITLE: ENTRANCE BATHROOM LIGHTSWITCH # zorruno.com layout v1.1 2026 #:########################################################################################:# # REPO: # https://home.fox.co.nz/gitea/zorruno/zorruno-homeassistant/src/branch/master/esphome/esp-entrancebathrmlights.yaml #:########################################################################################:# # VERSIONS: # V4.1 2026-06-27 Changed fan timer back to MM:SS with 1 second countdown and 00:00 when fan is off # V4.0 2026-06-27 Added extract fan timer enable switch and countdown sensor # V3.9 2026-03-11 Updated yaml to zorruno layout V1.1 # V3.8 2026-02-25 Updated yaml to zorruno layout V1.1 # V3.7 2025-09-34 Upload to this bathroom (as noted in original header) # V3.5 2025-07-24 YAML tidyups #:########################################################################################:# # HARDWARE: # - Zemismart KS-811 Double push button (ESP8266) # - ESP8266 board: esp01_1m # - Pinout/schematic: # https://community.home-assistant.io/t/zemismart-ks-811-working-with-esphome/ #:########################################################################################:# # OPERATION NOTES: # - Entrance bathroom switch with 2 outputs: # 1) Main Lights # 2) Extract Fan # # - Optional extract fan timer: # - Controlled by the "Fan Timer Enabled" switch. # - When enabled, if the light is OFF and the fan is ON, the fan will run for the # configured fan_timer_duration_minutes, then turn OFF. # - Countdown starts when the light turns OFF while the fan is ON. # - Countdown also starts when the fan turns ON while the light is already OFF. # - Countdown is cancelled/reset if the light turns ON or the fan turns OFF manually. # - The timer remaining sensor shows MM:SS while counting down. # - When the fan is OFF, the timer remaining sensor shows 00:00. #:########################################################################################:# # OFFLINE NOTES: # a) Home Assistant OFFLINE, but Network and MQTT ONLINE # - Local button control continues to toggle relays. # - Fan timer logic continues locally on the ESP device. # - MQTT/API publishing to HA may not be available until HA returns. # b) MQTT OFFLINE (or broker unreachable) # - Local button control continues to toggle relays. # - Fan timer logic continues locally on the ESP device. # - MQTT state updates (if any) will not publish. # c) Entire WiFi/Network OFFLINE # - Local button control continues to toggle relays. # - Fan timer logic continues locally on the ESP device. # - 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-entrancebathrmlights" friendly_name: "Entrance Bathroom Lightswitch (2)" description_comment: "Entrance bathroom lightswitch using a Zemismart KS-811 Double Push Button. Main Lights (1), Extract Fan (2), Fan Timer (Layout V1.1)" device_area: "Entranceway" # Allows ESP device to be automatically linked to an 'Area' in Home Assistant. # Project Naming project_name: "Zemismart Technologies.KS-811 Double" project_version: "v4.1" # Passwords & Secrets 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-entrancebathrmlights_ip # 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 (Default), VERBOSE, VERY_VERBOSE update_interval: "60s" # update time for general sensors etc # Switch Naming switch_1_name: "Main Lights" switch_2_name: "Extract Fan" # Fan Timer Settings fan_timer_duration_minutes: "10" #:########################################################################################:# # 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}" #:########################################################################################:# # ESP PLATFORM AND FRAMEWORK # https://esphome.io/components/esp8266.html #:########################################################################################:# esp8266: board: esp01_1m early_pin_init: false # Initialise pins early to known values. Recommended false where switches are involved. Defaults to true. board_flash_mode: dout # Default is dout #:########################################################################################:# # LOGGING # https://esphome.io/components/logger.html #:########################################################################################:# logger: level: "${log_level}" #baud_rate: 0 # set to 0 for no logging via UART, needed if you are using it for other serial things (eg PZEM) #esp8266_store_log_strings_in_flash: false #tx_buffer_size: 64 #:########################################################################################:# # STATUS LED # https://esphome.io/components/status_led.html #:########################################################################################:# status_led: pin: number: GPIO2 inverted: true #:########################################################################################:# # GLOBALS # https://esphome.io/components/globals.html #:########################################################################################:# globals: - id: fan_timer_counting_down type: bool restore_value: false initial_value: "false" - id: fan_timer_remaining_seconds type: int restore_value: false initial_value: "0" #:########################################################################################:# # BINARY SENSORS # https://esphome.io/components/binary_sensor/ #:########################################################################################:# binary_sensor: - platform: gpio pin: number: GPIO16 mode: INPUT inverted: true use_interrupt: false # GPIO16 pin doesn't support interrupts so use polling. Only suppresses a warning. name: "Button 1: ${switch_1_name}" on_press: - switch.toggle: Relay_1 - platform: gpio pin: number: GPIO5 mode: INPUT inverted: true name: "Button 2: ${switch_2_name}" on_press: - switch.toggle: Relay_2 #:########################################################################################:# # SWITCH # https://esphome.io/components/switch/ #:########################################################################################:# switch: - platform: template name: "Fan Timer Enabled" id: fan_timer_enabled icon: "mdi:fan-clock" optimistic: true restore_mode: RESTORE_DEFAULT_ON entity_category: config turn_on_action: - script.execute: fan_timer_evaluate turn_off_action: - script.execute: fan_timer_cancel - platform: gpio name: "Relay 1: ${switch_1_name}" pin: GPIO13 id: Relay_1 on_turn_on: - script.execute: fan_timer_evaluate on_turn_off: - script.execute: fan_timer_evaluate - platform: gpio name: "Relay 2: ${switch_2_name}" pin: GPIO12 id: Relay_2 on_turn_on: - script.execute: fan_timer_evaluate on_turn_off: - script.execute: fan_timer_evaluate #:########################################################################################:# # TEXT SENSORS # https://esphome.io/components/text_sensor/ #:########################################################################################:# text_sensor: - platform: template name: "Fan Timer Remaining" id: fan_timer_remaining_text icon: "mdi:timer-outline" update_interval: 1s lambda: |- int seconds = 0; if (id(Relay_2).state && id(fan_timer_counting_down)) { seconds = id(fan_timer_remaining_seconds); } else { seconds = 0; } if (seconds < 0) { seconds = 0; } int mins = seconds / 60; int secs = seconds % 60; char buffer[8]; snprintf(buffer, sizeof(buffer), "%02d:%02d", mins, secs); return std::string(buffer); #:########################################################################################:# # SCRIPTS # https://esphome.io/components/script.html #:########################################################################################:# script: - id: fan_timer_cancel mode: restart then: - lambda: |- id(fan_timer_counting_down) = false; id(fan_timer_remaining_seconds) = 0; - component.update: fan_timer_remaining_text - id: fan_timer_evaluate mode: restart then: - if: condition: lambda: |- return id(fan_timer_enabled).state && !id(Relay_1).state && id(Relay_2).state; then: - lambda: |- id(fan_timer_remaining_seconds) = ${fan_timer_duration_minutes} * 60; id(fan_timer_counting_down) = true; - component.update: fan_timer_remaining_text else: - script.execute: fan_timer_cancel #:########################################################################################:# # INTERVALS # https://esphome.io/components/interval.html #:########################################################################################:# interval: - interval: 1s then: - if: condition: lambda: |- return id(fan_timer_counting_down); then: - if: condition: lambda: |- return id(fan_timer_enabled).state && !id(Relay_1).state && id(Relay_2).state; then: - lambda: |- if (id(fan_timer_remaining_seconds) > 0) { id(fan_timer_remaining_seconds) -= 1; } - component.update: fan_timer_remaining_text - if: condition: lambda: |- return id(fan_timer_remaining_seconds) <= 0; then: - lambda: |- id(fan_timer_counting_down) = false; id(fan_timer_remaining_seconds) = 0; - component.update: fan_timer_remaining_text - switch.turn_off: Relay_2 else: - script.execute: fan_timer_cancel