# ESPHome Firmware for # Sonoff Switchman M5 1-Gang US # Copyright (c) 2024 Mario Di Vece # License: [MIT](https://opensource.org/license/mit/) # Description: # Aims to provide a feature-rich, production-ready firmware for this elegant device # - Provides Diagnostic data plus, status LED indicator when not connected to Home Assistant API # - Relays may be configured individually on the UI to work in decoupled mode. # LEDs are physically connected to the relays, and they can't be individually controlled :( # - Exposes gestures via events: # esphome.on_gesture { button: (A), gesture: (click|double_click|button_hold) } # - Off-state (Background Brightness) of the LEDs is configurable via the UI # # For the below example, you need to keep the following entries in your secrets.yaml file: # - wifi_ssid: "" # - wifi_password: "" # - ota_password: "" # - api_key: "<32-byte-base-64-secret>" substitutions: # User-required substitutions device_site: "home" device_location_code: "bedroom01" device_location_name: "Bedroom 1" device_type_code: "wasw" device_type_name: "Wall Switch" device_number: "044" device_ip: "0.0.0.0" # Project Substitutions device_model: "Switchman M5 (1-Gang)" device_make: "Sonoff" package_version: "2024.2.13" package_url: "https://gitea.domowyasystent.com/PeakControl/devices/raw/branch/main/sonoff-m5-1g-us.yaml" # Generated device name/code device_name: "${device_location_name} - ${device_type_name} - ${device_number}" device_code: "${device_site}-${device_location_code}-${device_type_code}-${device_number}" # WiFi / API secrets api_key: !secret api_key wifi_ssid: !secret wifi_ssid wifi_password: !secret wifi_password ota_password: !secret ota_password # Relay GPIO relay_a_gpio: GPIO23 # Button GPIO button_a_gpio: GPIO00 # Indicator LED GPIO led_indicator_gpio: GPIO19 # Diagnostics log_level: INFO timezone: "America/Mexico_City" # Button timing configurations filter_delay_on: 50ms filter_delay_off: 50ms timing_click_1: ON for at most 400ms timing_click_2: OFF for at least 600ms timing_double_click_1: ON for at most 500ms timing_double_click_2: OFF for at most 400ms timing_double_click_3: ON for at most 500ms timing_double_click_4: OFF for at least 250ms timing_hold: ON for at least 1s timing_hold_repeat: 100ms timing_pulse: 250ms # ── Board ────────────────────────────────────────────────────────────────────── esp32: board: esp32dev framework: type: arduino version: recommended # ── ESPHome / Project ────────────────────────────────────────────────────────── globals: - id: cpu_speed type: int restore_value: no initial_value: "0" esphome: name: "${device_code}" friendly_name: "${device_name}" comment: "${device_model} by ${device_make}" name_add_mac_suffix: false min_version: "2023.2.0" project: name: "${device_make}.${device_model}" version: "${package_version}" on_boot: - priority: 900.0 then: - lambda: |- id(cpu_speed) = ESP.getCpuFreqMHz(); # ── Connectivity ─────────────────────────────────────────────────────────────── api: reboot_timeout: 0s encryption: key: "${api_key}" ota: - platform: esphome password: "${ota_password}" safe_mode: wifi: fast_connect: false power_save_mode: light ssid: "${wifi_ssid}" password: "${wifi_password}" use_address: "${device_ip}" ap: ssid: "${device_code}-setup" password: "${wifi_password}" ap_timeout: 5min captive_portal: # ── Logging / Time ───────────────────────────────────────────────────────────── logger: level: "${log_level}" baud_rate: 0 time: - platform: sntp id: time_service timezone: ${timezone} update_interval: 15min servers: - 0.pool.ntp.org - 1.pool.ntp.org - 2.pool.ntp.org # ── Outputs ──────────────────────────────────────────────────────────────────── output: - platform: ledc id: pwm_output pin: GPIO18 frequency: 1000 Hz # ── Lights ───────────────────────────────────────────────────────────────────── light: - platform: status_led name: "LED" id: led_status pin: number: GPIO05 inverted: true ignore_strapping_warning: true internal: true restore_mode: RESTORE_DEFAULT_ON - platform: monochromatic output: pwm_output name: "Background Brightness" restore_mode: RESTORE_DEFAULT_OFF icon: 'mdi:led-outline' entity_category: 'config' # ── Switches ─────────────────────────────────────────────────────────────────── switch: - platform: gpio name: "Relay A" pin: ${relay_a_gpio} id: relay_a restore_mode: RESTORE_DEFAULT_OFF on_turn_on: - switch.turn_on: led_indicator on_turn_off: - switch.turn_off: led_indicator - platform: gpio id: led_indicator internal: true pin: ${led_indicator_gpio} # ── Selects ──────────────────────────────────────────────────────────────────── select: - platform: template name: "Mode - Button A" id: mode_a icon: 'mdi:link-box-outline' entity_category: 'config' options: - "Latching" # 0 - "Momentary" # 1 - "Pulse" # 2 - "Decoupled" # 3 initial_option: "Latching" restore_value: true optimistic: true set_action: - switch.turn_off: relay_a # ── Binary Sensors ───────────────────────────────────────────────────────────── binary_sensor: - platform: status icon: 'mdi:home-assistant' name: "API Status" id: sensor_status disabled_by_default: true - platform: template name: "API connected" id: sensor_api_connected internal: true entity_category: 'diagnostic' device_class: 'connectivity' lambda: return global_api_server->is_connected(); on_press: - light.turn_off: led_status on_release: - light.turn_on: led_status - platform: gpio name: "Button A" id: button_a pin: number: ${button_a_gpio} mode: INPUT_PULLUP inverted: true ignore_strapping_warning: true filters: - delayed_on: ${filter_delay_on} - delayed_off: ${filter_delay_off} on_press: # Latching - if: condition: - lambda: 'return id(mode_a).active_index() == 0;' then: - switch.toggle: relay_a # Momentary - if: condition: - lambda: 'return id(mode_a).active_index() == 1;' then: - switch.turn_on: relay_a # Pulse - if: condition: - lambda: 'return id(mode_a).active_index() == 2;' then: - switch.turn_on: relay_a - delay: ${timing_pulse} - switch.turn_off: relay_a # Decoupled - if: condition: - lambda: 'return id(mode_a).active_index() == 3;' then: - switch.turn_on: led_indicator on_release: # Momentary - if: condition: - lambda: 'return id(mode_a).active_index() == 1;' then: - switch.turn_off: relay_a # Decoupled - if: condition: - lambda: 'return id(mode_a).active_index() == 3;' then: - switch.turn_off: led_indicator on_multi_click: - timing: - ${timing_click_1} - ${timing_click_2} then: - homeassistant.event: event: esphome.on_gesture data: button: A gesture: single_click - timing: - ${timing_double_click_1} - ${timing_double_click_2} - ${timing_double_click_3} - ${timing_double_click_4} then: - homeassistant.event: event: esphome.on_gesture data: button: A gesture: double_click - timing: - ${timing_hold} then: - while: condition: binary_sensor.is_on: button_a then: - light.toggle: led_status - homeassistant.event: event: esphome.on_gesture data: button: A gesture: button_hold - delay: ${timing_hold_repeat} - light.turn_off: led_status # ── Sensors ──────────────────────────────────────────────────────────────────── sensor: - platform: wifi_signal name: "RSSI" id: sensor_rssi icon: 'mdi:signal' update_interval: 60s entity_category: "diagnostic" - platform: internal_temperature name: "Internal Temperature" disabled_by_default: true icon: mdi:heat-wave - platform: template name: "CPU Frequency" icon: "mdi:speedometer" accuracy_decimals: 0 unit_of_measurement: Mhz disabled_by_default: true lambda: |- return (id(cpu_speed)); entity_category: diagnostic - platform: template id: esp_memory icon: mdi:memory name: Free Memory lambda: return heap_caps_get_free_size(MALLOC_CAP_INTERNAL) / 1024; unit_of_measurement: "kB" state_class: measurement entity_category: "diagnostic" disabled_by_default: true - platform: uptime name: "Uptime" id: sensor_uptime update_interval: 60s entity_category: "diagnostic" internal: true on_raw_value: then: - text_sensor.template.publish: id: uptime_human state: !lambda |- int seconds = round(id(sensor_uptime).raw_state); int days = seconds / (24 * 3600); seconds = seconds % (24 * 3600); int hours = seconds / 3600; seconds = seconds % 3600; int minutes = seconds / 60; seconds = seconds % 60; return ( (days ? String(days) + "d " : "") + (hours ? String(hours) + "h " : "") + (minutes ? String(minutes) + "m " : "") + (String(seconds) + "s") ).c_str(); # ── Text Sensors ─────────────────────────────────────────────────────────────── text_sensor: - platform: wifi_info ip_address: id: ip_address name: "IP Address" icon: "mdi:wan" - platform: template name: "Uptime" id: uptime_human icon: "mdi:timer-check-outline" update_interval: 60s entity_category: "diagnostic" disabled_by_default: true - platform: template name: "Deployment Version" lambda: return {"${package_version}"}; disabled_by_default: true icon: "mdi:tag" entity_category: diagnostic # ── Buttons ──────────────────────────────────────────────────────────────────── button: - platform: restart name: "Reboot Device" id: button_restart icon: mdi:power-cycle entity_category: "diagnostic" - platform: factory_reset disabled_by_default: false name: "Load Factory Settings" id: factory_reset_all icon: mdi:factory