#:########################################################################################:# # TITLE: OFFICE DUAL LIGHT CONTROL # zorruno.com layout v1.1 2026 #:########################################################################################:# # REPO: # https://home.fox.co.nz/gitea/zorruno/zorruno-homeassistant/src/branch/master/esphome/esp-officeduallights.yaml #:########################################################################################:# # VERSIONS: # V2.7 2026-04-24 Updated MQTT local topics, retain false, and timed control using seconds # V2.6 2026-04-23 Added independent MQTT timed ON control for relay1 and relay2 # V2.5 2026-03-11 Updated yaml to zorruno layout V1.1 # V2.4 2025-06-30 Tidied up MQTT direct relay control (esps can control each other via MQTT) # V2.3 2025-06-18 Added MQTT direct relay control #:########################################################################################:# # HARDWARE: # - Sonoff Dual R1 # - ESP8266 based # - 2 x relay outputs # - 2 x local push buttons # - Relay control via UART serial commands # - Status LED on GPIO13 #:########################################################################################:# # OPERATION NOTES: # - Controls 2 office light circuits: # - Overhead cool white lights # - Right hand warm bunker light # - Local button 1 toggles relay/light 1 and cancels any active timer for relay 1 # - Local button 2 toggles relay/light 2 and cancels any active timer for relay 2 # - Relay state changes are sent out via MQTT status topics with retain false # - MQTT can command each relay with: # - On = turn on with no timer and cancel any active timer # - Off = turn off immediately and cancel any active timer # - 1 to 3600 = turn on for that many seconds, then turn off # - If a new valid timed value is received while a timer is already active, the timer restarts from zero # - Invalid MQTT timer values are ignored # - Template binary lights drive the template relay switches # - Sonoff Dual R1 requires UART serial packets to operate the physical relays # # MQTT COMMANDS: # - Command relay 1: # - ${mqtt_local_command1_topic} On, Off, or 1 to 3600 seconds # - Command relay 2: # - ${mqtt_local_command2_topic} On, Off, or 1 to 3600 seconds # - Relay 1 status publishes to: # - ${mqtt_local_status1_topic} On or Off # - Relay 2 status publishes to: # - ${mqtt_local_status2_topic} On or Off #:########################################################################################:# # OFFLINE NOTES: # a) Home Assistant offline (network and MQTT still online): # - Device continues to operate from local buttons # - Device can still be controlled directly by MQTT # - Relay status still publishes by MQTT if broker is available # # b) MQTT offline (or HA and MQTT both offline): # - Device continues to operate from local buttons only # - No direct inter-ESP MQTT control available # - No MQTT state feedback available # # c) Entire WiFi / Network offline: # - Device continues to operate from local buttons only # - No HA control or MQTT control/status available # - Accurate time is not needed; SNTP is not required for operation #:########################################################################################:# #:########################################################################################:# # SUBSTITUTIONS: # Specific device variable substitutions #:########################################################################################:# substitutions: # Device Naming device_name: "esp-officeduallights" friendly_name: "Office Dual Lights" description_comment: "Dual Office Lights (Overhead and right hand Bunker) :: Sonoff Dual R1 (Layout V1.1)" device_area: "Office" # Project Naming project_name: "Sonoff Technologies.Sonoff Dual R1" project_version: "v2.7" # Passwords and Secrets api_key: !secret esp-api_key ota_pass: !secret esp-ota_pass static_ip_address: !secret esp-officeduallights_ip current_ip_address: ${static_ip_address} mqtt_command_main_topic: !secret mqtt_command_main_topic mqtt_status_main_topic: !secret mqtt_status_main_topic # Device Settings relay_icon: "mdi:lightbulb-group" log_level: "NONE" update_interval: "60s" # MQTT LOCAL Controls mqtt_local_device1_name: "office-dual-lights-right" mqtt_local_command1_topic: "${mqtt_command_main_topic}/${mqtt_local_device1_name}" mqtt_local_status1_topic: "${mqtt_status_main_topic}/${mqtt_local_device1_name}" mqtt_local_device2_name: "office-dual-lights-left" mqtt_local_command2_topic: "${mqtt_command_main_topic}/${mqtt_local_device2_name}" mqtt_local_status2_topic: "${mqtt_status_main_topic}/${mqtt_local_device2_name}" # MQTT REMOTE Controls # mqtt_remote_device1_name: "masterbath-towelrail" # mqtt_remote_device1_command_topic: "${mqtt_command_main_topic}/${mqtt_remote_device1_name}/operation" # mqtt_remote_device1_command1: "BOOST" # Switch Naming switch_1_name: "Dual L1 Relay" relay_1_icon: "mdi:lightbulb" light_1_name: "Light 1 (Right Hand Bunker Light)" switch_2_name: "Dual L2 Relay" relay_2_icon: "mdi:lightbulb" light_2_name: "Light 2 (Cool White Overhead Lights)" #:########################################################################################:# # PACKAGES: # Included common packages # https://esphome.io/components/packages.html #:########################################################################################:# packages: common_wifi: !include file: common/network_common.yaml vars: local_device_name: "${device_name}" local_static_ip_address: "${static_ip_address}" local_current_ip_address: "${current_ip_address}" local_ota_pass: "${ota_pass}" common_api: !include file: common/api_common.yaml # file: common/api_common_noencryption.yaml vars: local_api_key: "${api_key}" common_mqtt: !include file: common/mqtt_common.yaml vars: local_device_name: "${device_name}" # common_webportal: !include common/webportal_common.yaml # common_sntp: !include common/sntp_common.yaml 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}" #:########################################################################################:# # ESP PLATFORM AND FRAMEWORK: # https://esphome.io/components/esp8266.html #:########################################################################################:# esp8266: board: esp01_1m restore_from_flash: false preferences: flash_write_interval: 5min mdns: disabled: true #:########################################################################################:# # LOGGER: # Sonoff Dual cannot use UART logging because UART is required for relay control # https://esphome.io/components/logger.html #:########################################################################################:# logger: level: "${log_level}" baud_rate: 0 #:########################################################################################:# # UART BUS: # https://esphome.io/components/uart.html #:########################################################################################:# uart: tx_pin: GPIO01 rx_pin: GPIO03 baud_rate: 19200 #:########################################################################################:# # MQTT COMPONENT: # Device-specific MQTT command triggers # https://esphome.io/components/mqtt.html #:########################################################################################:# mqtt: on_message: - topic: "${mqtt_local_command1_topic}" payload: "On" then: - script.stop: relay_1_timer_script - light.turn_on: light_1 - topic: "${mqtt_local_command1_topic}" payload: "Off" then: - script.stop: relay_1_timer_script - light.turn_off: light_1 - topic: "${mqtt_local_command1_topic}" then: - lambda: |- if (x.empty()) { return; } bool numeric_payload = true; for (char c : x) { if (c < '0' || c > '9') { numeric_payload = false; break; } } if (!numeric_payload) { return; } const int timer_seconds = atoi(x.c_str()); if (timer_seconds < 1 || timer_seconds > 3600) { return; } id(relay_1_timer_script)->execute(timer_seconds * 1000); - topic: "${mqtt_local_command2_topic}" payload: "On" then: - script.stop: relay_2_timer_script - light.turn_on: light_2 - topic: "${mqtt_local_command2_topic}" payload: "Off" then: - script.stop: relay_2_timer_script - light.turn_off: light_2 - topic: "${mqtt_local_command2_topic}" then: - lambda: |- if (x.empty()) { return; } bool numeric_payload = true; for (char c : x) { if (c < '0' || c > '9') { numeric_payload = false; break; } } if (!numeric_payload) { return; } const int timer_seconds = atoi(x.c_str()); if (timer_seconds < 1 || timer_seconds > 3600) { return; } id(relay_2_timer_script)->execute(timer_seconds * 1000); #:########################################################################################:# # SCRIPT COMPONENT: # Independent per-relay timer scripts # mode: restart means a new valid timer value restarts the timer from zero # https://esphome.io/components/script.html #:########################################################################################:# script: - id: relay_1_timer_script mode: restart parameters: timer_duration_ms: int then: - light.turn_on: light_1 - delay: !lambda "return timer_duration_ms;" - light.turn_off: light_1 - id: relay_2_timer_script mode: restart parameters: timer_duration_ms: int then: - light.turn_on: light_2 - delay: !lambda "return timer_duration_ms;" - light.turn_off: light_2 #:########################################################################################:# # STATUS LED COMPONENT: # Sonoff Dual LED is on GPIO13 # https://esphome.io/components/status_led.html #:########################################################################################:# status_led: pin: number: GPIO13 inverted: true #:########################################################################################:# # BINARY SENSOR COMPONENT: # Sonoff Dual R1 buttons are GPIO04 and GPIO14 # Local button presses cancel any active timer for that relay before toggling # https://esphome.io/components/binary_sensor/ #:########################################################################################:# binary_sensor: - platform: gpio pin: number: GPIO4 mode: INPUT_PULLUP inverted: true id: button_1 on_press: - script.stop: relay_1_timer_script - light.toggle: light_1 - platform: gpio pin: number: GPIO14 mode: INPUT_PULLUP inverted: true id: button_2 on_press: - script.stop: relay_2_timer_script - light.toggle: light_2 #:########################################################################################:# # SWITCH COMPONENT: # Sonoff Dual R1 requires serial data packets to switch the relays # Relay state changes publish MQTT status updates with retain false # https://esphome.io/components/switch/ #:########################################################################################:# switch: - platform: template name: "${switch_1_name}" id: relay_1 optimistic: true icon: "${relay_1_icon}" internal: true turn_on_action: - mqtt.publish: topic: "${mqtt_local_status1_topic}" payload: "On" retain: false - if: condition: switch.is_off: relay_2 then: - uart.write: [0xA0, 0x04, 0x01, 0xA1] else: - uart.write: [0xA0, 0x04, 0x03, 0xA1] turn_off_action: - mqtt.publish: topic: "${mqtt_local_status1_topic}" payload: "Off" retain: false - if: condition: switch.is_off: relay_2 then: - uart.write: [0xA0, 0x04, 0x00, 0xA1] else: - uart.write: [0xA0, 0x04, 0x02, 0xA1] - platform: template name: "${switch_2_name}" id: relay_2 optimistic: true icon: "${relay_2_icon}" internal: true turn_on_action: - mqtt.publish: topic: "${mqtt_local_status2_topic}" payload: "On" retain: false - if: condition: switch.is_off: relay_1 then: - uart.write: [0xA0, 0x04, 0x02, 0xA1] else: - uart.write: [0xA0, 0x04, 0x03, 0xA1] turn_off_action: - mqtt.publish: topic: "${mqtt_local_status2_topic}" payload: "Off" retain: false - if: condition: switch.is_off: relay_1 then: - uart.write: [0xA0, 0x04, 0x00, 0xA1] else: - uart.write: [0xA0, 0x04, 0x01, 0xA1] #:########################################################################################:# # OUTPUT COMPONENT: # Template outputs drive the relay template switches when the light state changes # https://esphome.io/components/output/ #:########################################################################################:# output: - platform: template id: light_output_1 type: binary write_action: - lambda: |- if (state) { id(relay_1).turn_on(); } else { id(relay_1).turn_off(); } - platform: template id: light_output_2 type: binary write_action: - lambda: |- if (state) { id(relay_2).turn_on(); } else { id(relay_2).turn_off(); } #:########################################################################################:# # LIGHT COMPONENT: # Main control entities for both local and MQTT control paths # https://esphome.io/components/light/ #:########################################################################################:# light: - platform: binary name: "${light_1_name}" id: light_1 output: light_output_1 - platform: binary name: "${light_2_name}" id: light_2 output: light_output_2