From 1e1d0571d96bf7f8a84701d73655a4800e7ba724 Mon Sep 17 00:00:00 2001 From: root Date: Sat, 20 Sep 2025 15:18:46 +1200 Subject: [PATCH] esphome device updates --- esphome/common/api_common.yaml | 4 +- .../common/include_basic_diag_sensors.yaml | 64 + .../common/include_basic_reset_buttons.yaml | 64 + .../common/include_debug_diag_sensors.yaml | 42 + esphome/common/include_more_diag_sensors.yaml | 54 + .../include_resetcount_diag_sensors.yaml | 183 +++ esphome/common/mqtt_common.yaml | 1 + esphome/common/network_common.yaml | 78 +- esphome/common/network_common_dual.yaml | 126 -- esphome/common/network_common_fallback.yaml | 81 -- esphome/common/secrets copy.yaml | 1 - esphome/common/sensors_common.yaml | 159 ++- esphome/common/sensors_common_lite.yaml | 12 + esphome/common/sntp_common.yaml | 14 +- esphome/esp-3dprinterpow.yaml | 112 +- ...{esp-attobat.yaml => esp-attobat.yaml.old} | 1 + esphome/esp-attobattery.yaml | 180 +++ esphome/esp-bedrm2ceilingfan.yaml | 58 +- esphome/esp-breakfastbarleds.yaml | 454 +++++--- esphome/esp-downstairskitchleds.yaml | 387 ++++--- esphome/esp-entmulti.yaml | 38 +- esphome/esp-entrancelightswitch.yaml | 181 +++ esphome/esp-laundrylights.yaml | 145 ++- ...xdimmer1.yaml => esp-lounge6chdimmer.yaml} | 221 +++- esphome/esp-loungebookshelfleds.yaml | 1028 +++++++++++++++++ ...tleds.yaml => esp-loungecabinetleds2.yaml} | 460 +++++--- esphome/esp-mainkitchenlights.yaml | 181 +++ esphome/esp-occupancyoffice.yaml | 20 +- esphome/esp-poollightpower.yaml | 32 +- esphome/esp-poolpumppower.yaml | 34 +- esphome/esp-weatherstation.temp | 521 ++++----- packages/simulation_lights.yaml | 8 +- 32 files changed, 3756 insertions(+), 1188 deletions(-) create mode 100644 esphome/common/include_basic_diag_sensors.yaml create mode 100644 esphome/common/include_basic_reset_buttons.yaml create mode 100644 esphome/common/include_debug_diag_sensors.yaml create mode 100644 esphome/common/include_more_diag_sensors.yaml create mode 100644 esphome/common/include_resetcount_diag_sensors.yaml delete mode 100644 esphome/common/network_common_dual.yaml delete mode 100644 esphome/common/network_common_fallback.yaml delete mode 100644 esphome/common/secrets copy.yaml rename esphome/{esp-attobat.yaml => esp-attobat.yaml.old} (98%) create mode 100644 esphome/esp-attobattery.yaml create mode 100644 esphome/esp-entrancelightswitch.yaml rename esphome/{esp-4chdmxdimmer1.yaml => esp-lounge6chdimmer.yaml} (66%) create mode 100644 esphome/esp-loungebookshelfleds.yaml rename esphome/{esp-loungecabinetleds.yaml => esp-loungecabinetleds2.yaml} (66%) create mode 100644 esphome/esp-mainkitchenlights.yaml diff --git a/esphome/common/api_common.yaml b/esphome/common/api_common.yaml index 26ba038..3a7e705 100644 --- a/esphome/common/api_common.yaml +++ b/esphome/common/api_common.yaml @@ -1,7 +1,7 @@ -############################################# +########################################################################################## # Enable the Home Assistant API # https://esphome.io/components/api.html -############################################# +########################################################################################## api: encryption: key: ${local_api_key} diff --git a/esphome/common/include_basic_diag_sensors.yaml b/esphome/common/include_basic_diag_sensors.yaml new file mode 100644 index 0000000..53b0b4a --- /dev/null +++ b/esphome/common/include_basic_diag_sensors.yaml @@ -0,0 +1,64 @@ +substitutions: + local_update_interval: 30s + +########################################################################################## +# GENERAL COMMON SENSORS +# https://esphome.io/components/sensor/ +# LITE (1MB-friendly) +########################################################################################## +sensor: + - platform: uptime + name: "Uptime (s):" + update_interval: ${local_update_interval} + id: uptime_sensor + entity_category: diagnostic + + - platform: wifi_signal + name: "Wifi (dB):" + id: wifi_signal_db + update_interval: ${local_update_interval} + entity_category: diagnostic + + - platform: copy # Reports the WiFi signal strength in % + source_id: wifi_signal_db + name: "WiFi (%):" + filters: + - lambda: return min(max(2 * (x + 100.0), 0.0), 100.0); + unit_of_measurement: "% Max" + entity_category: "diagnostic" + device_class: "" + +binary_sensor: + - platform: status + name: "Network Status" + icon: mdi:check-network-outline + entity_category: diagnostic + +########################################################################################## +# Text Sensors +# https://esphome.io/components/text_sensor/index.html +########################################################################################## +text_sensor: + - platform: uptime # Uptime for this device human readable + name: "Uptime:" + icon: mdi:clock-start + update_interval: ${local_update_interval} + entity_category: "diagnostic" + +########################################################################################## +# BUTTONS +# Diagnostic buttons, activated if needed in HA +########################################################################################## +button: + - platform: safe_mode + name: "Safe Mode Restart:" + entity_category: "diagnostic" + disabled_by_default: true + - platform: restart + name: "Restart:" + entity_category: "diagnostic" + disabled_by_default: true + - platform: factory_reset + name: "FACTORY RESET:" + entity_category: "diagnostic" + disabled_by_default: true diff --git a/esphome/common/include_basic_reset_buttons.yaml b/esphome/common/include_basic_reset_buttons.yaml new file mode 100644 index 0000000..53b0b4a --- /dev/null +++ b/esphome/common/include_basic_reset_buttons.yaml @@ -0,0 +1,64 @@ +substitutions: + local_update_interval: 30s + +########################################################################################## +# GENERAL COMMON SENSORS +# https://esphome.io/components/sensor/ +# LITE (1MB-friendly) +########################################################################################## +sensor: + - platform: uptime + name: "Uptime (s):" + update_interval: ${local_update_interval} + id: uptime_sensor + entity_category: diagnostic + + - platform: wifi_signal + name: "Wifi (dB):" + id: wifi_signal_db + update_interval: ${local_update_interval} + entity_category: diagnostic + + - platform: copy # Reports the WiFi signal strength in % + source_id: wifi_signal_db + name: "WiFi (%):" + filters: + - lambda: return min(max(2 * (x + 100.0), 0.0), 100.0); + unit_of_measurement: "% Max" + entity_category: "diagnostic" + device_class: "" + +binary_sensor: + - platform: status + name: "Network Status" + icon: mdi:check-network-outline + entity_category: diagnostic + +########################################################################################## +# Text Sensors +# https://esphome.io/components/text_sensor/index.html +########################################################################################## +text_sensor: + - platform: uptime # Uptime for this device human readable + name: "Uptime:" + icon: mdi:clock-start + update_interval: ${local_update_interval} + entity_category: "diagnostic" + +########################################################################################## +# BUTTONS +# Diagnostic buttons, activated if needed in HA +########################################################################################## +button: + - platform: safe_mode + name: "Safe Mode Restart:" + entity_category: "diagnostic" + disabled_by_default: true + - platform: restart + name: "Restart:" + entity_category: "diagnostic" + disabled_by_default: true + - platform: factory_reset + name: "FACTORY RESET:" + entity_category: "diagnostic" + disabled_by_default: true diff --git a/esphome/common/include_debug_diag_sensors.yaml b/esphome/common/include_debug_diag_sensors.yaml new file mode 100644 index 0000000..1bfc7ee --- /dev/null +++ b/esphome/common/include_debug_diag_sensors.yaml @@ -0,0 +1,42 @@ +substitutions: + local_update_interval: 30s + +########################################################################################## +# GENERAL COMMON SENSORS +# https://esphome.io/components/sensor/ +########################################################################################## + +# Debug helpers (loop time / heap / reset info) +debug: + update_interval: ${local_update_interval} + +sensor: + # Free heap via debug (replaces template-based ESP.getFreeHeap if you prefer) + - platform: debug + free: + name: "Free Heap" + entity_category: "diagnostic" + + # Loop time via debug (replaces broken template loop time) + - platform: debug + loop_time: + name: "Loop Time" + entity_category: "diagnostic" + + ## If you’d rather keep the legacy heap sensor, uncomment below and + ## delete the debug "free" block above to avoid duplicates: + # - platform: template + # name: "${friendly_name} Free Heap" + # unit_of_measurement: "B" + # update_interval: 30s + # entity_category: "diagnostic" + # lambda: |- + # return (float) ESP.getFreeHeap(); + + ## # Only works with esp8266 + #- platform: adc + # pin: VCC + # name: "VCC Voltage" + # id: vcc_voltage_sensor + # entity_category: diagnostic + diff --git a/esphome/common/include_more_diag_sensors.yaml b/esphome/common/include_more_diag_sensors.yaml new file mode 100644 index 0000000..f1cf0ab --- /dev/null +++ b/esphome/common/include_more_diag_sensors.yaml @@ -0,0 +1,54 @@ +substitutions: + local_update_interval: 30s + +########################################################################################## +# Text Sensors +# https://esphome.io/components/text_sensor/index.html +########################################################################################## +text_sensor: + - platform: version + name: "Version:" + entity_category: "diagnostic" + + - platform: wifi_info + ip_address: + icon: mdi:ip-network + entity_category: diagnostic + name: "IP Address:" + ssid: + name: "Connected SSID" + icon: mdi:wifi-strength-2 + entity_category: diagnostic + mac_address: + name: "MAC Address:" + icon: mdi:network-pos + entity_category: diagnostic + +##################################################################################################### +# Creates a sensor of the uptime of the device, in formatted days, hours, minutes and seconds +################################################################################################### +# - platform: template +# name: "Uptime (Days)" +# entity_category: diagnostic +# lambda: |- +# int seconds = (id(uptime_sensor).state); +# int days = seconds / (24 * 3600); +# seconds = seconds % (24 * 3600); +# int hours = seconds / 3600; +# seconds = seconds % 3600; +# int minutes = seconds / 60; +# seconds = seconds % 60; +# if ( days > 3650 ) { +# return "Starting up" ; +# } else if ( days ) { +# return (String(days) +"d " + String(hours) +"h " + String(minutes) +"m "+ String(seconds) +"s").c_str() ; +# } else if ( hours ) { +# return (String(hours) +"h " + String(minutes) +"m "+ String(seconds) +"s").c_str() ; +# } else if ( minutes ) { +# return (String(minutes) +"m "+ String(seconds) +"s").c_str() ; +# } else { +# return (String(seconds) +"s").c_str() ; +# } +# icon: mdi:clock-start + + diff --git a/esphome/common/include_resetcount_diag_sensors.yaml b/esphome/common/include_resetcount_diag_sensors.yaml new file mode 100644 index 0000000..85dd29b --- /dev/null +++ b/esphome/common/include_resetcount_diag_sensors.yaml @@ -0,0 +1,183 @@ +########################################################################################## +########################################################################################## +# Title: Reset Classification & Counters (Interval-based, no on_boot work) +# Target: ESP8266/ESP32 (optimized for ESP8266 heap stability) +# +# Exposes four entities: +# - Restarts: Power (sensor) +# - Restarts: Software (sensor) +# - Restarts: Crashes (sensor) +# - Restarts: Last Reason (text_sensor) +# +# Also exposes a button: +# - Restarts: Clear Counters +# +# Behavior: +# - Does NOT run on_boot. Instead, an interval waits a short time after power-up, then: +# * Reads ESP.getResetReason() +# * Categorizes into one of: power, software, crash (exactly one bucket) +# * Increments that counter and publishes all three sensors + the last reason. +# - A 'run-once per boot' guard prevents re-incrementing on subsequent intervals. +# +# Notes: +# - Uses Arduino String (not std::string) in lambdas to reduce heap churn. +# - Sensors use update_interval: never; they’re pushed when we publish. +# - If you prefer a different delay, change the interval: value below. +########################################################################################## +########################################################################################## + +########################################################################################## +# GLOBALS +########################################################################################## +globals: + # Cumulative counters (persist across reboots) + - id: power_reset_count + type: int + restore_value: true + initial_value: '0' + - id: software_reset_count + type: int + restore_value: true + initial_value: '0' + - id: crash_reset_count + type: int + restore_value: true + initial_value: '0' + + # Per-boot guard so the interval only classifies once each boot + - id: reset_classified_this_boot + type: bool + restore_value: false # always false at cold boot + initial_value: 'false' + +########################################################################################## +# SENSORS (push-only; never self-update) +########################################################################################## +sensor: + - platform: template + id: power_reset_count_sensor + name: "${friendly_name} Restarts: Power" + icon: mdi:power-plug + unit_of_measurement: "restarts" + accuracy_decimals: 0 + entity_category: diagnostic + update_interval: never + + - platform: template + id: software_reset_count_sensor + name: "${friendly_name} Restarts: Software" + icon: mdi:restart + unit_of_measurement: "restarts" + accuracy_decimals: 0 + entity_category: diagnostic + update_interval: never + + - platform: template + id: crash_reset_count_sensor + name: "${friendly_name} Restarts: Crashes" + icon: mdi:alert-decagram + unit_of_measurement: "restarts" + accuracy_decimals: 0 + entity_category: diagnostic + update_interval: never + +########################################################################################## +# TEXT SENSORS (push-only; never self-update) +########################################################################################## +text_sensor: + - platform: template + id: last_reset_reason_txt + name: "${friendly_name} Restarts: Last Reason" + icon: mdi:restart-alert + entity_category: diagnostic + update_interval: never + +########################################################################################## +# BUTTONS +########################################################################################## +button: + - platform: template + id: clear_reset_counters_btn + name: "${friendly_name} Restarts: Clear Counters" + icon: mdi:backup-restore + entity_category: diagnostic + on_press: + - lambda: |- + id(power_reset_count) = 0; + id(software_reset_count) = 0; + id(crash_reset_count) = 0; + id(power_reset_count_sensor).publish_state(0); + id(software_reset_count_sensor).publish_state(0); + id(crash_reset_count_sensor).publish_state(0); + // Keep the last reason as-is; it describes the most recent boot. + +########################################################################################## +# SCRIPTS +########################################################################################## +script: + # Classify last reset exactly once per boot + - id: classify_reset_once + mode: restart + then: + - lambda: |- + // Guard: only run once per boot + if (id(reset_classified_this_boot)) return; + + // Read raw reason string (examples: "Power on", "External System", + // "Software/System restart", "Fatal exception:29", "WDT reset") + String reason = ESP.getResetReason(); + + // Category: 1=power, 2=software, 3=crash + int cat = 0; + + // Crash has priority if phrases appear (rare but be explicit) + if (reason.indexOf("Watchdog") >= 0 || reason.indexOf("WDT") >= 0 || + reason.indexOf("Exception") >= 0 || reason.indexOf("exception") >= 0) { + cat = 3; // crash + } + // Software/System restart (HA/API/OTA initiated) + else if (reason.indexOf("Software") >= 0 || reason.indexOf("System") >= 0) { + cat = 2; // software + } + // Power-like causes (power-on, external reset pin/button, deep sleep wake) + else if (reason.indexOf("Power") >= 0 || reason.indexOf("External") >= 0 || + reason.indexOf("Deep") >= 0) { + cat = 1; // power + } + // Anything else -> crash bucket + else { + cat = 3; + } + + if (cat == 1) id(power_reset_count) += 1; + if (cat == 2) id(software_reset_count) += 1; + if (cat == 3) id(crash_reset_count) += 1; + + // Publish reason text now + id(last_reset_reason_txt).publish_state(reason.c_str()); + + // Mark done for this boot + id(reset_classified_this_boot) = true; + + # Publish the three counters (kept separate so we can reuse) + - id: publish_reset_status + mode: restart + then: + - lambda: |- + id(power_reset_count_sensor).publish_state(id(power_reset_count)); + id(software_reset_count_sensor).publish_state(id(software_reset_count)); + id(crash_reset_count_sensor).publish_state(id(crash_reset_count)); + +########################################################################################## +# INTERVAL: run once shortly after boot, then no-op +########################################################################################## +interval: + - interval: 10s + then: + - if: + condition: + lambda: 'return !id(reset_classified_this_boot);' + then: + - script.execute: classify_reset_once + - script.execute: publish_reset_status + # On later passes, the guard prevents re-counting and this block does nothing. diff --git a/esphome/common/mqtt_common.yaml b/esphome/common/mqtt_common.yaml index 82b5833..b0dbee0 100644 --- a/esphome/common/mqtt_common.yaml +++ b/esphome/common/mqtt_common.yaml @@ -22,3 +22,4 @@ mqtt: id: mqtt_client reboot_timeout: 0s # same for MQTT log_topic: null # <— stops MQTT log streaming (big JSON payloads) + keepalive: 60s diff --git a/esphome/common/network_common.yaml b/esphome/common/network_common.yaml index 0dcd023..5559823 100644 --- a/esphome/common/network_common.yaml +++ b/esphome/common/network_common.yaml @@ -1,8 +1,8 @@ substitutions: - ############################################## + ############################################################################################ # SPECIFIC DEVICE VARIABLE SUBSTITUTIONS # If NOT using a secrets file, just replace these with the passwords etc (in quotes) - ############################################# + ########################################################################################### wifi_ssid: !secret ha_wifi_ssid wifi_password: !secret ha_wifi_password fallback_ap_password: !secret fallback_ap_password @@ -30,7 +30,7 @@ substitutions: base_interval_hours: "23" # Base interval in hours random_offset_max_minutes: "59" # Max random offset in minutes -############################################# +########################################################################################### # Common Wifi Settings # https://esphome.io/components/wifi.html # @@ -38,13 +38,13 @@ substitutions: # NONE (least power saving, Default for ESP8266) # LIGHT (Default for ESP32) # HIGH (most power saving) -############################################# +########################################################################################### wifi: ssid: ${wifi_ssid} password: ${wifi_password} #enable_rrm: true # (ESP32 only) enable 802.11k Radio Resource Management #enable_btm: true # (ESP32 only) enable 802.11v BSS Transition Management - #power_save_mode: LIGHT # https://esphome.io/components/wifi.html#wifi-power-save-mode + power_save_mode: none # https://esphome.io/components/wifi.html#wifi-power-save-mode manual_ip: # optional static IP address static_ip: ${local_static_ip_address} gateway: ${static_ip_gateway} @@ -63,60 +63,60 @@ wifi: #captive_portal: # extra fallback mechanism for when connecting if the configured WiFi fails -############################################# +########################################################################################### # Enable Over the Air Update Capability # https://esphome.io/components/ota.html?highlight=ota -############################################# +########################################################################################### ota: - platform: esphome password: ${local_ota_pass} version: 2 #- platform: web_server # Uncomment if you want to be able to do OTA with the web interface -############################################# +########################################################################################### # Safe Mode # Safe mode will detect boot loops # https://esphome.io/components/safe_mode -############################################# +########################################################################################### safe_mode: -############################################# +########################################################################################### # Network # global configuration for all types of networks # https://esphome.io/components/network.html -############################################# +########################################################################################### network: enable_ipv6: ${ipv6_enable} -############################################# +########################################################################################## # Interval # Restart Networking every x hours + rand mins # This ensure that the device is connected to the best AP -############################################# -script: - - id: random_reconnect - mode: restart - then: - - lambda: |- - // Compute total delay: base hours + random offset minutes - uint32_t extra; - #if defined(ESP32) - // ESP32 (both Arduino & IDF builds) uses esp_random() - extra = esp_random() % (${random_offset_max_minutes} + 1); - #elif defined(ESP8266) - // ESP8266 Arduino core - extra = os_random() % (${random_offset_max_minutes} + 1); - #else - // Fallback to esp_random() on other platforms - extra = esp_random() % (${random_offset_max_minutes} + 1); - #endif - uint32_t total_s = ${base_interval_hours} * 3600 + extra * 60; - ESP_LOGI("random_reconnect", "Next reconnect in %u seconds", total_s); - // Delay inside lambda (blocks script execution but OK for reconnect timing) - delay(total_s * 1000); - - logger.log: "network_check: performing reconnect" - - wifi.disable: {} - - delay: 1s - - wifi.enable: {} - - script.execute: random_reconnect +########################################################################################## +#script: +# - id: random_reconnect +# mode: restart +# then: +# - lambda: |- +# // Compute total delay: base hours + random offset minutes +# uint32_t extra; +# #if defined(ESP32) +# // ESP32 (both Arduino & IDF builds) uses esp_random() +# extra = esp_random() % (${random_offset_max_minutes} + 1); +# #elif defined(ESP8266) +# // ESP8266 Arduino core +# extra = os_random() % (${random_offset_max_minutes} + 1); +# #else +# // Fallback to esp_random() on other platforms +# extra = esp_random() % (${random_offset_max_minutes} + 1); +# #endif +# uint32_t total_s = ${base_interval_hours} * 3600 + extra * 60; +# ESP_LOGI("random_reconnect", "Next reconnect in %u seconds", total_s); +# // Delay inside lambda (blocks script execution but OK for reconnect timing) +# delay(total_s * 1000); +# - logger.log: "network_check: performing reconnect" +# - wifi.disable: {} +# - delay: 1s +# - wifi.enable: {} +# - script.execute: random_reconnect diff --git a/esphome/common/network_common_dual.yaml b/esphome/common/network_common_dual.yaml deleted file mode 100644 index 5003a71..0000000 --- a/esphome/common/network_common_dual.yaml +++ /dev/null @@ -1,126 +0,0 @@ -substitutions: - ############################################## - # SPECIFIC DEVICE VARIABLE SUBSTITUTIONS - # If NOT using a secrets file, just replace these with the passwords etc (in quotes) - ############################################# - wifi_ssid: !secret ha_wifi_ssid - wifi_password: !secret ha_wifi_password - fallback_ap_password: !secret fallback_ap_password - - # Enables faster network connections, with last connected SSID being connected to and no full scan for SSID being undertaken - wifi_fast_connect: "false" - - # Define a domain for this device to use. i.e. iot.home.lan (so device will appear as athom-smart-plug-v2.iot.home.lan in DNS/DHCP logs) - dns_domain: ".local" - - # Automatically add the mac address to the name - # eg so you can use a single firmware for all devices - add_mac_suffix: "false" - - # Enable or disable the use of IPv6 networking on the device - ipv6_enable: "false" - - # Add these if we are giving it a static ip, or remove them in the Wifi section - static_ip_subnet: !secret ha_wifi_subnet - static_ip_gateway: !secret ha_wifi_gateway - static_ip_dns1: !secret ha_wifi_dns1 - static_ip_dns2: !secret ha_wifi_dns2 - - # Network reconnect every x hours to ensure best access point - base_interval_hours: "6" # Base interval in hours - random_offset_max_minutes: "59" # Max random offset in minutes - -############################################# -# Common Wifi Settings -# https://esphome.io/components/wifi.html -# -# Power Save mode (can reduce wifi reliability) -# NONE (least power saving, Default for ESP8266) -# LIGHT (Default for ESP32) -# HIGH (most power saving) -############################################# -wifi: - networks: - - ssid: "vega_control_1" - password: "3Kd#vQBP8RDc" - priority: 1.0 - - ssid: ${wifi_ssid} - password: ${wifi_password} - #enable_rrm: true # (ESP32 only) enable 802.11k Radio Resource Management - #enable_btm: true # (ESP32 only) enable 802.11v BSS Transition Management - #power_save_mode: LIGHT # https://esphome.io/components/wifi.html#wifi-power-save-mode - priority: 0.5 - manual_ip: # optional static IP address - static_ip: ${local_static_ip_address} - gateway: ${static_ip_gateway} - subnet: ${static_ip_subnet} - dns1: ${static_ip_dns1} - dns2: ${static_ip_dns2} - - use_address: ${local_static_ip_address} # required when any network uses manual_ip - ap: # Details for fallback hotspot in case wifi connection fails https://esphome.io/components/wifi.html#access-point-mode - ssid: ${device_name} AP - password: ${fallback_ap_password} - ap_timeout: 10min # Time until it brings up fallback AP. default is 1min - # Allow rapid re-connection to previously connect WiFi SSID, skipping scan of all SSID - fast_connect: "${wifi_fast_connect}" - # Define dns domain / suffix to add to hostname - domain: "${dns_domain}" - -#captive_portal: # extra fallback mechanism for when connecting if the configured WiFi fails - -############################################# -# Enable Over the Air Update Capability -# https://esphome.io/components/ota.html?highlight=ota -############################################# -ota: - - platform: esphome - password: ${local_ota_pass} - version: 2 - -############################################# -# Safe Mode -# Safe mode will detect boot loops -# https://esphome.io/components/safe_mode -############################################# -safe_mode: - -############################################# -# Network -# global configuration for all types of networks -# https://esphome.io/components/network.html -############################################# -network: - enable_ipv6: ${ipv6_enable} - -############################################# -# Interval -# Restart Networking every x hours + rand mins -############################################# -script: - - id: random_reconnect - mode: restart - then: - - lambda: |- - // Compute total delay: base hours + random offset minutes - uint32_t extra; - #if defined(ESP32) - // ESP32 (both Arduino & IDF builds) uses esp_random() - extra = esp_random() % (${random_offset_max_minutes} + 1); - #elif defined(ESP8266) - // ESP8266 Arduino core - extra = os_random() % (${random_offset_max_minutes} + 1); - #else - // Fallback to esp_random() on other platforms - extra = esp_random() % (${random_offset_max_minutes} + 1); - #endif - uint32_t total_s = ${base_interval_hours} * 3600 + extra * 60; - ESP_LOGI("random_reconnect", "Next reconnect in %u seconds", total_s); - // Delay inside lambda (blocks script execution but OK for reconnect timing) - delay(total_s * 1000); - - logger.log: "network_check: performing reconnect" - - wifi.disable: {} - - delay: 1s - - wifi.enable: {} - - script.execute: random_reconnect - diff --git a/esphome/common/network_common_fallback.yaml b/esphome/common/network_common_fallback.yaml deleted file mode 100644 index bb7eb0a..0000000 --- a/esphome/common/network_common_fallback.yaml +++ /dev/null @@ -1,81 +0,0 @@ -substitutions: - ############################################## - # SPECIFIC DEVICE VARIABLE SUBSTITUTIONS - # If NOT using a secrets file, just replace these with the passwords etc (in quotes) - ############################################# - wifi_ssid: !secret ha_wifi_ssid - wifi_password: !secret ha_wifi_password - fallback_ap_password: !secret fallback_ap_password - - # Enables faster network connections, with last connected SSID being connected to and no full scan for SSID being undertaken - wifi_fast_connect: "false" - - # Define a domain for this device to use. i.e. iot.home.lan (so device will appear as athom-smart-plug-v2.iot.home.lan in DNS/DHCP logs) - dns_domain: ".local" - - # Automatically add the mac address to the name - # eg so you can use a single firmware for all devices - add_mac_suffix: "false" - - # Enable or disable the use of IPv6 networking on the device - ipv6_enable: "false" - - # Add these if we are giving it a static ip, or remove them in the Wifi section - static_ip_subnet: !secret ha_wifi_subnet - static_ip_gateway: !secret ha_wifi_gateway - static_ip_dns1: !secret ha_wifi_gateway - -############################################# -# Common Wifi Settings -# https://esphome.io/components/wifi.html -# -# Power Save mode (can reduce wifi reliability) -# NONE (least power saving, Default for ESP8266) -# LIGHT (Default for ESP32) -# HIGH (most power saving) -############################################# -wifi: - ssid: ${wifi_ssid} - password: ${wifi_password} - #enable_rrm: true # (ESP32 only) enable 802.11k Radio Resource Management - #enable_btm: true # (ESP32 only) enable 802.11v BSS Transition Management - #power_save_mode: LIGHT # https://esphome.io/components/wifi.html#wifi-power-save-mode - manual_ip: # optional static IP address - static_ip: ${local_static_ip_address} - gateway: ${static_ip_gateway} - subnet: ${static_ip_subnet} - dns1: ${static_ip_dns1} - ap: # Details for fallback hotspot in case wifi connection fails https://esphome.io/components/wifi.html#access-point-mode - ssid: ${device_name} AP - password: ${fallback_ap_password} - ap_timeout: 10min # Time until it brings up fallback AP. default is 1min - # Allow rapid re-connection to previously connect WiFi SSID, skipping scan of all SSID - fast_connect: "${wifi_fast_connect}" - # Define dns domain / suffix to add to hostname - domain: "${dns_domain}" - -#captive_portal: # extra fallback mechanism for when connecting if the configured WiFi fails - -############################################# -# Enable Over the Air Update Capability -# https://esphome.io/components/ota.html?highlight=ota -############################################# -ota: - - platform: esphome - password: ${local_ota_pass} - version: 2 - -############################################# -# Safe Mode -# Safe mode will detect boot loops -# https://esphome.io/components/safe_mode -############################################# -safe_mode: - -############################################# -# Network -# global configuration for all types of networks -# https://esphome.io/components/network.html -############################################# -network: - enable_ipv6: ${ipv6_enable} \ No newline at end of file diff --git a/esphome/common/secrets copy.yaml b/esphome/common/secrets copy.yaml deleted file mode 100644 index 257cb74..0000000 --- a/esphome/common/secrets copy.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include ../secrets.yaml \ No newline at end of file diff --git a/esphome/common/sensors_common.yaml b/esphome/common/sensors_common.yaml index 534d300..4f4a9e2 100644 --- a/esphome/common/sensors_common.yaml +++ b/esphome/common/sensors_common.yaml @@ -1,26 +1,58 @@ +substitutions: + local_update_interval: 30s + ########################################################################################## # GENERAL COMMON SENSORS # https://esphome.io/components/sensor/ ########################################################################################## + +# Debug helpers (loop time / heap / reset info) +debug: + update_interval: ${local_update_interval} + sensor: - - platform: uptime # Uptime for this device in seconds - name: "Uptime (s):" - update_interval: ${local_update_interval} - id: uptime_sensor - entity_category: "diagnostic" - - platform: wifi_signal # Wifi Strength - name: "Wifi (dB):" - id: wifi_signal_db - update_interval: ${local_update_interval} - entity_category: "diagnostic" - - platform: copy # Reports the WiFi signal strength in % - source_id: wifi_signal_db - name: "WiFi (%):" - filters: - - lambda: return min(max(2 * (x + 100.0), 0.0), 100.0); - unit_of_measurement: "% Max" - entity_category: "diagnostic" - device_class: "" +# - platform: uptime # Uptime for this device in seconds +# name: "Uptime (s):" +# update_interval: ${local_update_interval} +# id: uptime_sensor +# entity_category: "diagnostic" + +# - platform: wifi_signal # Wifi Strength +# name: "Wifi (dB):" +# id: wifi_signal_db +# update_interval: ${local_update_interval} +# entity_category: "diagnostic" + +# - platform: copy # Reports the WiFi signal strength in % +# source_id: wifi_signal_db +# name: "WiFi (%):" +# filters: +# - lambda: return min(max(2 * (x + 100.0), 0.0), 100.0); +# unit_of_measurement: "% Max" +# entity_category: "diagnostic" +# device_class: "" + + # Free heap via debug (replaces template-based ESP.getFreeHeap if you prefer) + - platform: debug + free: + name: "Free Heap" + entity_category: "diagnostic" + + # Loop time via debug (replaces broken template loop time) + - platform: debug + loop_time: + name: "Loop Time" + entity_category: "diagnostic" + + # If you’d rather keep the legacy heap sensor, uncomment below and + # delete the debug "free" block above to avoid duplicates: + # - platform: template + # name: "${friendly_name} Free Heap" + # unit_of_measurement: "B" + # update_interval: 30s + # entity_category: "diagnostic" + # lambda: |- + # return (float) ESP.getFreeHeap(); # Only works with esp8266 #- platform: adc @@ -29,11 +61,11 @@ sensor: # id: vcc_voltage_sensor # entity_category: diagnostic -binary_sensor: - - platform: status - name: "Network Status" - icon: mdi:check-network-outline - entity_category: diagnostic +#binary_sensor: +# - platform: status +# name: "Network Status" +# icon: mdi:check-network-outline +# entity_category: diagnostic ########################################################################################## # Text Sensors @@ -46,6 +78,7 @@ text_sensor: - platform: version name: "Version:" entity_category: "diagnostic" + - platform: wifi_info ip_address: icon: mdi:ip-network @@ -59,37 +92,12 @@ text_sensor: name: "MAC Address:" icon: mdi:network-pos entity_category: diagnostic + - platform: uptime # Uptime for this device human readable name: "Uptime:" icon: mdi:clock-start update_interval: ${local_update_interval} entity_category: "diagnostic" - - platform: template - name: "Last Reset Reason" - update_interval: 30s - entity_category: diagnostic - lambda: |- - #if defined(USE_ESP8266) - return { ESP.getResetReason().c_str() }; - #elif defined(USE_ESP32) - auto r = esp_reset_reason(); - switch (r) { - case ESP_RST_POWERON: return { "Power-on" }; - case ESP_RST_EXT: return { "External pin" }; - case ESP_RST_SW: return { "Software reset" }; - case ESP_RST_PANIC: return { "Exception/Panic" }; - case ESP_RST_INT_WDT: return { "Interrupt WDT" }; - case ESP_RST_TASK_WDT: return { "Task WDT" }; - case ESP_RST_WDT: return { "Other WDT" }; - case ESP_RST_DEEPSLEEP: return { "Deep sleep wake" }; - case ESP_RST_BROWNOUT: return { "Brownout" }; - case ESP_RST_SDIO: return { "SDIO" }; - default: return { "Unknown" }; - } - #else - return { "Unknown" }; - #endif - ################################################################################################### # Creates a sensor of the uptime of the device, in formatted days, hours, minutes and seconds @@ -118,44 +126,23 @@ text_sensor: # } # icon: mdi:clock-start -# - platform: template -# name: "Uptime (Days)" -# entity_category: diagnostic -# lambda: |- -# int seconds = (id(uptime_sensor).state); -# int days = seconds / (24 * 3600); -# seconds = seconds % (24 * 3600); -# int hours = seconds / 3600; -# seconds = seconds % 3600; -# int minutes = seconds / 60; -# seconds = seconds % 60; -# if ( days > 3650 ) { -# return { "Starting up" }; -# } else if ( days ) { -# return { (String(days) +"d " + String(hours) +"h " + String(minutes) +"m "+ String(seconds) +"s").c_str() }; -# } else if ( hours ) { -# return { (String(hours) +"h " + String(minutes) +"m "+ String(seconds) +"s").c_str() }; -# } else if ( minutes ) { -# return { (String(minutes) +"m "+ String(seconds) +"s").c_str() }; -# } else { -# return { (String(seconds) +"s").c_str() }; -# } -# icon: mdi:clock-start - ########################################################################################## # BUTTONS # Diagnostic buttons, activated if needed in HA ########################################################################################## -button: - - platform: safe_mode - name: "Safe Mode Restart:" - entity_category: "diagnostic" - disabled_by_default: true - - platform: restart - name: "Restart:" - entity_category: "diagnostic" - disabled_by_default: true - - platform: factory_reset - name: "FACTORY RESET:" - entity_category: "diagnostic" - disabled_by_default: true +#button: +# - platform: safe_mode +# name: "Safe Mode Restart:" +# entity_category: "diagnostic" +# disabled_by_default: true +# +# - platform: restart +# name: "Restart:" +# entity_category: "diagnostic" +# disabled_by_default: true +# +# - platform: factory_reset +# name: "FACTORY RESET:" +# entity_category: "diagnostic" +# disabled_by_default: true + diff --git a/esphome/common/sensors_common_lite.yaml b/esphome/common/sensors_common_lite.yaml index 7abd555..7f38cd3 100644 --- a/esphome/common/sensors_common_lite.yaml +++ b/esphome/common/sensors_common_lite.yaml @@ -1,3 +1,6 @@ +substitutions: + local_update_interval: 30s + ########################################################################################## # GENERAL COMMON SENSORS # https://esphome.io/components/sensor/ @@ -16,6 +19,15 @@ sensor: update_interval: ${local_update_interval} entity_category: diagnostic + - platform: copy # Reports the WiFi signal strength in % + source_id: wifi_signal_db + name: "WiFi (%):" + filters: + - lambda: return min(max(2 * (x + 100.0), 0.0), 100.0); + unit_of_measurement: "% Max" + entity_category: "diagnostic" + device_class: "" + binary_sensor: - platform: status name: "Network Status" diff --git a/esphome/common/sntp_common.yaml b/esphome/common/sntp_common.yaml index bede439..32f1463 100644 --- a/esphome/common/sntp_common.yaml +++ b/esphome/common/sntp_common.yaml @@ -1,8 +1,8 @@ -############################################## +############################################################################################ # SPECIFIC DEVICE VARIABLE SUBSTITUTIONS # If NOT using a secrets file, just replace # these with the values (in quotes) -############################################# +########################################################################################### substitutions: timezone: "Pacific/Auckland" sntp_update_interval: 6h # Set the duration between the sntp service polling @@ -11,12 +11,12 @@ substitutions: sntp_server_2: !secret ntp_server_2 sntp_server_3: !secret ntp_server_3 -############################################# +########################################################################################### # Real time clock time source for ESPHome # If it's invalid, we fall back to an internal clock # https://esphome.io/components/time/index.html # https://esphome.io/components/time/sntp -############################################# +########################################################################################### time: - platform: sntp id: sntp_time @@ -53,14 +53,14 @@ time: text_sensor: # Creates a sensor showing when the device was last restarted - platform: template - name: "Last Restart" + name: "Time: Last Restart" id: device_last_restart icon: mdi:clock entity_category: diagnostic #device_class: timestamp - platform: template - name: "Internal Time" + name: "Time: Internal Time" id: time_text update_interval: "1min" entity_category: diagnostic @@ -69,7 +69,7 @@ text_sensor: return { time_text }; - platform: template - name: "Time Sync Status" + name: "Time: Sync Status" id: time_sync update_interval: "1min" entity_category: diagnostic \ No newline at end of file diff --git a/esphome/esp-3dprinterpow.yaml b/esphome/esp-3dprinterpow.yaml index 7fc3a84..c13924d 100644 --- a/esphome/esp-3dprinterpow.yaml +++ b/esphome/esp-3dprinterpow.yaml @@ -17,47 +17,69 @@ # If NOT using a secrets file, just replace these with the passwords etc (in quotes) ############################################# substitutions: - device_name: "esp-3dprinterpow" # Reference name for the device in the system. - project_name: "Sonoff Technologies.POW R1" # Project Details - project_version: "v1" # Project V denotes release of yaml file, allowing checking of deployed vs latest version - entity_prefix: "Load" # Simple device name where we want to prefix a sensor or switch, eg "Load" Current. + # Device Naming + device_name: "esp-3dprinterpow" friendly_name: "3D Printer Power" description_comment: "3D Printer inline monitor and switch override using a Sonoff Pow R1." - device_area: "Laundry" # Allows ESP device to be automatically linked to an 'Area' in Home Assistant. - 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-3dprinterpow_ip + device_area: "Bedroom 3" # Allows ESP device to be automatically linked to an 'Area' in Home Assistant. + + # Project Naming + project_name: "Sonoff Technologies.POW R1" # Project Details + project_version: "v1" # Project V denotes release of yaml file, allowing checking of deployed vs latest version + + # Passwords & Secrets + api_key: !secret esp-api_key + ota_pass: !secret esp-ota_pass + static_ip_address: !secret esp-3dprinterpow_ip # unfortunately you can't use substitutions inside secrets names + mqtt_command_main_topic: !secret mqtt_command_main_topic + mqtt_status_main_topic: !secret mqtt_status_main_topic + + # MQTT LOCAL Controls + mqtt_local_device_name: "3dprinter-power" + mqtt_local_command_topic: "${mqtt_command_main_topic}/${mqtt_local_device_name}" # Topic we will use to command this locally without HA + mqtt_local_status_topic: "${mqtt_status_main_topic}/${mqtt_local_device_name}" # Topic we will use to view status locally without HA + + # Device Settings log_level: "INFO" # Define logging level: NONE, ERROR, WARN, INFO, DEBUG (Default), VERBOSE, VERY_VERBOSE update_interval: "60s" # update time for for general sensors etc + entity_prefix: "Load" # Simple device name where we want to prefix a sensor or switch, eg "Load" Current. ########################################################################################## # 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}" + + #### 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}" - #common_webportal: !include - # file: common/webportal_common.yaml + + #### MQTT #### common_mqtt: !include file: common/mqtt_common.yaml vars: local_device_name: "${device_name}" - common_sntp: !include - file: common/sntp_common.yaml - common_general_sensors: !include - file: common/sensors_common.yaml - vars: - local_friendly_name: "${friendly_name}" - local_update_interval: "${update_interval}" + + #### 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 @@ -71,10 +93,10 @@ esphome: project: name: "${project_name}" version: "${project_version}" - #on_boot: - #priority: 200 - #then: - # - switch.turn_on: "virtual_button" + on_boot: + priority: -10 + then: + - script.execute: publish_state_burst ############################################# # ESP Platform and Framework @@ -95,6 +117,24 @@ logger: #esp8266_store_log_strings_in_flash: false #tx_buffer_size: 64 +########################################################################################## +# MQTT COMMANDS +# This adds device-specific MQTT command triggers to the common MQTT configuration. +########################################################################################## +mqtt: + on_message: + # Switch control from MQTT + - topic: "${mqtt_local_command_topic}" + payload: "ON" + then: + - switch.turn_on: relay + - light.turn_on: led + - topic: "${mqtt_local_command_topic}" + payload: "OFF" + then: + - switch.turn_off: relay + - light.turn_off: led + ############################################# # STATUS LED # https://esphome.io/components/status_led.html @@ -164,10 +204,22 @@ switch: turn_off_action: - switch.turn_off: relay - light.turn_off: led + - platform: gpio id: relay pin: GPIO12 - #restore_mode: ALWAYS_ON # Or RESTORE_DEFAULT_ON/RESTORE_DEFAULT_OFF + restore_mode: RESTORE_DEFAULT_ON + # restore_mode: ALWAYS_ON # Or RESTORE_DEFAULT_ON/RESTORE_DEFAULT_OFF + on_turn_on: + - mqtt.publish: + topic: "${mqtt_local_status_topic}" + payload: "ON" + retain: false + on_turn_off: + - mqtt.publish: + topic: "${mqtt_local_status_topic}" + payload: "OFF" + retain: false output: - platform: esp8266_pwm @@ -180,4 +232,18 @@ light: - platform: monochromatic name: Status LED output: pow_blue_led - id: led \ No newline at end of file + id: led + +script: + - id: publish_state_burst + mode: restart + then: + - repeat: + count: 5 + then: + - mqtt.publish: + topic: "${mqtt_local_status_topic}" + payload: !lambda 'return id(relay).state ? "ON" : "OFF";' + retain: false + qos: 1 + - delay: 5s \ No newline at end of file diff --git a/esphome/esp-attobat.yaml b/esphome/esp-attobat.yaml.old similarity index 98% rename from esphome/esp-attobat.yaml rename to esphome/esp-attobat.yaml.old index a77a1b0..1587695 100644 --- a/esphome/esp-attobat.yaml +++ b/esphome/esp-attobat.yaml.old @@ -85,6 +85,7 @@ api: ota: - platform: esphome password: ${ota_pass} + - platform: web_server # Uncomment if you want to be able to do OTA with the web interface ############################################# # Safe Mode diff --git a/esphome/esp-attobattery.yaml b/esphome/esp-attobattery.yaml new file mode 100644 index 0000000..6ad38b2 --- /dev/null +++ b/esphome/esp-attobattery.yaml @@ -0,0 +1,180 @@ +########################################################################################## +########################################################################################## +# BYD ATTO3 12V Battery Monitor +# Monitoring the status of a vehicle 12V battery with +# an esp8266 (D1 Mini). It will obviously only +# transmit when the vehicle is within wifi range. +# Voltage is measured with a resistor voltage divider +# into the analogue GPIO on the esp8266. +# https://zorruno.com/2022/vehicle-12v-battery-monitoring/ +########################################################################################## +########################################################################################## + +########################################################################################## +# 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-attobattery" + friendly_name: "Atto3 12V Battery Monitor" + description_comment: "Atto3 12V Battery Monitor (when home)" + device_area: "Garage" # Allows ESP device to be automatically linked to an 'Area' in Home Assistant. + + # Project Naming + project_name: "Generic ESP8266.D1 Mini" # Project Details + project_version: "v1.0" # Project V denotes release of yaml file, allowing checking of deployed vs latest version + + # 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-attobattery_ip + mqtt_command_main_topic: !secret mqtt_command_main_topic + mqtt_status_main_topic: !secret mqtt_status_main_topic + + # MQTT LOCAL Controls + mqtt_device_name: "atto-battery" + mqtt_local_command_topic: "${mqtt_command_main_topic}/${mqtt_device_name}" # Topic we will use to command this locally without HA + mqtt_local_status_topic: "${mqtt_status_main_topic}/${mqtt_device_name}" # Topic we will use to view status locally without HA + + # Device Settings + #relay_icon: "mdi:lightbulb-group" + log_level: "DEBUG" # Define logging level: NONE, ERROR, WARN, INFO, DEBUG (Default), VERBOSE, VERY_VERBOSE + update_interval: "60s" # update time for for general sensors etc + +########################################################################################## +# 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 + min_version: 2024.6.0 + +########################################################################################## +# ESP Platform and Framework +# https://esphome.io/components/esp8266.html +# https://esphome.io/components/esp32.html +########################################################################################## +esp8266: + board: d1_mini + framework: + version: latest #recommended, latest or dev + +########################################################################################## +# ESPHome Logging Enable +# https://esphome.io/components/logger.html +########################################################################################## +logger: + level: "${log_level}" #INFO Level 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 (eg PZEM) + #esp8266_store_log_strings_in_flash: false + #tx_buffer_size: 64 + +############################################# +# Enable Over the Air Update Capability +# https://esphome.io/components/ota.html?highlight=ota +############################################# +ota: + - platform: web_server # Uncomment if you want to be able to do OTA with the web interface + +########################################################################################## +# 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_ota_pass: "${ota_pass}" + common_api: !include + #file: common/api_common_noencryption.yaml + file: common/api_common.yaml + vars: + local_api_key: "${api_key}" + common_webportal: !include + file: common/webportal_common.yaml + common_mqtt: !include + file: common/mqtt_common.yaml + vars: + local_device_name: "${device_name}" + #common_sntp: !include + # file: common/sntp_common.yaml + common_general_sensors: !include + file: common/sensors_common_lite.yaml + #file: common/sensors_common.yaml + vars: + local_friendly_name: "${friendly_name}" + local_update_interval: "${update_interval}" + +########################################################################################## +# MQTT COMMANDS +# This adds device-specific MQTT command triggers to the common MQTT configuration. +########################################################################################## +mqtt: + + # Availability Topic + birth_message: + topic: ${mqtt_local_status_topic}/availability + payload: online + will_message: + topic: ${mqtt_local_status_topic}/availability + payload: offline + + # A way to prevent deep sleep using MQTT command + on_message: + - topic: ${mqtt_local_command_topic}/deepsleep + payload: "OFF" + then: + - deep_sleep.prevent: deep_sleep_1 + - topic: ${mqtt_local_command_topic}/deepsleep + payload: "ON" + then: + - deep_sleep.enter: deep_sleep_1 + +########################################################################################## +# Deep Sleep +# https://esphome.io/components/deep_sleep.html +########################################################################################## +deep_sleep: + run_duration: 20s + sleep_duration: 5min + id: deep_sleep_1 + +########################################################################################## +# SENSOR COMPONENT +# https://esphome.io/components/sensor/ +########################################################################################## +sensor: + #Quality of Wifi in dBm + - platform: wifi_signal + name: "WiFi Signal" + update_interval: ${update_interval} + retain: true #Retain this as you'll have no value between sleeps otherwise + #Analog sensor for voltage reading (A0) + - platform: uptime + name: "Uptime" + update_interval: ${update_interval} + retain: true #Retain this as you'll have no value between sleeps otherwise + - platform: adc + pin: A0 + name: "Battery Voltage" + update_interval: ${update_interval} + retain: true #Retain this as you'll have no value between sleeps otherwise + filters: + - multiply: 3.3 #D1 mini V divider, 3.3V -> 1V on esp8266 + - calibrate_linear: #Read values with voltmeter and bench supply + - 3.11 -> 14.00 + - 3.00 -> 13.50 + - 2.89 -> 13.00 + - 2.77 -> 12.50 + - 2.67 -> 12.00 + - 2.55 -> 11.50 + - 2.45 -> 11.00 + - 2.34 -> 10.50 + - 2.22 -> 10.00 + - 2.11 -> 09.50 + - 2.00 -> 09.00 diff --git a/esphome/esp-bedrm2ceilingfan.yaml b/esphome/esp-bedrm2ceilingfan.yaml index cf10a8e..2e542bb 100644 --- a/esphome/esp-bedrm2ceilingfan.yaml +++ b/esphome/esp-bedrm2ceilingfan.yaml @@ -153,6 +153,60 @@ logger: #esp8266_store_log_strings_in_flash: false #tx_buffer_size: 64 +########################################################################################## +# UART: Fan MCU / RF Remote Sniff (iFan02 -> ESP over UART0) +# - Uses GPIO3 (RX0) to listen to the fan microcontroller frames (433 MHz remote is decoded there) +# - 9600 8N1 is typical; if no frames appear, try 19200 (uncomment below) +# - If still nothing, some revisions route MCU TX to another pin; try rx_pin: GPIO13 +########################################################################################## +uart: + id: ifan_uart + tx_pin: GPIO1 # ESP TX0 -> Fan MCU RX (not required for sniffing, but harmless) + rx_pin: GPIO3 # ESP RX0 <- Fan MCU TX (primary source for RF button frames) + baud_rate: 9600 + #baud_rate: 19200 # <-- try this if you see no frames in logs + rx_buffer_size: 256 + # NOTE: If no frames appear at either 9600 or 19200, try moving rx_pin to GPIO13 and reflash: + # rx_pin: GPIO13 + +########################################################################################## +# INTERVAL: Accumulate UART bytes and publish last frame to text sensor +# - Publishes after ~60ms of silence to group a single button press into one frame +########################################################################################## +interval: + - interval: 50ms + then: + - lambda: |- + static std::string frame; + static uint32_t last = 0; + uint8_t b; + while (id(ifan_uart).available()) { + if (id(ifan_uart).read_byte(&b)) { + char buf[4]; + sprintf(buf, "%02X", b); + frame += buf; + frame += " "; + last = millis(); + } else { + break; + } + } + if (!frame.empty() && (millis() - last > 60)) { + ESP_LOGI("ifan02", "MCU frame: %s", frame.c_str()); + id(ifan_last_rf).publish_state(frame.c_str()); + frame.clear(); + } + +########################################################################################## +# TEXT SENSORS: RF/MCU debug output (last frame seen) +# - Shows the last raw hex bytes received from the fan MCU when you press the RF remote +########################################################################################## +text_sensor: + - platform: template + id: ifan_last_rf + name: "iFan02 Remote Last Frame" + update_interval: never + ########################################################################################## # MQTT COMMANDS # This adds device-specific MQTT command triggers to the common MQTT configuration. @@ -371,11 +425,11 @@ fan: # whenever the fan goes off→on (e.g. via HA’s Fan switch): on_turn_on: - lambda: |- - // bump 0→1 if we were off + // 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));' \ No newline at end of file + payload: !lambda 'return to_string(id(speed_value));' diff --git a/esphome/esp-breakfastbarleds.yaml b/esphome/esp-breakfastbarleds.yaml index 75e2071..ab62ca2 100644 --- a/esphome/esp-breakfastbarleds.yaml +++ b/esphome/esp-breakfastbarleds.yaml @@ -1,103 +1,132 @@ ########################################################################################## ########################################################################################## -# Title: BREAKFAST BAR LEDS (Lounge) -# Hardware: Magichome inline single channel dimmer (ESP8266) +# Title: BREAKFAST BAR LEDS (Lounge) +# Repo: https://home.fox.co.nz/gitea/zorruno/zorruno-homeassistant/src/branch/master/esphome/esp-loungebookshelfleds.yaml # -# Repo: -# +# v2.0 - 2025-09-15 A bunch of fixes and made it more standard across my devices # v1.0 - 2025-08-17 First setup (and replacement of Tasmota) # # ------------------------------------------ +# OPERATION (as of v2.0) +# ------------------------------------------ +# 1. General-purpose LED controller. +# 2. Designed for the Sinilink XY-WFMS board with a MOSFET output (claimed 5A, 5-36 V DC), but will suit similar mosfet boards. +# 3. (In SUBSTITUTIONS) setting for MAX % output to extend LED life (ie can ensure it is only ever 95%) +# 4. (In SUBSTITUTIONS) Minimum output setting; switches fully OFF at/below the minimum to avoid low-PWM flicker. +# 5. (In SUBSTITUTIONS) PWM frequency is set to 1kHz by default. You can increase it, but higher values caused +# resets on Sinilink device. On ESP32 you can run much higher (~40 kHz). +# 6. Min/Max output settings are not exposed in Home Assistant/MQTT by default, but could be. +# With a 1 MB flash, space is tight and only minimal optimisation has been done so far. +# 7. (PACKAGES) include common items: network settings, diagnostic entities, MQTT, and SNTP (optional). +# 8. Default behaviours, for recovery from lost power or reset: +# - "Fade up to full": fade from floor to max on boot. +# - "Restore Brightness": fade from floor to the last brightness on boot (should be no on/off blip/flash). +# - "Remain Off": stays off on boot. +# 9. The green LED flashes while fading (different patterns for up/down). The red LED follows the +# output (it shares the MOSFET GPIO). (Obviously this depends on hardware) +# 10. Fade timing scales with the configured values (proportionally when starting mid-brightness). +# 11. Exposed in Home Assistant/MQTT: +# - Startup action (On,Off,Restore) +# - Fade Up / Fade Down / Fade Stop buttons +# - Fade Up/Down toggle switch (keeps fading up to full, or down to off unless stopped) +# - Normal On/Off switch (quick ramp up/down, up to the level of previous brightness) +# - Fade up/down times (0-60s) +# - Output % (pre-gamma) and PWM % (post-gamma) +# - Output Set (1-100, respects min/max). This also changes with other output so reflects value. +# - Many device diagnostics (from the included 'diagnostics' package) +# - Maximum 'on' time before automatic fade-down (1-48 h, 0 = no limit) +########################################################################################## +########################################################################################## +# Hardware: Magishome LED controller single channel (ESP8266, NOT the later ones with BL602) +# https://devices.esphome.io/devices/MagicHome-ZJ-WFMN-C-Single-Color +# ------------------------------------------ # DEVICE GPIO (Magichome ESP8266) # ------------------------------------------ # GPIO02 Blue LED (used for ESPHome status) - I think... can't see the board # GPIO12 MOSFET output (0 V when switched) -# GPIO13 Green LED (used to display fading status) -# -# ------------------------------------------ -# OPERATION -# ------------------------------------------ -# - Startup/power restore action -# - Fade Up / Fade Down / Fade Stop buttons -# - Fade Up/Down switch -# - Normal On/Off switch (quick ramp up/down) -# - Fade up/down times (0-60s) -# - Output % (pre-gamma) and PWM % (post-gamma) -# - Output Set (1-100, respects min/max) -# - Device diagnostics (from the included package) -# - Maximum 'on' time before automatic fade-down (1-48 h, 0 = no limit) -# ########################################################################################## ########################################################################################## - ########################################################################################## # 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-breakfastbarleds" + device_name: "esp-breakfastbarleds" # yaml file should be device_name.yaml friendly_name: "Breakfast Bar LEDs" description_comment: "LED Strip under the breakfast bar, upstairs lounge :: Magichome ESP8266" device_area: "Lounge" # Allows the ESP device to be automatically linked to an 'Area' in Home Assistant. # Project Naming - project_name: "Magichome ESP8266" # Project details - project_version: "v1.0" # Project version denotes release of the YAML file, allowing checking of deployed vs latest version - - # Passwords & Secrets + project_name: "Magichome Single Colour V1.1 LED Controller.Generic ESP8266" # Project details. Manufacturer before the dot, device after the dot. + project_version: "v2.0" # Project version denotes release of the YAML file, allowing checking of deployed vs latest version + + # Passwords & Secrets (Unfortunately, you can't use substitutions inside secret names) api_key: !secret esp-api_key ota_pass: !secret esp-ota_pass - static_ip_address: !secret esp-breakfastbarleds_ip # Unfortunately, you can't use substitutions inside secret names + static_ip_address: !secret esp-breakfastbarleds_ip # CHANGE THIS + + # MQTT LOCAL Controls (This is just my local method of controlling via MQTT, I also use the built in method) + mqtt_local_device_name: "lounge-breakfastbarleds" mqtt_local_command_main_topic: !secret mqtt_local_command_main_topic mqtt_local_status_main_topic: !secret mqtt_local_status_main_topic - - # MQTT LOCAL Controls - mqtt_local_device_name: "lounge-breakfastbarleds" mqtt_local_command_topic: "${mqtt_local_command_main_topic}/${mqtt_local_device_name}" # Topic we will use to command this locally without HA mqtt_local_status_topic: "${mqtt_local_status_main_topic}/${mqtt_local_device_name}" # Topic we will use to view status locally without HA mqtt_local_device_command_ON: "ON" mqtt_local_device_command_OFF: "OFF" # Device Specific Settings - log_level: "NONE" # Define logging level: NONE, ERROR, WARN, INFO, DEBUG (default), VERBOSE, VERY_VERBOSE - update_interval: "20s" # Update time for general sensors, etc. - led_gamma: "1.2" # Gamma from 1.2-3 is sensible to normalise the LED fading vs PWM - minimum_led_output: "1" # % If at this value or below, we'll switch it completely off - maximum_led_output: "90" # % Maximum output; it is sometimes nice to limit the output for longevity or aesthetics - max_on_default_hours: "12" # The maximum time the LEDs will be on, in case they get left on. 0 = no automatic turn-off + log_level: "NONE" # Define logging level: NONE, ERROR, WARN, INFO, DEBUG (default), VERBOSE, VERY_VERBOSE + update_interval: "20s" # Update time for general sensors, etc. + led_gamma: "1.2" # Gamma from 1.2-3 is sensible to normalise the LED fading vs PWM + minimum_led_output: "1" # % If at this value or below, we'll switch it completely off + maximum_led_output: "90" # % Maximum output; it is sometimes nice to limit the output for longevity or aesthetics + max_on_default_hours: "24" # The maximum time the LEDs will be on, in case they get left on. 0 = no automatic turn-off + pwm_frequency: "1000hz" # PWM output Frequency. High enough to avoid audible/visible artifacts + + # Device Specific GPIO (so we can easily update for other devices) + device_status_led: GPIO02 # Can't really see if there is an LED + device_mosfet_out: GPIO12 + device_usr_button: GPIO04 # this is just a fake/safeish choice so it compiles with my generic yaml + device_fading_led: GPIO13 # this is just a fake/safeish choice so it compiles with my generic yaml ########################################################################################## # 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}" + + #### 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}" - #common_webportal: !include - # file: common/webportal_common.yaml + + #### MQTT #### common_mqtt: !include file: common/mqtt_common.yaml vars: local_device_name: "${device_name}" - #common_sntp: !include - # file: common/sntp_common.yaml - common_general_sensors: !include - file: common/sensors_common.yaml - #file: common/sensors_common_lite.yaml - vars: - local_friendly_name: "${friendly_name}" - local_update_interval: "${update_interval}" + + #### 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 CORE CONFIGURATION @@ -109,7 +138,8 @@ esphome: comment: "${description_comment}" area: "${device_area}" on_boot: - priority: 600 + # Run later so everything is initialised before we touch states. + priority: 200 then: # Keep the HA dropdown in sync with the stored mode - lambda: |- @@ -127,31 +157,12 @@ esphome: - lambda: 'id(ramp_switch_target_on) = true;' - script.execute: ramp_on_script - # Mode 1: Restore Brightness using a scripted ramp 0 -> last non-zero brightness + # Mode 1: Restore Brightness (deferred to avoid races) - if: condition: lambda: 'return id(restart_mode) == 1;' then: - - lambda: |- - float target = id(last_nonzero_brightness_pct); - if (target < 0.0f) target = 0.0f; - if (target > 100.0f) target = 100.0f; - const float minp = (float) id(min_brightness_pct); - const float maxp = (float) id(max_brightness_pct); - if (target > 0.0f && target < minp) target = minp; - if (target > maxp) target = maxp; - id(restore_target_pct) = target; // set scripted target - id(ramp_switch_target_on) = (target > 0.0f); - - if: - condition: - lambda: 'return id(restore_target_pct) > 0.0f;' - then: - - script.stop: ramp_off_script - - script.execute: ramp_to_target_script - else: - - light.turn_off: - id: mosfet_leds - transition_length: 0s + - script.execute: deferred_restore_brightness # Mode 2: Remain Off (no blip; stays off) - if: @@ -165,15 +176,19 @@ esphome: id: mosfet_leds transition_length: 0s - platformio_options: - build_unflags: - - -flto - build_flags: - - -fno-lto - - -Wl,--gc-sections - - -ffunction-sections - - -fdata-sections - - -DNDEBUG + # Boot complete: allow OFF handlers again + - lambda: 'id(booting) = false;' + +# Only if you want to play with build flags... +# platformio_options: +# build_unflags: +# - -flto +# build_flags: +# - -fno-lto +# - -Wl,--gc-sections +# - -ffunction-sections +# - -fdata-sections +# - -DNDEBUG ########################################################################################## # ESP PLATFORM AND FRAMEWORK @@ -185,10 +200,10 @@ esp8266: restore_from_flash: true # restore some values on reboot mdns: - disabled: false # Disabling will make the build file smaller (and it is still available via static IP) + disabled: false # Disabling will make the build file smaller (and device is still available via static IP) preferences: - flash_write_interval: 5min + flash_write_interval: 5sec # enough time to update values for reboots, but not enough to wear flash ########################################################################################## # GLOBAL VARIABLES @@ -209,7 +224,8 @@ globals: - id: max_on_hours type: int restore_value: true - initial_value: '${max_on_default_hours}' + initial_value: "${max_on_default_hours}" + # Default Fading Up Time (Selectable and will be retained) - id: ramp_up_ms # fade-in when turned ON type: int @@ -273,6 +289,16 @@ globals: type: float restore_value: false initial_value: '0.0' + # guards OFF persistence during startup caused by restore_mode: ALWAYS_OFF + - id: booting + type: bool + restore_value: false + initial_value: 'true' +# true only while we are performing a ramp-down that briefly turns the light ON to hit the floor + - id: ramping_for_off + type: bool + restore_value: false + initial_value: 'false' ########################################################################################## # LOGGER COMPONENT @@ -283,6 +309,15 @@ logger: level: "${log_level}" # INFO Level 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 (eg PZEM, Serial control) +######################################################################################### +# STATUS LED +# https://esphome.io/components/status_led.html +######################################################################################### +status_led: + pin: + number: ${device_status_led} + inverted: true + ########################################################################################## # MQTT COMMANDS # This adds device-specific MQTT command triggers to the common MQTT configuration. @@ -300,15 +335,6 @@ mqtt: then: - switch.turn_off: mosfet_ramp_switch -######################################################################################### -# STATUS LED -# https://esphome.io/components/status_led.html -######################################################################################### -status_led: - pin: - number: GPIO2 - inverted: true - ########################################################################################## # SWITCH COMPONENT # https://esphome.io/components/switch/ @@ -367,12 +393,18 @@ button: name: "${friendly_name} Fade Stop" icon: mdi:pause on_press: - # Stop any pending scripts (and their delayed turn_off) + # Stop any pending scripts (and their delayed actions) - script.stop: ramp_on_script - script.stop: ramp_off_script - # Cancel the light's transition by commanding the current level with 0 ms, - # but DO NOT change the ramp switch state/flag. + - script.stop: led_flash_up + - script.stop: led_flash_down + - output.turn_off: green_led_out - lambda: |- + // We are no longer ramping (up or down) + id(ramping_for_off) = false; + id(is_ramping) = false; + + // Freeze the light at its *current* level by overwriting the transition const auto &cv = id(mosfet_leds).current_values; if (cv.is_on()) { auto call = id(mosfet_leds).make_call(); @@ -382,6 +414,14 @@ button: call.perform(); } + // Persist the exact frozen level so "Restore Brightness" survives a reboot + if (cv.is_on()) { + float pct = cv.get_brightness() * 100.0f; + id(last_brightness_pct) = pct; + if (pct > 0.5f) id(last_nonzero_brightness_pct) = pct; + } + + ######################################################################################### # SELECT COMPONENT # https://esphome.io/components/select/index.html @@ -411,7 +451,37 @@ select: # BINARY SENSORS # https://esphome.io/components/binary_sensor/ ######################################################################################### -#binary_sensor: +binary_sensor: + - platform: gpio + id: btn_gpio12 + name: "${friendly_name} Button" + pin: + number: ${device_usr_button} + mode: + input: true + pullup: true + inverted: true + filters: + - delayed_on: 20ms + - delayed_off: 20ms + on_press: + - if: + condition: + lambda: 'return id(ramp_switch_target_on);' + then: + # Target is currently ON -> press should go OFF (start ramp-down) + - lambda: |- + id(ramp_switch_target_on) = false; + id(mosfet_ramp_switch).publish_state(false); // reflect in HA immediately + - script.stop: ramp_on_script + - script.execute: ramp_off_script + else: + # Target is currently OFF -> press should go ON (start ramp-up) + - lambda: |- + id(ramp_switch_target_on) = true; + id(mosfet_ramp_switch).publish_state(true); // reflect in HA immediately + - script.stop: ramp_off_script + - script.execute: ramp_on_script ########################################################################################## # SENSOR COMPONENT @@ -431,16 +501,22 @@ sensor: on_value: then: - lambda: |- - // Remember latest actual output (0..100) for "Restore Brightness" - id(last_brightness_pct) = x; - if (x > 0.5f) { - // Only persist when not ramping; always track a volatile copy - if (!id(is_ramping)) id(last_nonzero_brightness_pct) = x; - id(last_nonzero_tmp) = x; + const auto &cv_now = id(mosfet_leds).current_values; + + // Only persist exact last brightness while actually ON, + // and not during the OFF ramp's brief floor-kick. + if (cv_now.is_on()) { + if (x > 0.5f) { + if (!id(is_ramping)) id(last_nonzero_brightness_pct) = x; + id(last_nonzero_tmp) = x; + } + } else { + if (x > 0.5f) id(last_nonzero_tmp) = x; } + // If not suppressing sync, update the 0..100 slider only when its INT changes if (!id(suppress_slider_sync)) { - float actual = x; // actual % + float actual = x; float minp = (float) id(min_brightness_pct); float maxp = (float) id(max_brightness_pct); if (maxp <= minp) maxp = minp + 1.0f; @@ -464,7 +540,7 @@ sensor: lambda: |- const auto &cv = id(mosfet_leds).current_values; if (!cv.is_on()) return 0.0f; - const float lin = cv.get_brightness(); // 0..1 (linear brightness) + const float lin = cv.get_brightness(); // 0..1 linear brightness const float gamma = atof("${led_gamma}"); // parse substitution string -> float float pwm = powf(lin, gamma); // approx PWM duty after gamma if (pwm < 0.0f) pwm = 0.0f; @@ -481,8 +557,13 @@ sensor: output: - platform: esp8266_pwm id: mosfet_pwm - pin: GPIO12 - frequency: 1000 Hz # higher frequency helps reduce camera banding while filming + pin: ${device_mosfet_out} + frequency: ${pwm_frequency} + - platform: gpio + id: green_led_out # Green LED + pin: + number: ${device_fading_led} + inverted: false ########################################################################################## # LIGHT COMPONENT @@ -493,29 +574,48 @@ light: id: mosfet_leds name: "${friendly_name}" output: mosfet_pwm - restore_mode: ALWAYS_OFF + restore_mode: RESTORE_DEFAULT_OFF default_transition_length: 2s icon: mdi:led-strip-variant gamma_correct: "${led_gamma}" + + # ON: publish state, track intent, arm watchdog if configured on_turn_on: - mqtt.publish: topic: "${mqtt_local_status_topic}/light/state" payload: "${mqtt_local_device_command_ON}" retain: true - - lambda: 'id(ramp_switch_target_on) = true;' + - lambda: |- + // Do not flip "intent to ON" when we're inside an OFF ramp's floor-kick + if (!id(ramping_for_off)) { + id(ramp_switch_target_on) = true; + } - script.stop: max_on_watchdog - if: condition: lambda: 'return id(max_on_hours) > 0;' then: - script.execute: max_on_watchdog + + # OFF: publish state, track intent, clear watchdog, and persist OFF so Mode 1 will not restore + # Guarded by booting flag so the automatic OFF at boot from ALWAYS_OFF does not zero persistence. on_turn_off: - - mqtt.publish: - topic: "${mqtt_local_status_topic}/light/state" - payload: "${mqtt_local_device_command_OFF}" - retain: true - - lambda: 'id(ramp_switch_target_on) = false;' - - script.stop: max_on_watchdog + - if: + condition: + lambda: 'return !id(booting);' + then: + - mqtt.publish: + topic: "${mqtt_local_status_topic}/light/state" + payload: "${mqtt_local_device_command_OFF}" + retain: true + - lambda: 'id(ramp_switch_target_on) = false;' + - script.stop: max_on_watchdog + - lambda: |- + id(last_brightness_pct) = 0.0f; + id(last_set_pos) = 0; + id(led_output_set_pct).publish_state(0); + + # Cap brightness to max_brightness_pct without a visible step on_state: - lambda: |- const float cap = id(max_brightness_pct) / 100.0f; @@ -608,6 +708,8 @@ number: - lambda: |- id(ramp_switch_target_on) = false; id(led_output_set_pct).publish_state(0); + id(last_brightness_pct) = 0.0f; // persist OFF immediately + id(last_set_pos) = 0; - delay: 400ms - lambda: 'id(suppress_slider_sync) = false;' else: @@ -631,7 +733,19 @@ number: if (out_pct > maxp) out_pct = maxp; return out_pct / 100.0f; transition_length: 250ms - - lambda: 'id(ramp_switch_target_on) = true;' + - lambda: |- + id(ramp_switch_target_on) = true; + float pos = id(led_output_set_pct).state; // 1..100 + float minp = (float) id(min_brightness_pct); + float maxp = (float) id(max_brightness_pct); + if (maxp <= minp) maxp = minp + 1.0f; + float out_pct = minp + (pos * (maxp - minp) / 100.0f); + if (out_pct > maxp) out_pct = maxp; + id(last_brightness_pct) = out_pct; // persist exact target now + if (out_pct > 0.5f) { + id(last_nonzero_brightness_pct) = out_pct; + id(last_nonzero_tmp) = out_pct; + } - delay: 400ms - lambda: 'id(suppress_slider_sync) = false;' @@ -671,12 +785,43 @@ number: # Scripts can be executed nearly anywhere in your device configuration with a single call. ########################################################################################## script: + # Blink pattern while ramping UP: quick double-blink, pause, repeat + - id: led_flash_up + mode: restart + then: + - while: + condition: + lambda: 'return true;' + then: + - output.turn_on: green_led_out + - delay: 100ms + - output.turn_off: green_led_out + - delay: 100ms + - output.turn_on: green_led_out + - delay: 100ms + - output.turn_off: green_led_out + - delay: 400ms + # Blink pattern while ramping DOWN: steady slow blink + - id: led_flash_down + mode: restart + then: + - while: + condition: + lambda: 'return true;' + then: + - output.turn_on: green_led_out + - delay: 250ms + - output.turn_off: green_led_out + - delay: 250ms + # Script: ramp up from current level. Obey global max. - id: ramp_on_script mode: restart then: - - lambda: 'id(is_ramping) = true;' # mark ramping + - lambda: 'id(is_ramping) = true;' - script.stop: ramp_off_script + - script.stop: led_flash_down + - script.execute: led_flash_up # Ensure we start at at least the floor without a visible "pop". - if: condition: @@ -697,18 +842,14 @@ script: const auto &cv = id(mosfet_leds).current_values; const float floor = id(min_brightness_pct) / 100.0f; const float cap = id(max_brightness_pct) / 100.0f; - if (cap <= floor + 0.0005f) { id(last_ramp_ms) = 0; return (uint32_t) 0; } // guard degenerate case + if (cap <= floor + 0.0005f) { id(last_ramp_ms) = 0; return (uint32_t) 0; } - // IMPORTANT: when the light was OFF, treat current as 0 so we don't compute a 0ms jump to cap. float curr = cv.is_on() ? cv.get_brightness() : 0.0f; - // If we're below the floor (i.e. coming from OFF), we just did the "floor kick" above. - // Use full ramp_up_ms for a smooth 0 -> cap ramp. if (curr + 0.0005f < floor) { id(last_ramp_ms) = id(ramp_up_ms); return (uint32_t) id(last_ramp_ms); } - if (curr > cap) curr = cap; if (curr < floor) curr = floor; float frac = (cap - curr) / (cap - floor); @@ -717,20 +858,25 @@ script: id(last_ramp_ms) = (int) (id(ramp_up_ms) * frac); return (uint32_t) id(last_ramp_ms); - delay: !lambda 'return (uint32_t) id(last_ramp_ms);' + - script.stop: led_flash_up + - output.turn_off: green_led_out - lambda: |- - id(is_ramping) = false; // ramp complete + id(is_ramping) = false; const auto &cv = id(mosfet_leds).current_values; if (cv.is_on()) { float pct = cv.get_brightness() * 100.0f; - if (pct > 0.5f) id(last_nonzero_brightness_pct) = pct; // commit once + id(last_brightness_pct) = pct; // persist final level + if (pct > 0.5f) id(last_nonzero_brightness_pct) = pct; } # Script: ramp to a specific target (Restore Brightness path) - id: ramp_to_target_script mode: restart then: - - lambda: 'id(is_ramping) = true;' # mark ramping + - lambda: 'id(is_ramping) = true;' - script.stop: ramp_off_script + - script.stop: led_flash_down + - script.execute: led_flash_up # Ensure we start at the floor cleanly. - if: condition: @@ -757,12 +903,9 @@ script: float cap = id(restore_target_pct) / 100.0f; const float maxp = id(max_brightness_pct) / 100.0f; if (cap > maxp) cap = maxp; - if (cap <= floor + 0.0005f) { id(last_ramp_ms) = 0; return (uint32_t) 0; } // guard + if (cap <= floor + 0.0005f) { id(last_ramp_ms) = 0; return (uint32_t) 0; } - // Same OFF case handling: if off, treat curr as 0 so we don't collapse the ramp. float curr = cv.is_on() ? cv.get_brightness() : 0.0f; - - // Coming from OFF/below floor? We just did the "floor kick" above — use full ramp_up_ms. if (curr + 0.0005f < floor) { id(last_ramp_ms) = id(ramp_up_ms); return (uint32_t) id(last_ramp_ms); @@ -776,20 +919,27 @@ script: id(last_ramp_ms) = (int) (id(ramp_up_ms) * frac); return (uint32_t) id(last_ramp_ms); - delay: !lambda 'return (uint32_t) id(last_ramp_ms);' + - script.stop: led_flash_up + - output.turn_off: green_led_out - lambda: |- - id(is_ramping) = false; // ramp complete + id(is_ramping) = false; const auto &cv = id(mosfet_leds).current_values; if (cv.is_on()) { float pct = cv.get_brightness() * 100.0f; - if (pct > 0.5f) id(last_nonzero_brightness_pct) = pct; // commit once + id(last_brightness_pct) = pct; // persist final level + if (pct > 0.5f) id(last_nonzero_brightness_pct) = pct; } # Script: ramp down from current level to floor, then cleanly cut to OFF - id: ramp_off_script mode: restart then: - - lambda: 'id(is_ramping) = true;' # mark ramping + - lambda: |- + id(is_ramping) = true; + id(ramping_for_off) = true; - script.stop: ramp_on_script + - script.stop: led_flash_up + - script.execute: led_flash_down - light.turn_on: id: mosfet_leds brightness: !lambda 'return id(min_brightness_pct) / 100.0f;' @@ -799,7 +949,7 @@ script: float curr = cv.get_brightness(); if (curr < floor) curr = floor; const float denom = (1.0f - floor); - if (denom <= 0.0005f) { id(last_ramp_ms) = 0; return (uint32_t) 0; } // guard near-1.0 floor + if (denom <= 0.0005f) { id(last_ramp_ms) = 0; return (uint32_t) 0; } float frac = (curr - floor) / denom; if (frac < 0.0f) frac = 0.0f; if (frac > 1.0f) frac = 1.0f; @@ -810,12 +960,15 @@ script: id: mosfet_leds transition_length: 150ms - delay: 150ms + - script.stop: led_flash_down + - output.turn_off: green_led_out - lambda: |- - id(is_ramping) = false; // ramp complete - auto call = id(mosfet_leds).make_call(); - call.set_state(false); - call.set_brightness(id(max_brightness_pct) / 100.0f); - call.perform(); + id(is_ramping) = false; + id(ramping_for_off) = false; + id(last_brightness_pct) = 0.0f; // persist OFF for restore path + id(last_set_pos) = 0; + id(led_output_set_pct).publish_state(0); + - id: max_on_watchdog mode: restart then: @@ -833,3 +986,38 @@ script: id(mosfet_ramp_switch).publish_state(false); - script.stop: ramp_on_script - script.execute: ramp_off_script + + # Deferred restore to avoid early-boot races and ensure we apply the last saved level + - id: deferred_restore_brightness + mode: restart + then: + - delay: 5s # let preferences/globals load and light settle + - lambda: |- + // Use ONLY the exact last saved brightness. + float target = id(last_brightness_pct); + + // Clamp into [min..max] only if we are restoring a non-zero target + float minp = (float) id(min_brightness_pct); + float maxp = (float) id(max_brightness_pct); + if (target > 0.0f && target < minp) target = minp; + if (target > maxp) target = maxp; + + id(restore_target_pct) = target; + + - if: + condition: + lambda: 'return id(restore_target_pct) > 0.5f;' + then: + - lambda: |- + id(ramp_switch_target_on) = true; // reflect our intent + id(suppress_slider_sync) = true; + - script.stop: ramp_off_script + - script.execute: ramp_to_target_script + - delay: !lambda 'return (uint32_t) id(last_ramp_ms);' + - lambda: 'id(suppress_slider_sync) = false;' + else: + # Exact last was 0 -> remain OFF + - lambda: 'id(ramp_switch_target_on) = false;' + - light.turn_off: + id: mosfet_leds + transition_length: 0s diff --git a/esphome/esp-downstairskitchleds.yaml b/esphome/esp-downstairskitchleds.yaml index 1860fc1..9010e52 100644 --- a/esphome/esp-downstairskitchleds.yaml +++ b/esphome/esp-downstairskitchleds.yaml @@ -1,10 +1,9 @@ ########################################################################################## ########################################################################################## # Title: DOWNSTAIRS KITCHEN - OVER PANTRY LEDS -# Hardware: Sinilink MOSFET Board XY-WFMS (ESP8266) — sometimes listed as “XY-VFMS” -# https://devices.esphome.io/devices/Sinilink-XY-VFMS # Repo: https://home.fox.co.nz/gitea/zorruno/zorruno-homeassistant/src/branch/master/esphome/esp-downstairskitchleds.yaml # +# v2.0 - 2025-09-15 A bunch of fixes. # v1.5 - 2025-09-04 Adopt ramped "Restore Brightness" (0→last non-zero), avoid boot blip (ALWAYS_OFF + high-priority on_boot), # add flash-write coalescing (persist once at end of ramps), guards for edge cases, and flash_write_interval=5min # v1.4 - 2025-08-22 Improved power loss/on actions @@ -14,115 +13,131 @@ # v1.0 - 2025-08-17 First setup (and replacement of Tasmota) # # ------------------------------------------ -# DEVICE GPIO (Sinilink XY-WFMS) -# ------------------------------------------ -# GPIO02 Blue LED (used for ESPHome status) -# GPIO04 MOSFET output (0 V when switched) and Red LED -# GPIO12 Toggle button -# GPIO13 Green LED (used to display fading status) -# -# ------------------------------------------ -# OPERATION (as of v1.5) +# OPERATION (as of v2.0) # ------------------------------------------ # 1. General-purpose LED controller. -# 2. Designed for the Sinilink XY-WFMS board with a MOSFET output (claimed 5A, 5-36 V DC). -# 3. Global setting for MAX % output to extend LED life. -# 4. Minimum output setting; switches fully OFF at/below the minimum to avoid low-PWM flicker. -# 5. PWM frequency is set to 500 Hz by default. You can increase it, but higher values caused -# resets on this device. On ESP32 you can run much higher (~40 kHz). -# 6. Min/Max output settings are not exposed in Home Assistant/MQTT by default, but can be. +# 2. Designed for the Sinilink XY-WFMS board with a MOSFET output (claimed 5A, 5-36 V DC), but will suit similar mosfet boards. +# 3. (In SUBSTITUTIONS) setting for MAX % output to extend LED life (ie can ensure it is only ever 95%) +# 4. (In SUBSTITUTIONS) Minimum output setting; switches fully OFF at/below the minimum to avoid low-PWM flicker. +# 5. (In SUBSTITUTIONS) PWM frequency is set to 1kHz by default. You can increase it, but higher values caused +# resets on Sinilink device. On ESP32 you can run much higher (~40 kHz). +# 6. Min/Max output settings are not exposed in Home Assistant/MQTT by default, but could be. # With a 1 MB flash, space is tight and only minimal optimisation has been done so far. -# 7. PACKAGES include common items: network settings, diagnostic entities, MQTT, and SNTP (optional). -# 8. Default behaviour: +# 7. (PACKAGES) include common items: network settings, diagnostic entities, MQTT, and SNTP (optional). +# 8. Default behaviours, for recovery from lost power or reset: # - "Fade up to full": fade from floor to max on boot. -# - "Restore Brightness": fade from floor to the last non-zero brightness on boot (no on/off blip). +# - "Restore Brightness": fade from floor to the last non-zero brightness on boot (should be no on/off blip/flash). # - "Remain Off": stays off on boot. # 9. The green LED flashes while fading (different patterns for up/down). The red LED follows the -# output (it shares the MOSFET GPIO). +# output (it shares the MOSFET GPIO). (Obviously this depends on hardware) # 10. Fade timing scales with the configured values (proportionally when starting mid-brightness). -# 11. Useful 3D-printed case: https://cults3d.com/en/3d-model/tool/snapfit-enclosure-for-esp8266-sinilink-xy-wfms-5v-36v-mosfet-switch-module -# 12. Exposed in Home Assistant/MQTT: -# - Startup action +# 11. Exposed in Home Assistant/MQTT: +# - Startup action (On,Off,Restore) # - Fade Up / Fade Down / Fade Stop buttons -# - Fade Up/Down switch +# - Fade Up/Down toggle switch # - Normal On/Off switch (quick ramp up/down) # - Fade up/down times (0-60s) # - Output % (pre-gamma) and PWM % (post-gamma) -# - Output Set (1-100, respects min/max) -# - Device diagnostics (from the included package) +# - Output Set (1-100, respects min/max). This also changes with other output so reflects value. +# - Many device diagnostics (from the included 'diagnostics' package) # - Maximum 'on' time before automatic fade-down (1-48 h, 0 = no limit) +########################################################################################### +# Hardware: Sinilink MOSFET Board XY-WFMS (ESP8266) - sometimes listed as "XY-VFMS" +# https://devices.esphome.io/devices/Sinilink-XY-WFMS +# ------------------------------------------ +# DEVICE GPIO (Sinilink XY-WFMS) +# ------------------------------------------ +# GPIO02 Blue LED, active-low (used for ESPHome status) +# GPIO04 MOSFET output (0V when switched) and Red LED +# GPIO12 Toggle button +# GPIO13 Green LED (used to display fading status) +# +# 3D-printed case for Sinilink XY-WFMS +# https://cults3d.com/en/3d-model/tool/snapfit-enclosure-for-esp8266-sinilink-xy-wfms-5v-36v-mosfet-switch-module # ########################################################################################## ########################################################################################## - ########################################################################################## # 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-downstairskitchleds" + device_name: "esp-downstairskitchleds" # yaml file should be device_name.yaml friendly_name: "Downstairs Kitchen LEDs" description_comment: "Downstairs Kitchen Over Pantry LEDs :: Sinilink XY-WFMS" device_area: "Downstairs Kitchen" # Allows the ESP device to be automatically linked to an 'Area' in Home Assistant. # Project Naming - project_name: "Sinilink.XY-WFMS" # Project details - project_version: "v1.5" # Project version denotes release of the YAML file, allowing checking of deployed vs latest version - - # Passwords & Secrets + project_name: "Sinilink.XY-WFMS" # Project details. Manufacturer before the dot, device after the dot. + project_version: "v2.0" # Project version denotes release of the YAML file, allowing checking of deployed vs latest version + + # Passwords & Secrets (Unfortunately, you can't use substitutions inside secret names) api_key: !secret esp-api_key ota_pass: !secret esp-ota_pass - static_ip_address: !secret esp-downstairskitchleds_ip # Unfortunately, you can't use substitutions inside secret names - mqtt_local_command_main_topic: !secret mqtt_local_command_main_topic - mqtt_local_status_main_topic: !secret mqtt_local_status_main_topic + static_ip_address: !secret esp-downstairskitchleds_ip # CHANGE THIS # MQTT LOCAL Controls mqtt_local_device_name: "downstairskitchen-pantryleds" + mqtt_local_command_main_topic: !secret mqtt_local_command_main_topic + mqtt_local_status_main_topic: !secret mqtt_local_status_main_topic mqtt_local_command_topic: "${mqtt_local_command_main_topic}/${mqtt_local_device_name}" # Topic we will use to command this locally without HA mqtt_local_status_topic: "${mqtt_local_status_main_topic}/${mqtt_local_device_name}" # Topic we will use to view status locally without HA mqtt_local_device_command_ON: "ON" mqtt_local_device_command_OFF: "OFF" # Device Specific Settings - log_level: "NONE" # Define logging level: NONE, ERROR, WARN, INFO, DEBUG (default), VERBOSE, VERY_VERBOSE - update_interval: "20s" # Update time for general sensors, etc. - led_gamma: "1.2" # Gamma from 1.2-3 is sensible to normalise the LED fading vs PWM - minimum_led_output: "1" # % If at this value or below, we'll switch it completely off - maximum_led_output: "90" # % Maximum output; it is sometimes nice to limit the output for longevity or aesthetics - max_on_default_hours: "6" # The maximum time the LEDs will be on, in case they get left on. 0 = no automatic turn-off + log_level: "NONE" # Define logging level: NONE, ERROR, WARN, INFO, DEBUG (default), VERBOSE, VERY_VERBOSE + update_interval: "20s" # Update time for general sensors, etc. + led_gamma: "1.2" # Gamma from 1.2-3 is sensible to normalise the LED fading vs PWM + minimum_led_output: "1" # % If at this value or below, we'll switch it completely off + maximum_led_output: "90" # % Maximum output; it is sometimes nice to limit the output for longevity or aesthetics + max_on_default_hours: "24" # The maximum time the LEDs will be on, in case they get left on. 0 = no automatic turn-off + pwm_frequency: "1000hz" # PWM output Frequency. High enough to avoid audible/visible artifacts + + # Device Specific GPIO (so we can easily update for other devices) + device_status_led: GPIO02 + device_mosfet_out: GPIO04 + device_usr_button: GPIO12 + device_fading_led: GPIO13 ########################################################################################## # 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}" + + #### 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}" - #common_webportal: !include - # file: common/webportal_common.yaml + + #### MQTT #### common_mqtt: !include file: common/mqtt_common.yaml vars: local_device_name: "${device_name}" - #common_sntp: !include - # file: common/sntp_common.yaml - common_general_sensors: !include - file: common/sensors_common.yaml - #file: common/sensors_common_lite.yaml - vars: - local_friendly_name: "${friendly_name}" - local_update_interval: "${update_interval}" + + #### 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 CORE CONFIGURATION @@ -134,7 +149,8 @@ esphome: comment: "${description_comment}" area: "${device_area}" on_boot: - priority: 600 + # Run later so everything is initialised before we touch states. + priority: 200 then: # Keep the HA dropdown in sync with the stored mode - lambda: |- @@ -152,34 +168,12 @@ esphome: - lambda: 'id(ramp_switch_target_on) = true;' - script.execute: ramp_on_script - # Mode 1: Restore Brightness using a scripted ramp 0 -> last non-zero brightness + # Mode 1: Restore Brightness (deferred to avoid races) - if: condition: lambda: 'return id(restart_mode) == 1;' then: - - lambda: |- - float target = id(last_nonzero_brightness_pct); - if (target < 0.0f) target = 0.0f; - if (target > 100.0f) target = 100.0f; - - // Gently clamp to min/max caps to avoid an immediate post-on_state correction. - const float minp = (float) id(min_brightness_pct); - const float maxp = (float) id(max_brightness_pct); - if (target > 0.0f && target < minp) target = minp; - if (target > maxp) target = maxp; - - id(restore_target_pct) = target; // set scripted target - id(ramp_switch_target_on) = (target > 0.0f); - - if: - condition: - lambda: 'return id(restore_target_pct) > 0.0f;' - then: - - script.stop: ramp_off_script - - script.execute: ramp_to_target_script - else: - - light.turn_off: - id: mosfet_leds - transition_length: 0s + - script.execute: deferred_restore_brightness # Mode 2: Remain Off (no blip; stays off) - if: @@ -193,15 +187,19 @@ esphome: id: mosfet_leds transition_length: 0s - platformio_options: - build_unflags: - - -flto - build_flags: - - -fno-lto - - -Wl,--gc-sections - - -ffunction-sections - - -fdata-sections - - -DNDEBUG + # Boot complete: allow OFF handlers again + - lambda: 'id(booting) = false;' + +# Only if you want to play with build flags... +# platformio_options: +# build_unflags: +# - -flto +# build_flags: +# - -fno-lto +# - -Wl,--gc-sections +# - -ffunction-sections +# - -fdata-sections +# - -DNDEBUG ########################################################################################## # ESP PLATFORM AND FRAMEWORK @@ -212,14 +210,11 @@ esp8266: board: esp01_1m restore_from_flash: true # restore some values on reboot -#preferences: -# flash_write_interval: 5min - mdns: - disabled: false # Disabling will make the build file smaller (and it is still available via static IP) + disabled: false # Disabling will make the build file smaller (and device is still available via static IP) preferences: - flash_write_interval: 5min + flash_write_interval: 5sec # enough time to update values for reboots, but not enough to wear flash ########################################################################################## # GLOBAL VARIABLES @@ -240,7 +235,8 @@ globals: - id: max_on_hours type: int restore_value: true - initial_value: '${max_on_default_hours}' + initial_value: "${max_on_default_hours}" + # Default Fading Up Time (Selectable and will be retained) - id: ramp_up_ms # fade-in when turned ON type: int @@ -304,6 +300,16 @@ globals: type: float restore_value: false initial_value: '0.0' + # guards OFF persistence during startup caused by restore_mode: ALWAYS_OFF + - id: booting + type: bool + restore_value: false + initial_value: 'true' +# true only while we are performing a ramp-down that briefly turns the light ON to hit the floor + - id: ramping_for_off + type: bool + restore_value: false + initial_value: 'false' ########################################################################################## # LOGGER COMPONENT @@ -314,6 +320,15 @@ logger: level: "${log_level}" # INFO Level 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 (eg PZEM, Serial control) +######################################################################################### +# STATUS LED +# https://esphome.io/components/status_led.html +######################################################################################### +status_led: + pin: + number: ${device_status_led} + inverted: true + ########################################################################################## # MQTT COMMANDS # This adds device-specific MQTT command triggers to the common MQTT configuration. @@ -331,17 +346,6 @@ mqtt: then: - switch.turn_off: mosfet_ramp_switch -######################################################################################### -# STATUS LED -# https://esphome.io/components/status_led.html -######################################################################################### -# SINILINK: Status LED Blue LED on GPIO2, active-low -######################################################################################### -status_led: - pin: - number: GPIO2 - inverted: true - ########################################################################################## # SWITCH COMPONENT # https://esphome.io/components/switch/ @@ -400,15 +404,18 @@ button: name: "${friendly_name} Fade Stop" icon: mdi:pause on_press: - # Stop any pending scripts (and their delayed turn_off) + # Stop any pending scripts (and their delayed actions) - script.stop: ramp_on_script - script.stop: ramp_off_script - script.stop: led_flash_up - script.stop: led_flash_down - output.turn_off: green_led_out - # Cancel the light's transition by commanding the current level with 0 ms, - # but DO NOT change the ramp switch state/flag. - lambda: |- + // We are no longer ramping (up or down) + id(ramping_for_off) = false; + id(is_ramping) = false; + + // Freeze the light at its *current* level by overwriting the transition const auto &cv = id(mosfet_leds).current_values; if (cv.is_on()) { auto call = id(mosfet_leds).make_call(); @@ -418,6 +425,14 @@ button: call.perform(); } + // Persist the exact frozen level so "Restore Brightness" survives a reboot + if (cv.is_on()) { + float pct = cv.get_brightness() * 100.0f; + id(last_brightness_pct) = pct; + if (pct > 0.5f) id(last_nonzero_brightness_pct) = pct; + } + + ######################################################################################### # SELECT COMPONENT # https://esphome.io/components/select/index.html @@ -452,7 +467,7 @@ binary_sensor: id: btn_gpio12 name: "${friendly_name} Button" pin: - number: GPIO12 + number: ${device_usr_button} mode: input: true pullup: true @@ -465,14 +480,14 @@ binary_sensor: condition: lambda: 'return id(ramp_switch_target_on);' then: - # Target is currently ON → press should go OFF (start ramp-down) + # Target is currently ON -> press should go OFF (start ramp-down) - lambda: |- id(ramp_switch_target_on) = false; id(mosfet_ramp_switch).publish_state(false); // reflect in HA immediately - script.stop: ramp_on_script - script.execute: ramp_off_script else: - # Target is currently OFF → press should go ON (start ramp-up) + # Target is currently OFF -> press should go ON (start ramp-up) - lambda: |- id(ramp_switch_target_on) = true; id(mosfet_ramp_switch).publish_state(true); // reflect in HA immediately @@ -497,17 +512,22 @@ sensor: on_value: then: - lambda: |- - // Remember latest actual output (0..100) for "Restore Brightness" - id(last_brightness_pct) = x; - if (x > 0.5f) { - // Only persist when not ramping; always track a volatile copy - if (!id(is_ramping)) id(last_nonzero_brightness_pct) = x; - id(last_nonzero_tmp) = x; + const auto &cv_now = id(mosfet_leds).current_values; + + // Only persist exact last brightness while actually ON, + // and not during the OFF ramp's brief floor-kick. + if (cv_now.is_on()) { + if (x > 0.5f) { + if (!id(is_ramping)) id(last_nonzero_brightness_pct) = x; + id(last_nonzero_tmp) = x; + } + } else { + if (x > 0.5f) id(last_nonzero_tmp) = x; } // If not suppressing sync, update the 0..100 slider only when its INT changes if (!id(suppress_slider_sync)) { - float actual = x; // actual % + float actual = x; float minp = (float) id(min_brightness_pct); float maxp = (float) id(max_brightness_pct); if (maxp <= minp) maxp = minp + 1.0f; @@ -531,7 +551,7 @@ sensor: lambda: |- const auto &cv = id(mosfet_leds).current_values; if (!cv.is_on()) return 0.0f; - const float lin = cv.get_brightness(); // 0..1 (linear brightness) + const float lin = cv.get_brightness(); // 0..1 linear brightness const float gamma = atof("${led_gamma}"); // parse substitution string -> float float pwm = powf(lin, gamma); // approx PWM duty after gamma if (pwm < 0.0f) pwm = 0.0f; @@ -548,12 +568,12 @@ sensor: output: - platform: esp8266_pwm id: mosfet_pwm - pin: GPIO4 - frequency: 1000 Hz # high frequency to avoid audible/visible artifacts + pin: ${device_mosfet_out} + frequency: ${pwm_frequency} - platform: gpio id: green_led_out # Green LED pin: - number: GPIO13 + number: ${device_fading_led} inverted: false ########################################################################################## @@ -565,29 +585,48 @@ light: id: mosfet_leds name: "${friendly_name}" output: mosfet_pwm - restore_mode: ALWAYS_OFF + restore_mode: RESTORE_DEFAULT_OFF default_transition_length: 2s icon: mdi:led-strip-variant gamma_correct: "${led_gamma}" + + # ON: publish state, track intent, arm watchdog if configured on_turn_on: - mqtt.publish: topic: "${mqtt_local_status_topic}/light/state" payload: "${mqtt_local_device_command_ON}" retain: true - - lambda: 'id(ramp_switch_target_on) = true;' + - lambda: |- + // Do not flip "intent to ON" when we're inside an OFF ramp's floor-kick + if (!id(ramping_for_off)) { + id(ramp_switch_target_on) = true; + } - script.stop: max_on_watchdog - if: condition: lambda: 'return id(max_on_hours) > 0;' then: - script.execute: max_on_watchdog + + # OFF: publish state, track intent, clear watchdog, and persist OFF so Mode 1 will not restore + # Guarded by booting flag so the automatic OFF at boot from ALWAYS_OFF does not zero persistence. on_turn_off: - - mqtt.publish: - topic: "${mqtt_local_status_topic}/light/state" - payload: "${mqtt_local_device_command_OFF}" - retain: true - - lambda: 'id(ramp_switch_target_on) = false;' - - script.stop: max_on_watchdog + - if: + condition: + lambda: 'return !id(booting);' + then: + - mqtt.publish: + topic: "${mqtt_local_status_topic}/light/state" + payload: "${mqtt_local_device_command_OFF}" + retain: true + - lambda: 'id(ramp_switch_target_on) = false;' + - script.stop: max_on_watchdog + - lambda: |- + id(last_brightness_pct) = 0.0f; + id(last_set_pos) = 0; + id(led_output_set_pct).publish_state(0); + + # Cap brightness to max_brightness_pct without a visible step on_state: - lambda: |- const float cap = id(max_brightness_pct) / 100.0f; @@ -680,6 +719,8 @@ number: - lambda: |- id(ramp_switch_target_on) = false; id(led_output_set_pct).publish_state(0); + id(last_brightness_pct) = 0.0f; // persist OFF immediately + id(last_set_pos) = 0; - delay: 400ms - lambda: 'id(suppress_slider_sync) = false;' else: @@ -703,7 +744,19 @@ number: if (out_pct > maxp) out_pct = maxp; return out_pct / 100.0f; transition_length: 250ms - - lambda: 'id(ramp_switch_target_on) = true;' + - lambda: |- + id(ramp_switch_target_on) = true; + float pos = id(led_output_set_pct).state; // 1..100 + float minp = (float) id(min_brightness_pct); + float maxp = (float) id(max_brightness_pct); + if (maxp <= minp) maxp = minp + 1.0f; + float out_pct = minp + (pos * (maxp - minp) / 100.0f); + if (out_pct > maxp) out_pct = maxp; + id(last_brightness_pct) = out_pct; // persist exact target now + if (out_pct > 0.5f) { + id(last_nonzero_brightness_pct) = out_pct; + id(last_nonzero_tmp) = out_pct; + } - delay: 400ms - lambda: 'id(suppress_slider_sync) = false;' @@ -776,7 +829,7 @@ script: - id: ramp_on_script mode: restart then: - - lambda: 'id(is_ramping) = true;' # mark ramping + - lambda: 'id(is_ramping) = true;' - script.stop: ramp_off_script - script.stop: led_flash_down - script.execute: led_flash_up @@ -800,13 +853,10 @@ script: const auto &cv = id(mosfet_leds).current_values; const float floor = id(min_brightness_pct) / 100.0f; const float cap = id(max_brightness_pct) / 100.0f; - if (cap <= floor + 0.0005f) { id(last_ramp_ms) = 0; return (uint32_t) 0; } // guard degenerate case + if (cap <= floor + 0.0005f) { id(last_ramp_ms) = 0; return (uint32_t) 0; } - // IMPORTANT: when the light was OFF, treat current as 0 so we don't compute a 0ms jump to cap. float curr = cv.is_on() ? cv.get_brightness() : 0.0f; - // If we're below the floor (i.e. coming from OFF), we just did the "floor kick" above. - // Use full ramp_up_ms for a smooth 0 -> cap ramp. if (curr + 0.0005f < floor) { id(last_ramp_ms) = id(ramp_up_ms); return (uint32_t) id(last_ramp_ms); @@ -822,18 +872,19 @@ script: - script.stop: led_flash_up - output.turn_off: green_led_out - lambda: |- - id(is_ramping) = false; // ramp complete + id(is_ramping) = false; const auto &cv = id(mosfet_leds).current_values; if (cv.is_on()) { float pct = cv.get_brightness() * 100.0f; - if (pct > 0.5f) id(last_nonzero_brightness_pct) = pct; // commit once + id(last_brightness_pct) = pct; // persist final level + if (pct > 0.5f) id(last_nonzero_brightness_pct) = pct; } # Script: ramp to a specific target (Restore Brightness path) - id: ramp_to_target_script mode: restart then: - - lambda: 'id(is_ramping) = true;' # mark ramping + - lambda: 'id(is_ramping) = true;' - script.stop: ramp_off_script - script.stop: led_flash_down - script.execute: led_flash_up @@ -863,12 +914,9 @@ script: float cap = id(restore_target_pct) / 100.0f; const float maxp = id(max_brightness_pct) / 100.0f; if (cap > maxp) cap = maxp; - if (cap <= floor + 0.0005f) { id(last_ramp_ms) = 0; return (uint32_t) 0; } // guard + if (cap <= floor + 0.0005f) { id(last_ramp_ms) = 0; return (uint32_t) 0; } - // Same OFF case handling: if off, treat curr as 0 so we don't collapse the ramp. float curr = cv.is_on() ? cv.get_brightness() : 0.0f; - - // Coming from OFF/below floor? We just did the "floor kick" above — use full ramp_up_ms. if (curr + 0.0005f < floor) { id(last_ramp_ms) = id(ramp_up_ms); return (uint32_t) id(last_ramp_ms); @@ -885,18 +933,21 @@ script: - script.stop: led_flash_up - output.turn_off: green_led_out - lambda: |- - id(is_ramping) = false; // ramp complete + id(is_ramping) = false; const auto &cv = id(mosfet_leds).current_values; if (cv.is_on()) { float pct = cv.get_brightness() * 100.0f; - if (pct > 0.5f) id(last_nonzero_brightness_pct) = pct; // commit once + id(last_brightness_pct) = pct; // persist final level + if (pct > 0.5f) id(last_nonzero_brightness_pct) = pct; } # Script: ramp down from current level to floor, then cleanly cut to OFF - id: ramp_off_script mode: restart then: - - lambda: 'id(is_ramping) = true;' # mark ramping + - lambda: |- + id(is_ramping) = true; + id(ramping_for_off) = true; - script.stop: ramp_on_script - script.stop: led_flash_up - script.execute: led_flash_down @@ -909,7 +960,7 @@ script: float curr = cv.get_brightness(); if (curr < floor) curr = floor; const float denom = (1.0f - floor); - if (denom <= 0.0005f) { id(last_ramp_ms) = 0; return (uint32_t) 0; } // guard near-1.0 floor + if (denom <= 0.0005f) { id(last_ramp_ms) = 0; return (uint32_t) 0; } float frac = (curr - floor) / denom; if (frac < 0.0f) frac = 0.0f; if (frac > 1.0f) frac = 1.0f; @@ -923,11 +974,12 @@ script: - script.stop: led_flash_down - output.turn_off: green_led_out - lambda: |- - id(is_ramping) = false; // ramp complete - auto call = id(mosfet_leds).make_call(); - call.set_state(false); - call.set_brightness(id(max_brightness_pct) / 100.0f); - call.perform(); + id(is_ramping) = false; + id(ramping_for_off) = false; + id(last_brightness_pct) = 0.0f; // persist OFF for restore path + id(last_set_pos) = 0; + id(led_output_set_pct).publish_state(0); + - id: max_on_watchdog mode: restart then: @@ -945,3 +997,38 @@ script: id(mosfet_ramp_switch).publish_state(false); - script.stop: ramp_on_script - script.execute: ramp_off_script + + # Deferred restore to avoid early-boot races and ensure we apply the last saved level + - id: deferred_restore_brightness + mode: restart + then: + - delay: 5s # let preferences/globals load and light settle + - lambda: |- + // Use ONLY the exact last saved brightness. + float target = id(last_brightness_pct); + + // Clamp into [min..max] only if we are restoring a non-zero target + float minp = (float) id(min_brightness_pct); + float maxp = (float) id(max_brightness_pct); + if (target > 0.0f && target < minp) target = minp; + if (target > maxp) target = maxp; + + id(restore_target_pct) = target; + + - if: + condition: + lambda: 'return id(restore_target_pct) > 0.5f;' + then: + - lambda: |- + id(ramp_switch_target_on) = true; // reflect our intent + id(suppress_slider_sync) = true; + - script.stop: ramp_off_script + - script.execute: ramp_to_target_script + - delay: !lambda 'return (uint32_t) id(last_ramp_ms);' + - lambda: 'id(suppress_slider_sync) = false;' + else: + # Exact last was 0 -> remain OFF + - lambda: 'id(ramp_switch_target_on) = false;' + - light.turn_off: + id: mosfet_leds + transition_length: 0s diff --git a/esphome/esp-entmulti.yaml b/esphome/esp-entmulti.yaml index 229e2fa..16ba455 100644 --- a/esphome/esp-entmulti.yaml +++ b/esphome/esp-entmulti.yaml @@ -49,29 +49,57 @@ substitutions: # https://esphome.io/components/packages.html ########################################################################################## packages: + ################################################## + # MANDATORY packages (won't compile without them!) + ################################################## + + #### 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}" + + ################################################## + # OPTIONAL packages (comment if you don't want them) + ################################################## + + #### 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}" + + #### WEB PORTAL #### #common_webportal: !include # file: common/webportal_common.yaml + + #### MQTT #### common_mqtt: !include file: common/mqtt_common.yaml vars: local_device_name: "${device_name}" - common_sntp: !include - file: common/sntp_common.yaml + + #### SNTP (Only use if you want/need accurate timeclocks) #### + #common_sntp: !include + # file: common/sntp_common.yaml + + #### DIAGNOSTICS Lite Sensors #### + # Option of everything, or a lite version + common_lite_sensors: !include + file: common/sensors_common_lite.yaml + + #### DIAGNOSTICS General Sensors #### + # Option of everything, or a lite version common_general_sensors: !include file: common/sensors_common.yaml - vars: - local_friendly_name: "${friendly_name}" - local_update_interval: "${update_interval}" + + #### DEBUGGING Sensors #### + # A count of various resets + #common_resetcount_debugging: !include + # file: common/resetcount_common.yaml ############################################# # ESPHome diff --git a/esphome/esp-entrancelightswitch.yaml b/esphome/esp-entrancelightswitch.yaml new file mode 100644 index 0000000..43bfdad --- /dev/null +++ b/esphome/esp-entrancelightswitch.yaml @@ -0,0 +1,181 @@ +########################################################################################## +########################################################################################## +# Main Entrance LIGHTSWITCH +# V3.7 2025-09-15 upload to this lightswitch from copy +# +########################################################################################## +# Zemismart KS-811 Double push button +# pinout/schematic https://community.home-assistant.io/t/zemismart-ks-811-working-with-esphome/ +# +# NOTES +# - Light switch with 2 sets of lights +# +########################################################################################## +########################################################################################## + +########################################################################################## +# 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-entrancelightswitch" + friendly_name: "Main Entrance Lightswitch (2)" + description_comment: "Main Entrance lightswitch using a Zemismart KS-811 Double Push Button. Main Lights (1), Bench Lights (2)" + device_area: "Kitchen" # Allows ESP device to be automatically linked to an 'Area' in Home Assistant. + + # Project Naming + project_name: "Zemismart Technologies.KS-811 Double" # Project Details + project_version: "v3.7" # Project V denotes release of yaml file, allowing checking of deployed vs latest version + + # 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-entrancelightswitch_ip + #mqtt_local_command_main_topic: !secret mqtt_local_command_main_topic + #mqtt_local_status_main_topic: !secret mqtt_local_status_main_topic + + # Device Settings + #relay_icon: "mdi:lightbulb-group" + log_level: "INFO" # Define logging level: NONE, ERROR, WARN, INFO, DEBUG (Default), VERBOSE, VERY_VERBOSE + update_interval: "60s" # update time for for general sensors etc + + # MQTT LOCAL Controls + #mqtt_device_name: "bedroom2-lights" + #mqtt_local_command_topic: "${mqtt_local_command_main_topic}/${mqtt_device_name}" # Topic we will use to command this locally without HA + #mqtt_local_status_topic: "${mqtt_local_status_main_topic}/${mqtt_device_name}" # Topic we will use to view status locally without HA + + # Switch Naming + switch_1_name: "Entrance Lights" + switch_2_name: "External Light Controller" #permanently on, and pushbutton sends MQTT + #switch_3_name: "Nil" + +######################################################################################### +# 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_ota_pass: "${ota_pass}" + common_api: !include + file: common/api_common.yaml + vars: + local_api_key: "${api_key}" + #common_webportal: !include + # file: common/webportal_common.yaml + common_mqtt: !include + file: common/mqtt_common.yaml + vars: + local_device_name: "${device_name}" + common_sntp: !include + file: common/sntp_common.yaml + common_general_sensors: !include + file: common/sensors_common.yaml + vars: + local_friendly_name: "${friendly_name}" + local_update_interval: "${update_interval}" + +######################################################################################### +# 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: + priority: 200 + then: + - switch.turn_on: Relay_2 # Permanently on + +######################################################################################### +# ESP Platform and Framework +# https://esphome.io/components/esp32.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 + +######################################################################################### +# ESPHome Logging Enable +# https://esphome.io/components/logger.html +############################################# +logger: + level: "${log_level}" #INFO Level 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 (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: yes + +######################################################################################### +# BINARY SENSORS +# https://esphome.io/components/binary_sensor/ +######################################################################################### +binary_sensor: + - platform: gpio + pin: + number: GPIO16 + mode: INPUT + inverted: True + name: "Button 1: ${switch_1_name}" + on_press: + - switch.toggle: Relay_1 + + - platform: gpio + pin: + number: GPIO05 + mode: INPUT + inverted: True + name: "Button 2: ${switch_2_name}" +# on_press: +# - switch.toggle: Relay_2 + +# KS-811-2 is a double only +# - platform: gpio +# pin: +# number: GPIO4 +# mode: INPUT +# inverted: True +# name: "Button 3: ${switch_3_name}" +# on_press: +# - switch.toggle: Relay_3 + +######################################################################################### +# SWITCH COMPONENT +# https://esphome.io/components/switch/ +######################################################################################### +switch: + - platform: gpio + name: "Relay 1: ${switch_1_name}" + pin: GPIO13 + id: Relay_1 + + - platform: gpio + name: "Relay 2: ${switch_2_name}" + pin: GPIO12 + id: Relay_2 + +# KS-811-2 is a double only +# - platform: gpio +# name: "Relay 3: ${switch_3_name}" +# pin: GPIO14 +# id: Relay_3 + + diff --git a/esphome/esp-laundrylights.yaml b/esphome/esp-laundrylights.yaml index cf391d7..6fe56f9 100644 --- a/esphome/esp-laundrylights.yaml +++ b/esphome/esp-laundrylights.yaml @@ -34,18 +34,18 @@ substitutions: 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-laundrylights_ip - #mqtt_local_command_main_topic: !secret mqtt_local_command_main_topic - #mqtt_local_status_main_topic: !secret mqtt_local_status_main_topic + 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: "INFO" # Define logging level: NONE, ERROR, WARN, INFO, DEBUG (Default), VERBOSE, VERY_VERBOSE update_interval: "60s" # update time for for general sensors etc - # MQTT Controls - #mqtt_device_name: "bedroom2-ceilingfan" - #mqtt_local_command_topic: "${mqtt_local_command_main_topic}/${mqtt_device_name}" # Topic we will use to command this locally without HA - #mqtt_local_status_topic: "${mqtt_local_status_main_topic}/${mqtt_device_name}" # Topic we will use to view status locally without HA + # MQTT REMOTE Controls + mqtt_remote1_device_name: "3dprinter-power" + mqtt_remote1_command_topic: "${mqtt_command_main_topic}/${mqtt_remote1_device_name}" # Topic we will use to command this locally without HA + mqtt_remote1_status_topic: "${mqtt_status_main_topic}/${mqtt_remote1_device_name}" # Topic we will use to view status locally without HA # Switch Naming switch_1_name: "Laundry Lights" @@ -58,29 +58,45 @@ substitutions: # https://esphome.io/components/packages.html ########################################################################################## packages: + ################################################## + # MANDATORY packages (won't compile without them!) + ################################################## + + #### 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}" + + ################################################## + # OPTIONAL packages (comment if you don't want them) + ################################################## + + #### 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}" - #common_webportal: !include - # file: common/webportal_common.yaml + + #### MQTT #### common_mqtt: !include file: common/mqtt_common.yaml vars: local_device_name: "${device_name}" - common_sntp: !include - file: common/sntp_common.yaml - common_general_sensors: !include - file: common/sensors_common.yaml - vars: - local_friendly_name: "${friendly_name}" - local_update_interval: "${update_interval}" + + #### 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 @@ -127,6 +143,20 @@ status_led: number: GPIO2 inverted: yes +######################################################################################### +# GLOBAL VARIABLES +# +######################################################################################### +globals: + - id: remote1_state + type: bool + restore_value: no + initial_value: 'false' + - id: remote1_known + type: bool + restore_value: no + initial_value: 'false' + ######################################################################################### # BINARY SENSORS # https://esphome.io/components/binary_sensor/ @@ -145,20 +175,76 @@ binary_sensor: pin: number: GPIO05 mode: INPUT - inverted: True + inverted: true name: "Button 2: ${switch_2_name}" - on_press: - - switch.toggle: Relay_2 + on_multi_click: + # SINGLE CLICK: quick tap -> toggle Relay_2 + - timing: + - ON for at most 0.5s + - OFF for at least 0.5s + then: + - if: + condition: + lambda: 'return id(Relay_2).state;' + then: + - switch.turn_off: Relay_2 + else: + - switch.turn_on: Relay_2 + # HOLD: pressed for 2s or more -> toggle remote; default to ON if unknown + - timing: + - ON for at least 2s + then: + - if: + condition: + lambda: 'return id(remote1_known) && id(remote1_state);' # known AND currently ON + then: + - mqtt.publish: + topic: "${mqtt_remote1_command_topic}" + payload: "OFF" + retain: false + else: + - mqtt.publish: + topic: "${mqtt_remote1_command_topic}" + payload: "ON" + retain: false -# KS-811-2 is a double only -# - platform: gpio -# pin: -# number: GPIO4 -# mode: INPUT -# inverted: True -# name: "Button 3: ${switch_3_name}" -# on_press: -# - switch.toggle: Relay_3 +######################################################################################### +# TEXT SENSORS +# +######################################################################################### +text_sensor: + - platform: mqtt_subscribe + id: remote1_status + internal: true + topic: "${mqtt_remote1_status_topic}" + on_value: + then: + - lambda: |- + // copy, trim whitespace, uppercase + std::string s = x; + // trim leading/trailing whitespace + auto notspace = [](int ch){ return !std::isspace(ch); }; + s.erase(s.begin(), std::find_if(s.begin(), s.end(), notspace)); + s.erase(std::find_if(s.rbegin(), s.rend(), notspace).base(), s.end()); + for (auto &c : s) c = (char)toupper(c); + + bool recognized = false; + if (s == "ON" || s == "1" || s == "TRUE") { + id(remote1_state) = true; + id(remote1_known) = true; + recognized = true; + } else if (s == "OFF" || s == "0" || s == "FALSE") { + id(remote1_state) = false; + id(remote1_known) = true; + recognized = true; + } else { + // unknown payload (e.g. "ONLINE", "OFFLINE", JSON, etc.) + id(remote1_known) = false; + } + ESP_LOGI("remote1_status", "Parsed status='%s' -> known=%s state=%s", + s.c_str(), + id(remote1_known) ? "true" : "false", + id(remote1_state) ? "ON" : "OFF"); ######################################################################################### # SWITCH COMPONENT @@ -175,10 +261,5 @@ switch: pin: GPIO12 id: Relay_2 -# KS-811-2 is a double only -# - platform: gpio -# name: "Relay 3: ${switch_3_name}" -# pin: GPIO14 -# id: Relay_3 diff --git a/esphome/esp-4chdmxdimmer1.yaml b/esphome/esp-lounge6chdimmer.yaml similarity index 66% rename from esphome/esp-4chdmxdimmer1.yaml rename to esphome/esp-lounge6chdimmer.yaml index 17a2a98..8ed612e 100644 --- a/esphome/esp-4chdmxdimmer1.yaml +++ b/esphome/esp-lounge6chdimmer.yaml @@ -1,12 +1,13 @@ ########################################################################################## ########################################################################################## # 4 Channel DMX Mains Dimmer -# This is using an esp8366 (D1 Mini) sending DMX512 Serial to a pair of 2 channel +# This is using an esp8366 (D1 Mini) sending DMX512 Serial to a trio of 2 channel # mains lighting contollers (1.2A/Channel) # # Contoller: https://www.aliexpress.com/item/32838426377.html # RS-485 TTL converter: https://www.aliexpress.com/item/1005005737922222.html # +# V1.1 2025-09-13 Extended to 3 modules and installed # V1.0 2025-09-06 First Rev (only tested one module, 2 channels at this stage, and incandescent) ########################################################################################## # @@ -56,25 +57,41 @@ ########################################################################################## substitutions: # Device Naming - device_name: "esp-4chdmxdimmer1" - friendly_name: "4 Channel DMX Dimmer 1" - description_comment: "Lounge 4 Channel Mains Lighting Dimmer using DMX512 protocol" + device_name: "esp-lounge6chdimmer" + friendly_name: "6 Channel Lounge Dimmer" + description_comment: "Lounge 6 Channel Mains Lighting Dimmer (3 x 2 channel modules) controlled using DMX512 protocol with a D1 Mini Pro" device_area: "Lounge" # Allows ESP device to be automatically linked to an 'Area' in Home Assistant. # Project Naming project_name: "Generic ESP8266.D1 Mini" # Project Details - project_version: "v1.0" # Project V denotes release of yaml file, allowing checking of deployed vs latest version + project_version: "v1.1" # Project V denotes release of yaml file, allowing checking of deployed vs latest version # 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-4chdmxdimmer1_ip + static_ip_address: !secret esp-lounge6chdimmer_ip # Device Settings #relay_icon: "mdi:lightbulb-group" - log_level: "DEBUG" # Define logging level: NONE, ERROR, WARN, INFO, DEBUG (Default), VERBOSE, VERY_VERBOSE + log_level: "INFO" # Define logging level: NONE, ERROR, WARN, INFO, DEBUG (Default), VERBOSE, VERY_VERBOSE update_interval: "60s" # update time for for general sensors etc + output_1_name: "Lounge Downlights South" + output_2_name: "Lounge Rafter Buttons" + output_3_name: "Lounge Downlights Centre" + output_4_name: "Dining Table Light" + output_5_name: "Couch Spots Right" + output_6_name: "Couch Spots Left" + + # Per-channel minimum output when ON (0.00f..1.00f). OFF remains 0%. + # Adjust per channel to avoid flicker at very low levels. + min_output_1: "0.20f" + min_output_2: "0.20f" + min_output_3: "0.20f" + min_output_4: "0.20f" + min_output_5: "0.20f" + min_output_6: "0.20f" + ######################################################################################### # PACKAGES: Included Common Packages # https://esphome.io/components/packages.html @@ -82,29 +99,37 @@ substitutions: # Comment some of these out where not needed, eg MQTT, Web portal, SNTP, General Sensors ########################################################################################## 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}" + + #### 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}" - #common_webportal: !include - # file: common/webportal_common.yaml + + #### MQTT #### common_mqtt: !include file: common/mqtt_common.yaml vars: local_device_name: "${device_name}" - common_sntp: !include - file: common/sntp_common.yaml - common_general_sensors: !include - file: common/sensors_common.yaml - vars: - local_friendly_name: "${friendly_name}" - local_update_interval: "${update_interval}" + + #### 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 @@ -126,13 +151,15 @@ esphome: id(dmx_ch2_sensor).publish_state(0); id(dmx_ch3_sensor).publish_state(0); id(dmx_ch4_sensor).publish_state(0); + id(dmx_ch5_sensor).publish_state(0); + id(dmx_ch6_sensor).publish_state(0); ######################################################################################### # ESP Platform and Framework # https://esphome.io/components/esp32.html ######################################################################################### esp8266: - board: esp01_1m + board: d1_mini_pro #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 restore_from_flash: true # restore some values on reboot @@ -140,8 +167,8 @@ esp8266: mdns: disabled: false # Disabling will make the build file smaller (and it is still available via static IP) -preferences: - flash_write_interval: 0s +#preferences: +# flash_write_interval: 0s ######################################################################################### # ESPHome Logging Enable @@ -183,33 +210,47 @@ uart: ########################################################################################## light: - platform: monochromatic - name: "Test Lights Chan 1" + name: ${output_1_name} output: dmx_1 id: light_test_1 - default_transition_length: 5s - gamma_correct: 1 + default_transition_length: 10s + gamma_correct: 2.2 restore_mode: ALWAYS_ON - platform: monochromatic - name: "Test Lights Chan 2" + name: ${output_2_name} output: dmx_2 id: light_test_2 - default_transition_length: 5s - gamma_correct: 1 + default_transition_length: 10s + gamma_correct: 2.2 restore_mode: ALWAYS_ON - platform: monochromatic - name: "Test Lights Chan 3" + name: ${output_3_name} output: dmx_3 id: light_test_3 - default_transition_length: 5s - gamma_correct: 1 + default_transition_length: 10s + gamma_correct: 2.2 restore_mode: ALWAYS_ON - platform: monochromatic - name: "Test Lights Chan 4" + name: ${output_4_name} output: dmx_4 id: light_test_4 - default_transition_length: 5s + default_transition_length: 10s gamma_correct: 1 - restore_mode: ALWAYS_ON + restore_mode: ALWAYS_ON + - platform: monochromatic + name: ${output_5_name} + output: dmx_5 + id: light_test_5 + default_transition_length: 10s + gamma_correct: 1 + restore_mode: RESTORE_DEFAULT_OFF + - platform: monochromatic + name: ${output_6_name} + output: dmx_6 + id: light_test_6 + default_transition_length: 10s + gamma_correct: 1 + restore_mode: RESTORE_DEFAULT_OFF ########################################################################################## # SENSOR COMPONENT @@ -219,12 +260,14 @@ sensor: # Expose each DMX channel's current output level as 0..255 integers (post-gamma) - platform: template id: dmx_ch1_sensor - name: "DMX Ch 1 Output (0-255)" + name: ${output_1_name} Output icon: mdi:counter #entity_category: diagnostic accuracy_decimals: 0 update_interval: never filters: + - throttle: 200ms + - delta: 1 - round: 0 - clamp: min_value: 0 @@ -232,12 +275,14 @@ sensor: - platform: template id: dmx_ch2_sensor - name: "DMX Ch 2 Output (0-255)" + name: ${output_2_name} Output icon: mdi:counter #entity_category: diagnostic accuracy_decimals: 0 update_interval: never filters: + - throttle: 200ms + - delta: 1 - round: 0 - clamp: min_value: 0 @@ -245,12 +290,14 @@ sensor: - platform: template id: dmx_ch3_sensor - name: "DMX Ch 3 Output (0-255)" + name: ${output_3_name} Output icon: mdi:counter #entity_category: diagnostic accuracy_decimals: 0 update_interval: never filters: + - throttle: 200ms + - delta: 1 - round: 0 - clamp: min_value: 0 @@ -258,12 +305,44 @@ sensor: - platform: template id: dmx_ch4_sensor - name: "DMX Ch 4 Output (0-255)" + name: ${output_4_name} Output icon: mdi:counter #entity_category: diagnostic accuracy_decimals: 0 update_interval: never filters: + - throttle: 200ms + - delta: 1 + - round: 0 + - clamp: + min_value: 0 + max_value: 255 + + - platform: template + id: dmx_ch5_sensor + name: ${output_5_name} Output + icon: mdi:counter + #entity_category: diagnostic + accuracy_decimals: 0 + update_interval: never + filters: + - throttle: 200ms + - delta: 1 + - round: 0 + - clamp: + min_value: 0 + max_value: 255 + + - platform: template + id: dmx_ch6_sensor + name: ${output_6_name} Output + icon: mdi:counter + #entity_category: diagnostic + accuracy_decimals: 0 + update_interval: never + filters: + - throttle: 200ms + - delta: 1 - round: 0 - clamp: min_value: 0 @@ -284,7 +363,7 @@ dmx512: number: GPIO2 allow_other_uses: true # For ESPHome >= 2023.12.0 uart_num: 1 - #periodic_update: true # optional. Some dimmers may turn off without regular updates. + periodic_update: true # optional. Some dimmers may turn off without regular updates. #force_full_frames: false # optional. If true the 513 byte is always sent #custom_break_len: 92 # optional. Break Length. Default is 92uS #custom_mab_len: 12 # optional. Mark after Break Length. Default is 12mS @@ -312,6 +391,14 @@ output: channel: 4 # The ID we set the device to universe: dmx # The ID of the DMX512 Component id: dmx_4_raw + - platform: dmx512 + channel: 5 # The ID we set the device to + universe: dmx # The ID of the DMX512 Component + id: dmx_5_raw + - platform: dmx512 + channel: 6 # The ID we set the device to + universe: dmx # The ID of the DMX512 Component + id: dmx_6_raw # Wrapped outputs: publish exact 0..255 to sensors, then forward float to raw DMX - platform: template @@ -319,31 +406,77 @@ output: type: float write_action: - lambda: |- - id(dmx_ch1_sensor).publish_state(state * 255.0f); - id(dmx_1_raw).set_level(state); + const float MIN = ${min_output_1}; + float scaled = state; + if (state > 0.0f) { + // Remap 0..1 -> MIN..1.0 (keep OFF at 0) + scaled = state * (1.0f - MIN) + MIN; + } + id(dmx_ch1_sensor).publish_state(scaled * 255.0f); + id(dmx_1_raw).set_level(scaled); - platform: template id: dmx_2 type: float write_action: - lambda: |- - id(dmx_ch2_sensor).publish_state(state * 255.0f); - id(dmx_2_raw).set_level(state); + const float MIN = ${min_output_2}; + float scaled = state; + if (state > 0.0f) { + scaled = state * (1.0f - MIN) + MIN; + } + id(dmx_ch2_sensor).publish_state(scaled * 255.0f); + id(dmx_2_raw).set_level(scaled); - platform: template id: dmx_3 type: float write_action: - lambda: |- - id(dmx_ch3_sensor).publish_state(state * 255.0f); - id(dmx_3_raw).set_level(state); + const float MIN = ${min_output_3}; + float scaled = state; + if (state > 0.0f) { + scaled = state * (1.0f - MIN) + MIN; + } + id(dmx_ch3_sensor).publish_state(scaled * 255.0f); + id(dmx_3_raw).set_level(scaled); - platform: template id: dmx_4 type: float write_action: - lambda: |- - id(dmx_ch4_sensor).publish_state(state * 255.0f); - id(dmx_4_raw).set_level(state); + const float MIN = ${min_output_4}; + float scaled = state; + if (state > 0.0f) { + scaled = state * (1.0f - MIN) + MIN; + } + id(dmx_ch4_sensor).publish_state(scaled * 255.0f); + id(dmx_4_raw).set_level(scaled); + - platform: template + id: dmx_5 + type: float + write_action: + - lambda: |- + const float MIN = ${min_output_5}; + float scaled = state; + if (state > 0.0f) { + scaled = state * (1.0f - MIN) + MIN; + } + id(dmx_ch5_sensor).publish_state(scaled * 255.0f); + id(dmx_5_raw).set_level(scaled); + + - platform: template + id: dmx_6 + type: float + write_action: + - lambda: |- + const float MIN = ${min_output_6}; + float scaled = state; + if (state > 0.0f) { + scaled = state * (1.0f - MIN) + MIN; + } + id(dmx_ch6_sensor).publish_state(scaled * 255.0f); + id(dmx_6_raw).set_level(scaled); diff --git a/esphome/esp-loungebookshelfleds.yaml b/esphome/esp-loungebookshelfleds.yaml new file mode 100644 index 0000000..57c2bc8 --- /dev/null +++ b/esphome/esp-loungebookshelfleds.yaml @@ -0,0 +1,1028 @@ +########################################################################################## +########################################################################################## +# Title: LOUNGE BOOKSHELF LEDS +# Repo: https://home.fox.co.nz/gitea/zorruno/zorruno-homeassistant/src/branch/master/esphome/esp-loungebookshelfleds.yaml +# +# v2.0 - 2025-09-15 A bunch of fixes and made it more standard across my devices +# v1.0 - 2025-08-17 First setup (and replacement of Tasmota) +# +# ------------------------------------------ +# OPERATION (as of v2.0) +# ------------------------------------------ +# 1. General-purpose LED controller. +# 2. Designed for the Sinilink XY-WFMS board with a MOSFET output (claimed 5A, 5-36 V DC), but will suit similar mosfet boards. +# 3. (In SUBSTITUTIONS) setting for MAX % output to extend LED life (ie can ensure it is only ever 95%) +# 4. (In SUBSTITUTIONS) Minimum output setting; switches fully OFF at/below the minimum to avoid low-PWM flicker. +# 5. (In SUBSTITUTIONS) PWM frequency is set to 1kHz by default. You can increase it, but higher values caused +# resets on Sinilink device. On ESP32 you can run much higher (~40 kHz). +# 6. Min/Max output settings are not exposed in Home Assistant/MQTT by default, but could be. +# With a 1 MB flash, space is tight and only minimal optimisation has been done so far. +# 7. (PACKAGES) include common items: network settings, diagnostic entities, MQTT, and SNTP (optional). +# 8. Default behaviours, for recovery from lost power or reset: +# - "Fade up to full": fade from floor to max on boot. +# - "Restore Brightness": fade from floor to the last non-zero brightness on boot (should be no on/off blip/flash). +# - "Remain Off": stays off on boot. +# 9. The green LED flashes while fading (different patterns for up/down). The red LED follows the +# output (it shares the MOSFET GPIO). (Obviously this depends on hardware) +# 10. Fade timing scales with the configured values (proportionally when starting mid-brightness). +# 11. Exposed in Home Assistant/MQTT: +# - Startup action (On,Off,Restore) +# - Fade Up / Fade Down / Fade Stop buttons +# - Fade Up/Down toggle switch (keeps fading up to full, or down to off unless stopped) +# - Normal On/Off switch (quick ramp up/down, up to the level of previous brightness) +# - Fade up/down times (0-60s) +# - Output % (pre-gamma) and PWM % (post-gamma) +# - Output Set (1-100, respects min/max). This also changes with other output so reflects value. +# - Many device diagnostics (from the included 'diagnostics' package) +# - Maximum 'on' time before automatic fade-down (1-48 h, 0 = no limit) +########################################################################################## +########################################################################################## +# Hardware: Magishome LED controller single channel (ESP8266, NOT the later ones with BL602) +# https://devices.esphome.io/devices/MagicHome-ZJ-WFMN-C-Single-Color +# ------------------------------------------ +# DEVICE GPIO (Magichome ESP8266) +# ------------------------------------------ +# GPIO02 Blue LED (used for ESPHome status) - I think... can't see the board +# GPIO12 MOSFET output (0 V when switched) +########################################################################################## +########################################################################################## + +########################################################################################## +# 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-loungebookshelfleds" # yaml file should be device_name.yaml + friendly_name: "Lounge Bookshelf LEDs" + description_comment: "LED Strip around the bookshelf in the upstairs lounge :: Magichome ESP8266" + device_area: "Lounge" # Allows the ESP device to be automatically linked to an 'Area' in Home Assistant. + + # Project Naming + project_name: "Magichome Single Colour V1.1 LED Controller.Generic ESP8266" # Project details. Manufacturer before the dot, device after the dot. + project_version: "v2.0" # Project version denotes release of the YAML file, allowing checking of deployed vs latest version + + # Passwords & Secrets (Unfortunately, you can't use substitutions inside secret names) + api_key: !secret esp-api_key # If using HA API + ota_pass: !secret esp-ota_pass # If passwording OTA updates + static_ip_address: !secret esp-loungebookshelfleds_ip # CHANGE THIS + + # MQTT LOCAL Controls (This is just my local method of controlling via MQTT, I also use the built in method) + mqtt_local_device_name: "lounge-bookshelfleds" + mqtt_local_command_main_topic: !secret mqtt_local_command_main_topic + mqtt_local_status_main_topic: !secret mqtt_local_status_main_topic + mqtt_local_command_topic: "${mqtt_local_command_main_topic}/${mqtt_local_device_name}" # Topic we will use to command this locally without HA + mqtt_local_status_topic: "${mqtt_local_status_main_topic}/${mqtt_local_device_name}" # Topic we will use to view status locally without HA + mqtt_local_device_command_ON: "ON" + mqtt_local_device_command_OFF: "OFF" + + # Device Specific Settings + log_level: "NONE" # Define logging level: NONE, ERROR, WARN, INFO, DEBUG (default), VERBOSE, VERY_VERBOSE + update_interval: "20s" # Update time for general sensors, etc. + led_gamma: "1.2" # Gamma from 1.2-3 is sensible to normalise the LED fading vs PWM + minimum_led_output: "1" # % If at this value or below, we'll switch it completely off + maximum_led_output: "90" # % Maximum output; it is sometimes nice to limit the output for longevity or aesthetics + max_on_default_hours: "24" # The maximum time the LEDs will be on, in case they get left on. 0 = no automatic turn-off + pwm_frequency: "1000hz" # PWM output Frequency. High enough to avoid audible/visible artifacts + + # Device Specific GPIO (so we can easily update for other devices) + device_status_led: GPIO02 # Can't really see if there is an LED + device_mosfet_out: GPIO12 + device_usr_button: GPIO04 # this is just a fake/safeish choice so it compiles with my generic yaml + device_fading_led: GPIO13 # this is just a fake/safeish choice so it compiles with my generic yaml + + +########################################################################################## +# 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}" + + #### 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 CORE CONFIGURATION +# https://esphome.io/components/esphome.html +########################################################################################## +esphome: + platformio_options: + build_flags: + - -DVTABLES_IN_FLASH + - -DPIO_FRAMEWORK_ARDUINO_LWIP2_LOW_MEMORY + name: "${device_name}" + friendly_name: "${friendly_name}" + comment: "${description_comment}" + area: "${device_area}" + on_boot: + # Run later so everything is initialised before we touch states. + priority: 150 + then: + # Keep the HA dropdown in sync with the stored mode + - lambda: |- + switch (id(restart_mode)) { + case 0: id(restart_action).publish_state("Fade up to full"); break; + case 1: id(restart_action).publish_state("Restore Brightness"); break; + case 2: default: id(restart_action).publish_state("Remain Off"); break; + } + + # Mode 0: Fade up to full (obeys fade settings; no on/off flicker) + - if: + condition: + lambda: 'return id(restart_mode) == 0;' + then: + - lambda: 'id(ramp_switch_target_on) = true;' + - script.execute: ramp_on_script + + # Mode 1: Restore Brightness (deferred to avoid races) + - if: + condition: + lambda: 'return id(restart_mode) == 1;' + then: + - script.execute: deferred_restore_brightness + + # Mode 2: Remain Off (no blip; stays off) + - if: + condition: + lambda: 'return id(restart_mode) == 2;' + then: + - script.stop: ramp_on_script + - script.stop: ramp_off_script + - lambda: 'id(ramp_switch_target_on) = false;' + - light.turn_off: + id: mosfet_leds + transition_length: 0s + + # Boot complete: allow OFF handlers again + - lambda: 'id(booting) = false;' + +# Only if you want to play with build flags... +# platformio_options: +# build_unflags: +# - -flto +# build_flags: +# - -fno-lto +# - -Wl,--gc-sections +# - -ffunction-sections +# - -fdata-sections +# - -DNDEBUG + +########################################################################################## +# ESP PLATFORM AND FRAMEWORK +# https://esphome.io/components/esp8266.html +# https://esphome.io/components/esp32.html +########################################################################################## +esp8266: + board: esp01_1m + restore_from_flash: true # restore some values on reboot + +mdns: + disabled: true # Disabling will make the build file smaller (and device is still available via static IP) + +preferences: + flash_write_interval: 10sec # enough time to update values for reboots, but not enough to wear flash + +########################################################################################## +# GLOBAL VARIABLES +# https://esphome.io/components/globals.html +########################################################################################## +globals: + # Minimum Brightness % for LEDs (will switch off if <=) + - id: min_brightness_pct + type: int + restore_value: true + initial_value: "${minimum_led_output}" # start/finish at X% + # Maximum Brightness % for LEDs (should never go beyond this) + - id: max_brightness_pct + type: int + restore_value: false + initial_value: "${maximum_led_output}" # hard cap; never exceed this + # The maximum time the lights will stay on, in hours. Just in case they are left on. 0 = forever + - id: max_on_hours + type: int + restore_value: true + initial_value: "${max_on_default_hours}" + + # Default Fading Up Time (Selectable and will be retained) + - id: ramp_up_ms # fade-in when turned ON + type: int + restore_value: true + initial_value: '5000' # 5 s + # Default Fading Down Time (Selectable and will be retained) + - id: ramp_down_ms # fade-out when turned OFF + type: int + restore_value: true + initial_value: '10000' # 10 s + # Action on Restart. (0=Fade full, 1=Restore brightness, 2=Remain off) + - id: restart_mode + type: int + restore_value: true + initial_value: '0' # default = Fade Up to Full (so can be deployed with no other setup) + + # Determine last fade direction. + # true when you asked the light to end up ON (Ramp Up) + # false when you asked the light to end up OFF (Ramp Down) + - id: ramp_switch_target_on + type: bool + restore_value: true + initial_value: 'false' + # Prevent jitter when adjusting the slider + - id: suppress_slider_sync + type: bool + restore_value: false + initial_value: 'false' + # actual 0..100 seen last time, for restart + - id: last_brightness_pct + type: float + restore_value: true + initial_value: '0.0' + # last published "Output Set (0-100)" integer + - id: last_set_pos + type: int + restore_value: false + initial_value: '-1' + # helper to keep blink time == transition time + - id: last_ramp_ms + type: int + restore_value: false + initial_value: '0' + # last non-zero actual 0..100 used, for stable "Restore Brightness" + - id: last_nonzero_brightness_pct + type: float + restore_value: true + initial_value: '0.0' + # target for "Restore Brightness" scripted ramp + - id: restore_target_pct + type: float + restore_value: false + initial_value: '0.0' + # true while a scripted ramp is in progress (to limit flash writes) + - id: is_ramping + type: bool + restore_value: false + initial_value: 'false' + # volatile (non-persistent) last non-zero brightness during ramps + - id: last_nonzero_tmp + type: float + restore_value: false + initial_value: '0.0' + # guards OFF persistence during startup caused by restore_mode: ALWAYS_OFF + - id: booting + type: bool + restore_value: false + initial_value: 'true' +# true only while we are performing a ramp-down that briefly turns the light ON to hit the floor + - id: ramping_for_off + type: bool + restore_value: false + initial_value: 'false' + +########################################################################################## +# LOGGER COMPONENT +# https://esphome.io/components/logger.html +# Logs all log messages through the serial port and through MQTT topics. +########################################################################################## +logger: + level: "${log_level}" # INFO Level 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 (eg PZEM, Serial control) + +######################################################################################### +# STATUS LED +# https://esphome.io/components/status_led.html +######################################################################################### +status_led: + pin: + number: ${device_status_led} + inverted: true + +########################################################################################## +# MQTT COMMANDS +# This adds device-specific MQTT command triggers to the common MQTT configuration. +########################################################################################## +mqtt: + reboot_timeout: 0s + on_message: + # Light control to ramp up + - topic: "${mqtt_local_command_topic}/light/set" + payload: "${mqtt_local_device_command_ON}" + then: + - switch.turn_on: mosfet_ramp_switch + # Light control to ramp down + - topic: "${mqtt_local_command_topic}/light/set" + payload: "${mqtt_local_device_command_OFF}" + then: + - switch.turn_off: mosfet_ramp_switch + +########################################################################################## +# SWITCH COMPONENT +# https://esphome.io/components/switch/ +########################################################################################## +switch: + # Ramp-aware ON/OFF for HA (asymmetric, eased; no bounce) + - platform: template + id: mosfet_ramp_switch + name: "${friendly_name} Fade Up/Down" + icon: mdi:led-strip-variant + lambda: |- + return id(ramp_switch_target_on); + turn_on_action: + - lambda: |- + id(ramp_switch_target_on) = true; + - script.stop: ramp_off_script + - script.execute: ramp_on_script + turn_off_action: + - lambda: |- + id(ramp_switch_target_on) = false; + - script.stop: ramp_on_script + - script.execute: ramp_off_script + +################################################################################################# +# BUTTON COMPONENT +# https://esphome.io/components/button/index.html +################################################################################################# +button: + # Start ramping UP (from current level) + - platform: template + id: fade_up_button + name: "${friendly_name} Fade Up" + icon: mdi:arrow-up-bold + on_press: + - lambda: |- + id(ramp_switch_target_on) = true; + id(mosfet_ramp_switch).publish_state(true); // reflect in HA immediately + - script.stop: ramp_off_script + - script.execute: ramp_on_script + + # Start ramping DOWN (from current level) + - platform: template + id: fade_down_button + name: "${friendly_name} Fade Down" + icon: mdi:arrow-down-bold + on_press: + - lambda: |- + id(ramp_switch_target_on) = false; + id(mosfet_ramp_switch).publish_state(false); // reflect in HA immediately + - script.stop: ramp_on_script + - script.execute: ramp_off_script + + # STOP any ramping (hold current brightness) + - platform: template + id: fade_stop_button + name: "${friendly_name} Fade Stop" + icon: mdi:pause + on_press: + # Stop any pending scripts (and their delayed actions) + - script.stop: ramp_on_script + - script.stop: ramp_off_script + - script.stop: led_flash_up + - script.stop: led_flash_down + - output.turn_off: green_led_out + - lambda: |- + // We are no longer ramping (up or down) + id(ramping_for_off) = false; + id(is_ramping) = false; + + // Freeze the light at its *current* level by overwriting the transition + const auto &cv = id(mosfet_leds).current_values; + if (cv.is_on()) { + auto call = id(mosfet_leds).make_call(); + call.set_state(true); + call.set_brightness(cv.get_brightness()); + call.set_transition_length(0); + call.perform(); + } + + // Persist the exact frozen level so "Restore Brightness" survives a reboot + if (cv.is_on()) { + float pct = cv.get_brightness() * 100.0f; + id(last_brightness_pct) = pct; + if (pct > 0.5f) id(last_nonzero_brightness_pct) = pct; + } + + +######################################################################################### +# SELECT COMPONENT +# https://esphome.io/components/select/index.html +######################################################################################### +select: + - platform: template + id: restart_action + name: "${friendly_name} Restart Action" + icon: mdi:restart + optimistic: true + options: + - "Fade up to full" + - "Restore Brightness" + - "Remain Off" + initial_option: "Restore Brightness" + set_action: + - lambda: |- + if (x == "Fade up to full") { + id(restart_mode) = 0; + } else if (x == "Restore Brightness") { + id(restart_mode) = 1; + } else { + id(restart_mode) = 2; + } + +######################################################################################### +# BINARY SENSORS +# https://esphome.io/components/binary_sensor/ +######################################################################################### +binary_sensor: + - platform: gpio + id: btn_gpio12 + name: "${friendly_name} Button" + pin: + number: ${device_usr_button} + mode: + input: true + pullup: true + inverted: true + filters: + - delayed_on: 20ms + - delayed_off: 20ms + on_press: + - if: + condition: + lambda: 'return id(ramp_switch_target_on);' + then: + # Target is currently ON -> press should go OFF (start ramp-down) + - lambda: |- + id(ramp_switch_target_on) = false; + id(mosfet_ramp_switch).publish_state(false); // reflect in HA immediately + - script.stop: ramp_on_script + - script.execute: ramp_off_script + else: + # Target is currently OFF -> press should go ON (start ramp-up) + - lambda: |- + id(ramp_switch_target_on) = true; + id(mosfet_ramp_switch).publish_state(true); // reflect in HA immediately + - script.stop: ramp_off_script + - script.execute: ramp_on_script + +########################################################################################## +# SENSOR COMPONENT +# https://esphome.io/components/sensor/ +########################################################################################## +sensor: + - platform: template + id: mosfet_output_pct + name: "${friendly_name} Output (%)" + unit_of_measurement: "%" + icon: mdi:percent + accuracy_decimals: 0 + update_interval: 500ms # consider higher if you want fewer updates + lambda: |- + const auto &cv = id(mosfet_leds).current_values; + return cv.is_on() ? (cv.get_brightness() * 100.0f) : 0.0f; + on_value: + then: + - lambda: |- + const auto &cv_now = id(mosfet_leds).current_values; + + // Only persist exact last brightness while actually ON, + // and not during the OFF ramp's brief floor-kick. + if (cv_now.is_on()) { + if (x > 0.5f) { + if (!id(is_ramping)) id(last_nonzero_brightness_pct) = x; + id(last_nonzero_tmp) = x; + } + } else { + if (x > 0.5f) id(last_nonzero_tmp) = x; + } + + // If not suppressing sync, update the 0..100 slider only when its INT changes + if (!id(suppress_slider_sync)) { + float actual = x; + float minp = (float) id(min_brightness_pct); + float maxp = (float) id(max_brightness_pct); + if (maxp <= minp) maxp = minp + 1.0f; + float pos = (actual <= 0.0f) ? 0.0f : ((actual - minp) * 100.0f / (maxp - minp)); + if (pos < 0.0f) pos = 0.0f; + if (pos > 100.0f) pos = 100.0f; + int pos_i = (int) floorf(pos + 0.5f); + + if (pos_i != id(last_set_pos)) { + id(last_set_pos) = pos_i; + id(led_output_set_pct).publish_state(pos_i); + } + } + - platform: template + id: mosfet_output_pwm_pct + name: "${friendly_name} Output PWM (%)" + unit_of_measurement: "%" + icon: mdi:square-wave + accuracy_decimals: 1 + update_interval: 500ms + lambda: |- + const auto &cv = id(mosfet_leds).current_values; + if (!cv.is_on()) return 0.0f; + const float lin = cv.get_brightness(); // 0..1 linear brightness + const float gamma = atof("${led_gamma}"); // parse substitution string -> float + float pwm = powf(lin, gamma); // approx PWM duty after gamma + if (pwm < 0.0f) pwm = 0.0f; + if (pwm > 1.0f) pwm = 1.0f; + return pwm * 100.0f; + +########################################################################################## +# OUTPUT COMPONENT +# https://esphome.io/components/light/index.html +########################################################################################## +# An OUTPUT can be binary (0,1) or float, which is any value between 0 and 1. +# PWM Outputs such as "ledc" are float. https://esphome.io/components/output/ledc.html +########################################################################################## +output: + - platform: esp8266_pwm + id: mosfet_pwm + pin: ${device_mosfet_out} + frequency: ${pwm_frequency} + - platform: gpio + id: green_led_out # Green LED + pin: + number: ${device_fading_led} + inverted: false + +########################################################################################## +# LIGHT COMPONENT +# https://esphome.io/components/light/ +########################################################################################## +light: + - platform: monochromatic + id: mosfet_leds + name: "${friendly_name}" + output: mosfet_pwm + restore_mode: RESTORE_DEFAULT_OFF + default_transition_length: 2s + icon: mdi:led-strip-variant + gamma_correct: "${led_gamma}" + + # ON: publish state, track intent, arm watchdog if configured + on_turn_on: + - mqtt.publish: + topic: "${mqtt_local_status_topic}/light/state" + payload: "${mqtt_local_device_command_ON}" + retain: true + - lambda: |- + // Do not flip "intent to ON" when we're inside an OFF ramp's floor-kick + if (!id(ramping_for_off)) { + id(ramp_switch_target_on) = true; + } + - script.stop: max_on_watchdog + - if: + condition: + lambda: 'return id(max_on_hours) > 0;' + then: + - script.execute: max_on_watchdog + + # OFF: publish state, track intent, clear watchdog, and persist OFF so Mode 1 will not restore + # Guarded by booting flag so the automatic OFF at boot from ALWAYS_OFF does not zero persistence. + on_turn_off: + - if: + condition: + lambda: 'return !id(booting);' + then: + - mqtt.publish: + topic: "${mqtt_local_status_topic}/light/state" + payload: "${mqtt_local_device_command_OFF}" + retain: true + - lambda: 'id(ramp_switch_target_on) = false;' + - script.stop: max_on_watchdog + - lambda: |- + id(last_brightness_pct) = 0.0f; + id(last_set_pos) = 0; + id(led_output_set_pct).publish_state(0); + + # Cap brightness to max_brightness_pct without a visible step + on_state: + - lambda: |- + const float cap = id(max_brightness_pct) / 100.0f; + const auto &cv = id(mosfet_leds).current_values; + if (cv.is_on() && cv.get_brightness() > cap + 0.001f) { + auto call = id(mosfet_leds).make_call(); + call.set_state(true); + call.set_brightness(cap); + call.set_transition_length(0); + call.perform(); + } + +########################################################################################## +# NUMBER COMPONENT +# https://esphome.io/components/number/ +########################################################################################## +number: + - platform: template + id: cfg_ramp_up_s + name: "${friendly_name} Fade Up Time (s)" + entity_category: config + unit_of_measurement: s + icon: mdi:timer-sand + mode: slider + min_value: 0 + max_value: 60 + step: 1 + lambda: |- + return (float) id(ramp_up_ms) / 1000.0f; + set_action: + - lambda: |- + int secs = (int) floorf(x + 0.5f); + if (secs < 0) secs = 0; + if (secs > 60) secs = 60; + id(ramp_up_ms) = secs * 1000; + id(cfg_ramp_up_s).publish_state((float) secs); + + - platform: template + id: cfg_ramp_down_s + name: "${friendly_name} Fade Down Time (s)" + entity_category: config + unit_of_measurement: s + icon: mdi:timer-sand-complete + mode: slider + min_value: 0 + max_value: 60 + step: 1 + lambda: |- + return (float) id(ramp_down_ms) / 1000.0f; + set_action: + - lambda: |- + int secs = (int) floorf(x + 0.5f); + if (secs < 0) secs = 0; + if (secs > 60) secs = 60; + id(ramp_down_ms) = secs * 1000; + id(cfg_ramp_down_s).publish_state((float) secs); + + - platform: template + id: led_output_set_pct + name: "${friendly_name} Output Set (0-100)" + icon: mdi:tune + mode: slider + min_value: 0 + max_value: 100 + step: 1 + # Show current position mapped into 0..100 across [min..max] + lambda: |- + const auto &cv = id(mosfet_leds).current_values; + float actual = cv.is_on() ? (cv.get_brightness() * 100.0f) : 0.0f; // 0..100 actual + float minp = (float) id(min_brightness_pct); + float maxp = (float) id(max_brightness_pct); + if (maxp <= minp) maxp = minp + 1.0f; // avoid div/0 + if (actual <= 0.0f) return 0.0f; // when OFF, show 0 + float pos = (actual - minp) * 100.0f / (maxp - minp); + if (pos < 0.0f) pos = 0.0f; + if (pos > 100.0f) pos = 100.0f; + return floorf(pos + 0.5f); // integer + set_action: + - if: + condition: + lambda: 'return x <= 0.0f;' + then: + # 0 means OFF + - lambda: 'id(suppress_slider_sync) = true;' + - script.stop: ramp_on_script + - script.stop: ramp_off_script + - light.turn_off: + id: mosfet_leds + transition_length: 200ms + - lambda: |- + id(ramp_switch_target_on) = false; + id(led_output_set_pct).publish_state(0); + id(last_brightness_pct) = 0.0f; // persist OFF immediately + id(last_set_pos) = 0; + - delay: 400ms + - lambda: 'id(suppress_slider_sync) = false;' + else: + # Map 1..100 - [min..max] and set ON + - lambda: |- + id(suppress_slider_sync) = true; + float pos = x; // 0..100 + if (pos < 1.0f) pos = 1.0f; // 0 is OFF + if (pos > 100.0f) pos = 100.0f; + id(led_output_set_pct).publish_state((int) floorf(pos + 0.5f)); + - script.stop: ramp_off_script + - script.stop: ramp_on_script + - light.turn_on: + id: mosfet_leds + brightness: !lambda |- + float pos = id(led_output_set_pct).state; // 1..100 + float minp = (float) id(min_brightness_pct); + float maxp = (float) id(max_brightness_pct); + if (maxp <= minp) maxp = minp + 1.0f; + float out_pct = minp + (pos * (maxp - minp) / 100.0f); + if (out_pct > maxp) out_pct = maxp; + return out_pct / 100.0f; + transition_length: 500ms + - lambda: |- + id(ramp_switch_target_on) = true; + float pos = id(led_output_set_pct).state; // 1..100 + float minp = (float) id(min_brightness_pct); + float maxp = (float) id(max_brightness_pct); + if (maxp <= minp) maxp = minp + 1.0f; + float out_pct = minp + (pos * (maxp - minp) / 100.0f); + if (out_pct > maxp) out_pct = maxp; + id(last_brightness_pct) = out_pct; // persist exact target now + if (out_pct > 0.5f) { + id(last_nonzero_brightness_pct) = out_pct; + id(last_nonzero_tmp) = out_pct; + } + - delay: 400ms + - lambda: 'id(suppress_slider_sync) = false;' + + - platform: template + id: cfg_max_on_hours + name: "${friendly_name} Max On (h)" + entity_category: config + unit_of_measurement: h + icon: mdi:timer-cog + mode: slider + min_value: 0 + max_value: 48 + step: 1 + lambda: |- + return (float) id(max_on_hours); + set_action: + - lambda: |- + int hrs = (int) x; + if (hrs < 0) hrs = 0; + if (hrs > 48) hrs = 48; + id(max_on_hours) = hrs; + id(cfg_max_on_hours).publish_state((float) hrs); + - if: + condition: + lambda: 'return id(mosfet_leds).current_values.is_on();' + then: + - script.stop: max_on_watchdog + - if: + condition: + lambda: 'return id(max_on_hours) > 0;' + then: + - script.execute: max_on_watchdog + +########################################################################################## +# SCRIPT COMPONENT +# https://esphome.io/components/script.html +# Scripts can be executed nearly anywhere in your device configuration with a single call. +########################################################################################## +script: + # Blink pattern while ramping UP: quick double-blink, pause, repeat + - id: led_flash_up + mode: restart + then: + - repeat: + count: 1000000000 # effectively forever, but cancellable + then: + - output.turn_on: green_led_out + - delay: 100ms + - output.turn_off: green_led_out + - delay: 100ms + - output.turn_on: green_led_out + - delay: 100ms + - output.turn_off: green_led_out + - delay: 400ms + # Blink pattern while ramping DOWN: steady slow blink + - id: led_flash_down + mode: restart + then: + - while: + condition: + lambda: 'return true;' + then: + - output.turn_on: green_led_out + - delay: 250ms + - output.turn_off: green_led_out + - delay: 250ms + + # Script: ramp up from current level. Obey global max. + - id: ramp_on_script + mode: restart + then: + - lambda: 'id(is_ramping) = true;' + - script.stop: ramp_off_script + - script.stop: led_flash_down + - script.execute: led_flash_up + # Ensure we start at at least the floor without a visible "pop". + - if: + condition: + lambda: |- + const auto &cv = id(mosfet_leds).current_values; + const float floor = id(min_brightness_pct) / 100.0f; + return (!cv.is_on()) || (cv.get_brightness() + 0.0005f < floor); + then: + - light.turn_on: + id: mosfet_leds + brightness: !lambda 'return id(min_brightness_pct) / 100.0f;' + transition_length: 80ms + # Ramp from current (>= floor) to cap over a fraction of ramp_up_ms. + - light.turn_on: + id: mosfet_leds + brightness: !lambda 'return id(max_brightness_pct) / 100.0f;' + transition_length: !lambda |- + const auto &cv = id(mosfet_leds).current_values; + const float floor = id(min_brightness_pct) / 100.0f; + const float cap = id(max_brightness_pct) / 100.0f; + if (cap <= floor + 0.0005f) { id(last_ramp_ms) = 0; return (uint32_t) 0; } + + float curr = cv.is_on() ? cv.get_brightness() : 0.0f; + + if (curr + 0.0005f < floor) { + id(last_ramp_ms) = id(ramp_up_ms); + return (uint32_t) id(last_ramp_ms); + } + if (curr > cap) curr = cap; + if (curr < floor) curr = floor; + float frac = (cap - curr) / (cap - floor); + if (frac < 0.0f) frac = 0.0f; + if (frac > 1.0f) frac = 1.0f; + id(last_ramp_ms) = (int) (id(ramp_up_ms) * frac); + return (uint32_t) id(last_ramp_ms); + - delay: !lambda 'return (uint32_t) id(last_ramp_ms);' + - script.stop: led_flash_up + - output.turn_off: green_led_out + - lambda: |- + id(is_ramping) = false; + const auto &cv = id(mosfet_leds).current_values; + if (cv.is_on()) { + float pct = cv.get_brightness() * 100.0f; + id(last_brightness_pct) = pct; // persist final level + if (pct > 0.5f) id(last_nonzero_brightness_pct) = pct; + } + + # Script: ramp to a specific target (Restore Brightness path) + - id: ramp_to_target_script + mode: restart + then: + - lambda: 'id(is_ramping) = true;' + - script.stop: ramp_off_script + - script.stop: led_flash_down + - script.execute: led_flash_up + # Ensure we start at the floor cleanly. + - if: + condition: + lambda: |- + const auto &cv = id(mosfet_leds).current_values; + const float floor = id(min_brightness_pct) / 100.0f; + return (!cv.is_on()) || (cv.get_brightness() + 0.0005f < floor); + then: + - light.turn_on: + id: mosfet_leds + brightness: !lambda 'return id(min_brightness_pct) / 100.0f;' + transition_length: 80ms + # Ramp from current (>= floor) to restore_target_pct over a fraction of ramp_up_ms. + - light.turn_on: + id: mosfet_leds + brightness: !lambda |- + float cap_pct = id(restore_target_pct); + float maxp = (float) id(max_brightness_pct); + if (cap_pct > maxp) cap_pct = maxp; + return cap_pct / 100.0f; + transition_length: !lambda |- + const auto &cv = id(mosfet_leds).current_values; + const float floor = id(min_brightness_pct) / 100.0f; + float cap = id(restore_target_pct) / 100.0f; + const float maxp = id(max_brightness_pct) / 100.0f; + if (cap > maxp) cap = maxp; + if (cap <= floor + 0.0005f) { id(last_ramp_ms) = 0; return (uint32_t) 0; } + + float curr = cv.is_on() ? cv.get_brightness() : 0.0f; + if (curr + 0.0005f < floor) { + id(last_ramp_ms) = id(ramp_up_ms); + return (uint32_t) id(last_ramp_ms); + } + + if (curr > cap) curr = cap; + if (curr < floor) curr = floor; + float frac = (cap - curr) / (cap - floor); + if (frac < 0.0f) frac = 0.0f; + if (frac > 1.0f) frac = 1.0f; + id(last_ramp_ms) = (int) (id(ramp_up_ms) * frac); + return (uint32_t) id(last_ramp_ms); + - delay: !lambda 'return (uint32_t) id(last_ramp_ms);' + - script.stop: led_flash_up + - output.turn_off: green_led_out + - lambda: |- + id(is_ramping) = false; + const auto &cv = id(mosfet_leds).current_values; + if (cv.is_on()) { + float pct = cv.get_brightness() * 100.0f; + id(last_brightness_pct) = pct; // persist final level + if (pct > 0.5f) id(last_nonzero_brightness_pct) = pct; + } + + # Script: ramp down from current level to floor, then cleanly cut to OFF + - id: ramp_off_script + mode: restart + then: + - lambda: |- + id(is_ramping) = true; + id(ramping_for_off) = true; + - script.stop: ramp_on_script + - script.stop: led_flash_up + - script.execute: led_flash_down + - light.turn_on: + id: mosfet_leds + brightness: !lambda 'return id(min_brightness_pct) / 100.0f;' + transition_length: !lambda |- + const auto &cv = id(mosfet_leds).current_values; + const float floor = id(min_brightness_pct) / 100.0f; + float curr = cv.get_brightness(); + if (curr < floor) curr = floor; + const float denom = (1.0f - floor); + if (denom <= 0.0005f) { id(last_ramp_ms) = 0; return (uint32_t) 0; } + float frac = (curr - floor) / denom; + if (frac < 0.0f) frac = 0.0f; + if (frac > 1.0f) frac = 1.0f; + id(last_ramp_ms) = (int) (id(ramp_down_ms) * frac); + return (uint32_t) id(last_ramp_ms); + - delay: !lambda 'return (uint32_t) id(last_ramp_ms);' + - light.turn_off: + id: mosfet_leds + transition_length: 150ms + - delay: 150ms + - script.stop: led_flash_down + - output.turn_off: green_led_out + - lambda: |- + id(is_ramping) = false; + id(ramping_for_off) = false; + id(last_brightness_pct) = 0.0f; // persist OFF for restore path + id(last_set_pos) = 0; + id(led_output_set_pct).publish_state(0); + + - id: max_on_watchdog + mode: restart + then: + - if: + condition: + lambda: 'return id(max_on_hours) > 0;' + then: + - delay: !lambda 'return (uint32_t) (id(max_on_hours) * 3600000UL);' + - if: + condition: + lambda: 'return id(mosfet_leds).current_values.is_on();' + then: + - lambda: |- + id(ramp_switch_target_on) = false; + id(mosfet_ramp_switch).publish_state(false); + - script.stop: ramp_on_script + - script.execute: ramp_off_script + + # Deferred restore to avoid early-boot races and ensure we apply the last saved level + - id: deferred_restore_brightness + mode: restart + then: + - delay: 5s # let preferences/globals load and light settle + - lambda: |- + // Use ONLY the exact last saved brightness. + float target = id(last_brightness_pct); + + // Clamp into [min..max] only if we are restoring a non-zero target + float minp = (float) id(min_brightness_pct); + float maxp = (float) id(max_brightness_pct); + if (target > 0.0f && target < minp) target = minp; + if (target > maxp) target = maxp; + + id(restore_target_pct) = target; + + - if: + condition: + lambda: 'return id(restore_target_pct) > 0.5f;' + then: + - lambda: |- + id(ramp_switch_target_on) = true; // reflect our intent + id(suppress_slider_sync) = true; + - script.stop: ramp_off_script + - script.execute: ramp_to_target_script + - delay: !lambda 'return (uint32_t) id(last_ramp_ms);' + - lambda: 'id(suppress_slider_sync) = false;' + else: + # Exact last was 0 -> remain OFF + - lambda: 'id(ramp_switch_target_on) = false;' + - light.turn_off: + id: mosfet_leds + transition_length: 0s diff --git a/esphome/esp-loungecabinetleds.yaml b/esphome/esp-loungecabinetleds2.yaml similarity index 66% rename from esphome/esp-loungecabinetleds.yaml rename to esphome/esp-loungecabinetleds2.yaml index 3e69dee..3a911a0 100644 --- a/esphome/esp-loungecabinetleds.yaml +++ b/esphome/esp-loungecabinetleds2.yaml @@ -1,102 +1,137 @@ ########################################################################################## ########################################################################################## -# Title: Lounge cabinet LEDS (Lounge) -# Hardware: Magichome inline single channel dimmer (ESP8266) -# -# Repo: +# Title: LOUNGE CABINET LEDS +# Repo: https://home.fox.co.nz/gitea/zorruno/zorruno-homeassistant/src/branch/master/esphome/esp-loungecabinetleds.yaml # -# v1.0 - 2025-08-17 First setup (and replacement of Tasmota) +# v2.0 - 2025-09-15 A bunch of fixes, this is now the master for other similar mosfet controllers of mine +# v1.5 - 2025-09-11 First setup (copied from another template) # # ------------------------------------------ -# DEVICE GPIO (Magichome ESP8266) +# OPERATION (as of v2.0) # ------------------------------------------ -# GPIO02 Blue LED (used for ESPHome status) - I think... can't see the board -# GPIO12 MOSFET output (0 V when switched) -# -# ------------------------------------------ -# OPERATION -# ------------------------------------------ -# - Startup/power restore action +# 1. General-purpose LED controller. +# 2. Designed for the Sinilink XY-WFMS board with a MOSFET output (claimed 5A, 5-36 V DC), but will suit similar mosfet boards. +# 3. (In SUBSTITUTIONS) setting for MAX % output to extend LED life (ie can ensure it is only ever 95%) +# 4. (In SUBSTITUTIONS) Minimum output setting; switches fully OFF at/below the minimum to avoid low-PWM flicker. +# 5. (In SUBSTITUTIONS) PWM frequency is set to 1kHz by default. You can increase it, but higher values caused +# resets on Sinilink device. On ESP32 you can run much higher (~40 kHz). +# 6. Min/Max output settings are not exposed in Home Assistant/MQTT by default, but could be. +# With a 1 MB flash, space is tight and only minimal optimisation has been done so far. +# 7. (PACKAGES) include common items: network settings, diagnostic entities, MQTT, and SNTP (optional). +# 8. Default behaviours, for recovery from lost power or reset: +# - "Fade up to full": fade from floor to max on boot. +# - "Restore Brightness": fade from floor to the last non-zero brightness on boot (should be no on/off blip/flash). +# - "Remain Off": stays off on boot. +# 9. The green LED flashes while fading (different patterns for up/down). The red LED follows the +# output (it shares the MOSFET GPIO). (Obviously this depends on hardware) +# 10. Fade timing scales with the configured values (proportionally when starting mid-brightness). +# 11. Exposed in Home Assistant/MQTT: +# - Startup action (On,Off,Restore) # - Fade Up / Fade Down / Fade Stop buttons -# - Fade Up/Down switch -# - Normal On/Off switch (quick ramp up/down) +# - Fade Up/Down toggle switch (keeps fading up to full, or down to off unless stopped) +# - Normal On/Off switch (quick ramp up/down, up to the level of previous brightness) # - Fade up/down times (0-60s) # - Output % (pre-gamma) and PWM % (post-gamma) -# - Output Set (1-100, respects min/max) -# - Device diagnostics (from the included package) +# - Output Set (1-100, respects min/max). This also changes with other output so reflects value. +# - Many device diagnostics (from the included 'diagnostics' package) # - Maximum 'on' time before automatic fade-down (1-48 h, 0 = no limit) +########################################################################################### +# Hardware: Sinilink MOSFET Board XY-WFMS (ESP8266) - sometimes listed as "XY-VFMS" +# https://devices.esphome.io/devices/Sinilink-XY-WFMS +# ------------------------------------------ +# DEVICE GPIO (Sinilink XY-WFMS) +# ------------------------------------------ +# GPIO02 Blue LED, active-low (used for ESPHome status) +# GPIO04 MOSFET output (0V when switched) and Red LED +# GPIO12 Toggle button +# GPIO13 Green LED (used to display fading status) +# +# 3D-printed case for Sinilink XY-WFMS +# https://cults3d.com/en/3d-model/tool/snapfit-enclosure-for-esp8266-sinilink-xy-wfms-5v-36v-mosfet-switch-module # ########################################################################################## ########################################################################################## - ########################################################################################## # 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-loungecabinetleds" + device_name: "esp-loungecabinetleds2" # yaml file should be device_name.yaml friendly_name: "Lounge Cabinet LEDs" - description_comment: "LED Strip inside the display cabinet upstairs lounge :: Magichome ESP8266" + description_comment: "LED Strip inside the display cabinet upstairs lounge :: Sinilink XY-WFMS" device_area: "Lounge" # Allows the ESP device to be automatically linked to an 'Area' in Home Assistant. # Project Naming - project_name: "Magichome ESP8266" # Project details - project_version: "v1.0" # Project version denotes release of the YAML file, allowing checking of deployed vs latest version + project_name: "Sinilink.XY-WFMS" # Project details. Manufacturer before the dot, device after the dot. + project_version: "v2.0" # Project version denotes release of the YAML file, allowing checking of deployed vs latest version - # Passwords & Secrets - api_key: !secret esp-api_key - ota_pass: !secret esp-ota_pass - static_ip_address: !secret esp-loungecabinetleds_ip # Unfortunately, you can't use substitutions inside secret names + # Passwords & Secrets (Unfortunately, you can't use substitutions inside secret names) + api_key: !secret esp-api_key # If using HA API + ota_pass: !secret esp-ota_pass # If passwording OTA updates + static_ip_address: !secret esp-loungecabinetleds2_ip # CHANGE THIS + + # MQTT LOCAL Controls (This is just my local method of controlling via MQTT, I also use the built in method) + mqtt_local_device_name: "lounge-cabinetleds" mqtt_local_command_main_topic: !secret mqtt_local_command_main_topic mqtt_local_status_main_topic: !secret mqtt_local_status_main_topic - - # MQTT LOCAL Controls - mqtt_local_device_name: "lounge-cabinetleds" mqtt_local_command_topic: "${mqtt_local_command_main_topic}/${mqtt_local_device_name}" # Topic we will use to command this locally without HA mqtt_local_status_topic: "${mqtt_local_status_main_topic}/${mqtt_local_device_name}" # Topic we will use to view status locally without HA mqtt_local_device_command_ON: "ON" mqtt_local_device_command_OFF: "OFF" # Device Specific Settings - log_level: "NONE" # Define logging level: NONE, ERROR, WARN, INFO, DEBUG (default), VERBOSE, VERY_VERBOSE - update_interval: "20s" # Update time for general sensors, etc. - led_gamma: "1.2" # Gamma from 1.2-3 is sensible to normalise the LED fading vs PWM - minimum_led_output: "1" # % If at this value or below, we'll switch it completely off - maximum_led_output: "90" # % Maximum output; it is sometimes nice to limit the output for longevity or aesthetics - max_on_default_hours: "12" # The maximum time the LEDs will be on, in case they get left on. 0 = no automatic turn-off + log_level: "NONE" # Define logging level: NONE, ERROR, WARN, INFO, DEBUG (default), VERBOSE, VERY_VERBOSE + update_interval: "20s" # Update time for general sensors, etc. + led_gamma: "1.2" # Gamma from 1.2-3 is sensible to normalise the LED fading vs PWM + minimum_led_output: "1" # % If at this value or below, we'll switch it completely off + maximum_led_output: "90" # % Maximum output; it is sometimes nice to limit the output for longevity or aesthetics + max_on_default_hours: "24" # The maximum time the LEDs will be on, in case they get left on. 0 = no automatic turn-off + pwm_frequency: "1000hz" # PWM output Frequency. High enough to avoid audible/visible artifacts + + # Device Specific GPIO (so we can easily update for other devices) + device_status_led: GPIO02 + device_mosfet_out: GPIO04 + device_usr_button: GPIO12 + device_fading_led: GPIO13 ########################################################################################## # 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}" + + #### 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}" - #common_webportal: !include - # file: common/webportal_common.yaml + + #### MQTT #### common_mqtt: !include file: common/mqtt_common.yaml vars: local_device_name: "${device_name}" - #common_sntp: !include - # file: common/sntp_common.yaml - common_general_sensors: !include - file: common/sensors_common.yaml - #file: common/sensors_common_lite.yaml - vars: - local_friendly_name: "${friendly_name}" - local_update_interval: "${update_interval}" + + #### 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 CORE CONFIGURATION @@ -108,7 +143,8 @@ esphome: comment: "${description_comment}" area: "${device_area}" on_boot: - priority: 600 + # Run later so everything is initialised before we touch states. + priority: 200 then: # Keep the HA dropdown in sync with the stored mode - lambda: |- @@ -126,31 +162,12 @@ esphome: - lambda: 'id(ramp_switch_target_on) = true;' - script.execute: ramp_on_script - # Mode 1: Restore Brightness using a scripted ramp 0 -> last non-zero brightness + # Mode 1: Restore Brightness (deferred to avoid races) - if: condition: lambda: 'return id(restart_mode) == 1;' then: - - lambda: |- - float target = id(last_nonzero_brightness_pct); - if (target < 0.0f) target = 0.0f; - if (target > 100.0f) target = 100.0f; - const float minp = (float) id(min_brightness_pct); - const float maxp = (float) id(max_brightness_pct); - if (target > 0.0f && target < minp) target = minp; - if (target > maxp) target = maxp; - id(restore_target_pct) = target; // set scripted target - id(ramp_switch_target_on) = (target > 0.0f); - - if: - condition: - lambda: 'return id(restore_target_pct) > 0.0f;' - then: - - script.stop: ramp_off_script - - script.execute: ramp_to_target_script - else: - - light.turn_off: - id: mosfet_leds - transition_length: 0s + - script.execute: deferred_restore_brightness # Mode 2: Remain Off (no blip; stays off) - if: @@ -164,15 +181,19 @@ esphome: id: mosfet_leds transition_length: 0s - platformio_options: - build_unflags: - - -flto - build_flags: - - -fno-lto - - -Wl,--gc-sections - - -ffunction-sections - - -fdata-sections - - -DNDEBUG + # Boot complete: allow OFF handlers again + - lambda: 'id(booting) = false;' + +# Only if you want to play with build flags... +# platformio_options: +# build_unflags: +# - -flto +# build_flags: +# - -fno-lto +# - -Wl,--gc-sections +# - -ffunction-sections +# - -fdata-sections +# - -DNDEBUG ########################################################################################## # ESP PLATFORM AND FRAMEWORK @@ -184,10 +205,10 @@ esp8266: restore_from_flash: true # restore some values on reboot mdns: - disabled: false # Disabling will make the build file smaller (and it is still available via static IP) + disabled: false # Disabling will make the build file smaller (and device is still available via static IP) preferences: - flash_write_interval: 5min + flash_write_interval: 5sec # enough time to update values for reboots, but not enough to wear flash ########################################################################################## # GLOBAL VARIABLES @@ -208,7 +229,8 @@ globals: - id: max_on_hours type: int restore_value: true - initial_value: '${max_on_default_hours}' + initial_value: "${max_on_default_hours}" + # Default Fading Up Time (Selectable and will be retained) - id: ramp_up_ms # fade-in when turned ON type: int @@ -272,6 +294,16 @@ globals: type: float restore_value: false initial_value: '0.0' + # guards OFF persistence during startup caused by restore_mode: ALWAYS_OFF + - id: booting + type: bool + restore_value: false + initial_value: 'true' +# true only while we are performing a ramp-down that briefly turns the light ON to hit the floor + - id: ramping_for_off + type: bool + restore_value: false + initial_value: 'false' ########################################################################################## # LOGGER COMPONENT @@ -282,6 +314,15 @@ logger: level: "${log_level}" # INFO Level 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 (eg PZEM, Serial control) +######################################################################################### +# STATUS LED +# https://esphome.io/components/status_led.html +######################################################################################### +status_led: + pin: + number: ${device_status_led} + inverted: true + ########################################################################################## # MQTT COMMANDS # This adds device-specific MQTT command triggers to the common MQTT configuration. @@ -299,15 +340,6 @@ mqtt: then: - switch.turn_off: mosfet_ramp_switch -######################################################################################### -# STATUS LED -# https://esphome.io/components/status_led.html -######################################################################################### -status_led: - pin: - number: GPIO2 - inverted: true - ########################################################################################## # SWITCH COMPONENT # https://esphome.io/components/switch/ @@ -366,12 +398,18 @@ button: name: "${friendly_name} Fade Stop" icon: mdi:pause on_press: - # Stop any pending scripts (and their delayed turn_off) + # Stop any pending scripts (and their delayed actions) - script.stop: ramp_on_script - script.stop: ramp_off_script - # Cancel the light's transition by commanding the current level with 0 ms, - # but DO NOT change the ramp switch state/flag. + - script.stop: led_flash_up + - script.stop: led_flash_down + - output.turn_off: green_led_out - lambda: |- + // We are no longer ramping (up or down) + id(ramping_for_off) = false; + id(is_ramping) = false; + + // Freeze the light at its *current* level by overwriting the transition const auto &cv = id(mosfet_leds).current_values; if (cv.is_on()) { auto call = id(mosfet_leds).make_call(); @@ -381,6 +419,14 @@ button: call.perform(); } + // Persist the exact frozen level so "Restore Brightness" survives a reboot + if (cv.is_on()) { + float pct = cv.get_brightness() * 100.0f; + id(last_brightness_pct) = pct; + if (pct > 0.5f) id(last_nonzero_brightness_pct) = pct; + } + + ######################################################################################### # SELECT COMPONENT # https://esphome.io/components/select/index.html @@ -410,7 +456,37 @@ select: # BINARY SENSORS # https://esphome.io/components/binary_sensor/ ######################################################################################### -#binary_sensor: +binary_sensor: + - platform: gpio + id: btn_gpio12 + name: "${friendly_name} Button" + pin: + number: ${device_usr_button} + mode: + input: true + pullup: true + inverted: true + filters: + - delayed_on: 20ms + - delayed_off: 20ms + on_press: + - if: + condition: + lambda: 'return id(ramp_switch_target_on);' + then: + # Target is currently ON -> press should go OFF (start ramp-down) + - lambda: |- + id(ramp_switch_target_on) = false; + id(mosfet_ramp_switch).publish_state(false); // reflect in HA immediately + - script.stop: ramp_on_script + - script.execute: ramp_off_script + else: + # Target is currently OFF -> press should go ON (start ramp-up) + - lambda: |- + id(ramp_switch_target_on) = true; + id(mosfet_ramp_switch).publish_state(true); // reflect in HA immediately + - script.stop: ramp_off_script + - script.execute: ramp_on_script ########################################################################################## # SENSOR COMPONENT @@ -430,16 +506,22 @@ sensor: on_value: then: - lambda: |- - // Remember latest actual output (0..100) for "Restore Brightness" - id(last_brightness_pct) = x; - if (x > 0.5f) { - // Only persist when not ramping; always track a volatile copy - if (!id(is_ramping)) id(last_nonzero_brightness_pct) = x; - id(last_nonzero_tmp) = x; + const auto &cv_now = id(mosfet_leds).current_values; + + // Only persist exact last brightness while actually ON, + // and not during the OFF ramp's brief floor-kick. + if (cv_now.is_on()) { + if (x > 0.5f) { + if (!id(is_ramping)) id(last_nonzero_brightness_pct) = x; + id(last_nonzero_tmp) = x; + } + } else { + if (x > 0.5f) id(last_nonzero_tmp) = x; } + // If not suppressing sync, update the 0..100 slider only when its INT changes if (!id(suppress_slider_sync)) { - float actual = x; // actual % + float actual = x; float minp = (float) id(min_brightness_pct); float maxp = (float) id(max_brightness_pct); if (maxp <= minp) maxp = minp + 1.0f; @@ -463,7 +545,7 @@ sensor: lambda: |- const auto &cv = id(mosfet_leds).current_values; if (!cv.is_on()) return 0.0f; - const float lin = cv.get_brightness(); // 0..1 (linear brightness) + const float lin = cv.get_brightness(); // 0..1 linear brightness const float gamma = atof("${led_gamma}"); // parse substitution string -> float float pwm = powf(lin, gamma); // approx PWM duty after gamma if (pwm < 0.0f) pwm = 0.0f; @@ -480,8 +562,13 @@ sensor: output: - platform: esp8266_pwm id: mosfet_pwm - pin: GPIO12 - frequency: 1000 Hz # higher frequency helps reduce camera banding while filming + pin: ${device_mosfet_out} + frequency: ${pwm_frequency} + - platform: gpio + id: green_led_out # Green LED + pin: + number: ${device_fading_led} + inverted: false ########################################################################################## # LIGHT COMPONENT @@ -492,29 +579,48 @@ light: id: mosfet_leds name: "${friendly_name}" output: mosfet_pwm - restore_mode: ALWAYS_OFF + restore_mode: RESTORE_DEFAULT_OFF default_transition_length: 2s icon: mdi:led-strip-variant gamma_correct: "${led_gamma}" + + # ON: publish state, track intent, arm watchdog if configured on_turn_on: - mqtt.publish: topic: "${mqtt_local_status_topic}/light/state" payload: "${mqtt_local_device_command_ON}" retain: true - - lambda: 'id(ramp_switch_target_on) = true;' + - lambda: |- + // Do not flip "intent to ON" when we're inside an OFF ramp's floor-kick + if (!id(ramping_for_off)) { + id(ramp_switch_target_on) = true; + } - script.stop: max_on_watchdog - if: condition: lambda: 'return id(max_on_hours) > 0;' then: - script.execute: max_on_watchdog + + # OFF: publish state, track intent, clear watchdog, and persist OFF so Mode 1 will not restore + # Guarded by booting flag so the automatic OFF at boot from ALWAYS_OFF does not zero persistence. on_turn_off: - - mqtt.publish: - topic: "${mqtt_local_status_topic}/light/state" - payload: "${mqtt_local_device_command_OFF}" - retain: true - - lambda: 'id(ramp_switch_target_on) = false;' - - script.stop: max_on_watchdog + - if: + condition: + lambda: 'return !id(booting);' + then: + - mqtt.publish: + topic: "${mqtt_local_status_topic}/light/state" + payload: "${mqtt_local_device_command_OFF}" + retain: true + - lambda: 'id(ramp_switch_target_on) = false;' + - script.stop: max_on_watchdog + - lambda: |- + id(last_brightness_pct) = 0.0f; + id(last_set_pos) = 0; + id(led_output_set_pct).publish_state(0); + + # Cap brightness to max_brightness_pct without a visible step on_state: - lambda: |- const float cap = id(max_brightness_pct) / 100.0f; @@ -607,6 +713,8 @@ number: - lambda: |- id(ramp_switch_target_on) = false; id(led_output_set_pct).publish_state(0); + id(last_brightness_pct) = 0.0f; // persist OFF immediately + id(last_set_pos) = 0; - delay: 400ms - lambda: 'id(suppress_slider_sync) = false;' else: @@ -630,7 +738,19 @@ number: if (out_pct > maxp) out_pct = maxp; return out_pct / 100.0f; transition_length: 250ms - - lambda: 'id(ramp_switch_target_on) = true;' + - lambda: |- + id(ramp_switch_target_on) = true; + float pos = id(led_output_set_pct).state; // 1..100 + float minp = (float) id(min_brightness_pct); + float maxp = (float) id(max_brightness_pct); + if (maxp <= minp) maxp = minp + 1.0f; + float out_pct = minp + (pos * (maxp - minp) / 100.0f); + if (out_pct > maxp) out_pct = maxp; + id(last_brightness_pct) = out_pct; // persist exact target now + if (out_pct > 0.5f) { + id(last_nonzero_brightness_pct) = out_pct; + id(last_nonzero_tmp) = out_pct; + } - delay: 400ms - lambda: 'id(suppress_slider_sync) = false;' @@ -670,12 +790,43 @@ number: # Scripts can be executed nearly anywhere in your device configuration with a single call. ########################################################################################## script: + # Blink pattern while ramping UP: quick double-blink, pause, repeat + - id: led_flash_up + mode: restart + then: + - while: + condition: + lambda: 'return true;' + then: + - output.turn_on: green_led_out + - delay: 100ms + - output.turn_off: green_led_out + - delay: 100ms + - output.turn_on: green_led_out + - delay: 100ms + - output.turn_off: green_led_out + - delay: 400ms + # Blink pattern while ramping DOWN: steady slow blink + - id: led_flash_down + mode: restart + then: + - while: + condition: + lambda: 'return true;' + then: + - output.turn_on: green_led_out + - delay: 250ms + - output.turn_off: green_led_out + - delay: 250ms + # Script: ramp up from current level. Obey global max. - id: ramp_on_script mode: restart then: - - lambda: 'id(is_ramping) = true;' # mark ramping + - lambda: 'id(is_ramping) = true;' - script.stop: ramp_off_script + - script.stop: led_flash_down + - script.execute: led_flash_up # Ensure we start at at least the floor without a visible "pop". - if: condition: @@ -696,18 +847,14 @@ script: const auto &cv = id(mosfet_leds).current_values; const float floor = id(min_brightness_pct) / 100.0f; const float cap = id(max_brightness_pct) / 100.0f; - if (cap <= floor + 0.0005f) { id(last_ramp_ms) = 0; return (uint32_t) 0; } // guard degenerate case + if (cap <= floor + 0.0005f) { id(last_ramp_ms) = 0; return (uint32_t) 0; } - // IMPORTANT: when the light was OFF, treat current as 0 so we don't compute a 0ms jump to cap. float curr = cv.is_on() ? cv.get_brightness() : 0.0f; - // If we're below the floor (i.e. coming from OFF), we just did the "floor kick" above. - // Use full ramp_up_ms for a smooth 0 -> cap ramp. if (curr + 0.0005f < floor) { id(last_ramp_ms) = id(ramp_up_ms); return (uint32_t) id(last_ramp_ms); } - if (curr > cap) curr = cap; if (curr < floor) curr = floor; float frac = (cap - curr) / (cap - floor); @@ -716,20 +863,25 @@ script: id(last_ramp_ms) = (int) (id(ramp_up_ms) * frac); return (uint32_t) id(last_ramp_ms); - delay: !lambda 'return (uint32_t) id(last_ramp_ms);' + - script.stop: led_flash_up + - output.turn_off: green_led_out - lambda: |- - id(is_ramping) = false; // ramp complete + id(is_ramping) = false; const auto &cv = id(mosfet_leds).current_values; if (cv.is_on()) { float pct = cv.get_brightness() * 100.0f; - if (pct > 0.5f) id(last_nonzero_brightness_pct) = pct; // commit once + id(last_brightness_pct) = pct; // persist final level + if (pct > 0.5f) id(last_nonzero_brightness_pct) = pct; } # Script: ramp to a specific target (Restore Brightness path) - id: ramp_to_target_script mode: restart then: - - lambda: 'id(is_ramping) = true;' # mark ramping + - lambda: 'id(is_ramping) = true;' - script.stop: ramp_off_script + - script.stop: led_flash_down + - script.execute: led_flash_up # Ensure we start at the floor cleanly. - if: condition: @@ -756,12 +908,9 @@ script: float cap = id(restore_target_pct) / 100.0f; const float maxp = id(max_brightness_pct) / 100.0f; if (cap > maxp) cap = maxp; - if (cap <= floor + 0.0005f) { id(last_ramp_ms) = 0; return (uint32_t) 0; } // guard + if (cap <= floor + 0.0005f) { id(last_ramp_ms) = 0; return (uint32_t) 0; } - // Same OFF case handling: if off, treat curr as 0 so we don't collapse the ramp. float curr = cv.is_on() ? cv.get_brightness() : 0.0f; - - // Coming from OFF/below floor? We just did the "floor kick" above — use full ramp_up_ms. if (curr + 0.0005f < floor) { id(last_ramp_ms) = id(ramp_up_ms); return (uint32_t) id(last_ramp_ms); @@ -775,20 +924,27 @@ script: id(last_ramp_ms) = (int) (id(ramp_up_ms) * frac); return (uint32_t) id(last_ramp_ms); - delay: !lambda 'return (uint32_t) id(last_ramp_ms);' + - script.stop: led_flash_up + - output.turn_off: green_led_out - lambda: |- - id(is_ramping) = false; // ramp complete + id(is_ramping) = false; const auto &cv = id(mosfet_leds).current_values; if (cv.is_on()) { float pct = cv.get_brightness() * 100.0f; - if (pct > 0.5f) id(last_nonzero_brightness_pct) = pct; // commit once + id(last_brightness_pct) = pct; // persist final level + if (pct > 0.5f) id(last_nonzero_brightness_pct) = pct; } # Script: ramp down from current level to floor, then cleanly cut to OFF - id: ramp_off_script mode: restart then: - - lambda: 'id(is_ramping) = true;' # mark ramping + - lambda: |- + id(is_ramping) = true; + id(ramping_for_off) = true; - script.stop: ramp_on_script + - script.stop: led_flash_up + - script.execute: led_flash_down - light.turn_on: id: mosfet_leds brightness: !lambda 'return id(min_brightness_pct) / 100.0f;' @@ -798,7 +954,7 @@ script: float curr = cv.get_brightness(); if (curr < floor) curr = floor; const float denom = (1.0f - floor); - if (denom <= 0.0005f) { id(last_ramp_ms) = 0; return (uint32_t) 0; } // guard near-1.0 floor + if (denom <= 0.0005f) { id(last_ramp_ms) = 0; return (uint32_t) 0; } float frac = (curr - floor) / denom; if (frac < 0.0f) frac = 0.0f; if (frac > 1.0f) frac = 1.0f; @@ -809,12 +965,15 @@ script: id: mosfet_leds transition_length: 150ms - delay: 150ms + - script.stop: led_flash_down + - output.turn_off: green_led_out - lambda: |- - id(is_ramping) = false; // ramp complete - auto call = id(mosfet_leds).make_call(); - call.set_state(false); - call.set_brightness(id(max_brightness_pct) / 100.0f); - call.perform(); + id(is_ramping) = false; + id(ramping_for_off) = false; + id(last_brightness_pct) = 0.0f; // persist OFF for restore path + id(last_set_pos) = 0; + id(led_output_set_pct).publish_state(0); + - id: max_on_watchdog mode: restart then: @@ -832,3 +991,38 @@ script: id(mosfet_ramp_switch).publish_state(false); - script.stop: ramp_on_script - script.execute: ramp_off_script + + # Deferred restore to avoid early-boot races and ensure we apply the last saved level + - id: deferred_restore_brightness + mode: restart + then: + - delay: 5s # let preferences/globals load and light settle + - lambda: |- + // Use ONLY the exact last saved brightness. + float target = id(last_brightness_pct); + + // Clamp into [min..max] only if we are restoring a non-zero target + float minp = (float) id(min_brightness_pct); + float maxp = (float) id(max_brightness_pct); + if (target > 0.0f && target < minp) target = minp; + if (target > maxp) target = maxp; + + id(restore_target_pct) = target; + + - if: + condition: + lambda: 'return id(restore_target_pct) > 0.5f;' + then: + - lambda: |- + id(ramp_switch_target_on) = true; // reflect our intent + id(suppress_slider_sync) = true; + - script.stop: ramp_off_script + - script.execute: ramp_to_target_script + - delay: !lambda 'return (uint32_t) id(last_ramp_ms);' + - lambda: 'id(suppress_slider_sync) = false;' + else: + # Exact last was 0 -> remain OFF + - lambda: 'id(ramp_switch_target_on) = false;' + - light.turn_off: + id: mosfet_leds + transition_length: 0s diff --git a/esphome/esp-mainkitchenlights.yaml b/esphome/esp-mainkitchenlights.yaml new file mode 100644 index 0000000..84882b6 --- /dev/null +++ b/esphome/esp-mainkitchenlights.yaml @@ -0,0 +1,181 @@ +########################################################################################## +########################################################################################## +# Main Kitchen LIGHTSWITCH +# V3.7 2025-09-14 upload to this lightswitch from copy +# +########################################################################################## +# Zemismart KS-811 Double push button +# pinout/schematic https://community.home-assistant.io/t/zemismart-ks-811-working-with-esphome/ +# +# NOTES +# - Light switch with 2 sets of lights +# +########################################################################################## +########################################################################################## + +########################################################################################## +# 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-mainkitchenlights" + friendly_name: "Main Kitchen Lightswitch (2)" + description_comment: "Main Kitchen lightswitch using a Zemismart KS-811 Double Push Button. Main Lights (1), Bench Lights (2)" + device_area: "Kitchen" # Allows ESP device to be automatically linked to an 'Area' in Home Assistant. + + # Project Naming + project_name: "Zemismart Technologies.KS-811 Double" # Project Details + project_version: "v3.7" # Project V denotes release of yaml file, allowing checking of deployed vs latest version + + # 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-mainkitchenlights_ip + #mqtt_local_command_main_topic: !secret mqtt_local_command_main_topic + #mqtt_local_status_main_topic: !secret mqtt_local_status_main_topic + + # Device Settings + #relay_icon: "mdi:lightbulb-group" + log_level: "INFO" # Define logging level: NONE, ERROR, WARN, INFO, DEBUG (Default), VERBOSE, VERY_VERBOSE + update_interval: "60s" # update time for for general sensors etc + + # MQTT LOCAL Controls + #mqtt_device_name: "bedroom2-lights" + #mqtt_local_command_topic: "${mqtt_local_command_main_topic}/${mqtt_device_name}" # Topic we will use to command this locally without HA + #mqtt_local_status_topic: "${mqtt_local_status_main_topic}/${mqtt_device_name}" # Topic we will use to view status locally without HA + + # Switch Naming + switch_1_name: "Main Lights" + switch_2_name: "Benchtop Lights" + #switch_3_name: "Nil" + +######################################################################################### +# 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_ota_pass: "${ota_pass}" + common_api: !include + file: common/api_common.yaml + vars: + local_api_key: "${api_key}" + #common_webportal: !include + # file: common/webportal_common.yaml + common_mqtt: !include + file: common/mqtt_common.yaml + vars: + local_device_name: "${device_name}" + common_sntp: !include + file: common/sntp_common.yaml + common_general_sensors: !include + file: common/sensors_common.yaml + vars: + local_friendly_name: "${friendly_name}" + local_update_interval: "${update_interval}" + +######################################################################################### +# 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: +# priority: 200 +# then: +# - switch.turn_on: Relay_2 # Stays on permanently + +######################################################################################### +# ESP Platform and Framework +# https://esphome.io/components/esp32.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 + +######################################################################################### +# ESPHome Logging Enable +# https://esphome.io/components/logger.html +############################################# +logger: + level: "${log_level}" #INFO Level 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 (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: yes + +######################################################################################### +# BINARY SENSORS +# https://esphome.io/components/binary_sensor/ +######################################################################################### +binary_sensor: + - platform: gpio + pin: + number: GPIO16 + mode: INPUT + inverted: True + name: "Button 1: ${switch_1_name}" + on_press: + - switch.toggle: Relay_1 + + - platform: gpio + pin: + number: GPIO05 + mode: INPUT + inverted: True + name: "Button 2: ${switch_2_name}" + on_press: + - switch.toggle: Relay_2 + +# KS-811-2 is a double only +# - platform: gpio +# pin: +# number: GPIO4 +# mode: INPUT +# inverted: True +# name: "Button 3: ${switch_3_name}" +# on_press: +# - switch.toggle: Relay_3 + +######################################################################################### +# SWITCH COMPONENT +# https://esphome.io/components/switch/ +######################################################################################### +switch: + - platform: gpio + name: "Relay 1: ${switch_1_name}" + pin: GPIO13 + id: Relay_1 + + - platform: gpio + name: "Relay 2: ${switch_2_name}" + pin: GPIO12 + id: Relay_2 + +# KS-811-2 is a double only +# - platform: gpio +# name: "Relay 3: ${switch_3_name}" +# pin: GPIO14 +# id: Relay_3 + + diff --git a/esphome/esp-occupancyoffice.yaml b/esphome/esp-occupancyoffice.yaml index 3fbee39..fd162ce 100644 --- a/esphome/esp-occupancyoffice.yaml +++ b/esphome/esp-occupancyoffice.yaml @@ -262,7 +262,7 @@ interval: ############################################# number: - platform: template - name: ${friendly_name} LD1125H mth1 #mth1 is 0~2.8m Sensitivity. + name: "0-2.8m Sensitivity" # mth1 is 0~2.8m Sensitivity. id: LD1125H_mth1 icon: "mdi:cogs" optimistic: true @@ -279,12 +279,12 @@ number: std::string th1st = "mth1=" + str_sprintf("%.0f",x) +"\r\n"; return std::vector(th1st.begin(), th1st.end()); - platform: template - name: ${friendly_name} LD1125H mth2 #mth2 is 2.8~8m Sensitivity. + name: "2.8-8m Sensitivity" # mth2 is 2.8~8m Sensitivity. id: LD1125H_mth2 icon: "mdi:cogs" optimistic: true - restore_value: true #If you don't want to store the setting at ESP, set it to false. - initial_value: "30" #Default mth2 Setting + restore_value: true # If you don't want to store the setting at ESP, set it to false. + initial_value: "30" # Default mth2 Setting min_value: 5 max_value: 300 step: 5 @@ -296,12 +296,12 @@ number: std::string th2st = "mth2=" + str_sprintf("%.0f",x) +"\r\n"; return std::vector(th2st.begin(), th2st.end()); - platform: template - name: ${friendly_name} LD1125H mth3 #mth3 is above 8m Sensitivity. + name: "8m+ Sensitivity" # mth3 is above 8m Sensitivity. id: LD1125H_mth3 icon: "mdi:cogs" optimistic: true - restore_value: true #If you don't want to store the setting at ESP, set it to false. - initial_value: "20" #Default mth3 Setting + restore_value: true # If you don't want to store the setting at ESP, set it to false. + initial_value: "20" # Default mth3 Setting min_value: 5 max_value: 200 step: 5 @@ -313,7 +313,7 @@ number: std::string th3st = "mth3=" + str_sprintf("%.0f",x) +"\r\n"; return std::vector(th3st.begin(), th3st.end()); - platform: template - name: ${friendly_name} LD1125H rmax #rmax is max detection distance. + name: "Max Detection (m)" # rmax is max detection distance. id: LD1125H_rmax icon: "mdi:cogs" optimistic: true @@ -330,7 +330,7 @@ number: std::string rmaxst = "rmax=" + str_sprintf("%.1f",x) +"\r\n"; return std::vector(rmaxst.begin(), rmaxst.end()); - platform: template - name: ${friendly_name} LD1125H Clearence Time + name: "Clearence Time (s)" id: LD1125H_Clear_Time icon: "mdi:cogs" optimistic: true @@ -340,7 +340,7 @@ number: max_value: 20 step: 0.5 - platform: template - name: ${friendly_name} LD1125H Movement Time + name: "Movement Time (s)" id: LD1125H_Mov_Time icon: "mdi:cogs" optimistic: true diff --git a/esphome/esp-poollightpower.yaml b/esphome/esp-poollightpower.yaml index 8035658..c17588f 100644 --- a/esphome/esp-poollightpower.yaml +++ b/esphome/esp-poollightpower.yaml @@ -102,29 +102,55 @@ substitutions: # https://esphome.io/components/packages.html ########################################################################################## packages: + ################################################## + # MANDATORY packages (won't compile without them!) + ################################################## + + #### 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}" + + ################################################## + # OPTIONAL packages (comment if you don't want them) + ################################################## + + #### 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}" + + #### WEB PORTAL #### #common_webportal: !include # file: common/webportal_common.yaml + + #### MQTT #### common_mqtt: !include file: common/mqtt_common.yaml vars: local_device_name: "${device_name}" + + #### SNTP (Only use if you want/need accurate timeclocks) #### common_sntp: !include file: common/sntp_common.yaml + + #### DIAGNOSTICS Lite Sensors #### + common_lite_sensors: !include + file: common/sensors_common_lite.yaml + + #### DIAGNOSTICS General Sensors #### common_general_sensors: !include file: common/sensors_common.yaml - vars: - local_friendly_name: "${friendly_name}" - local_update_interval: "${update_interval}" + + #### DEBUGGING Sensors #### + # A count of various resets + #common_resetcount_debugging: !include + # file: common/resetcount_common.yaml # Device Specific included packages common_athompowermonV1: !include diff --git a/esphome/esp-poolpumppower.yaml b/esphome/esp-poolpumppower.yaml index b5d05b8..6a27bf2 100644 --- a/esphome/esp-poolpumppower.yaml +++ b/esphome/esp-poolpumppower.yaml @@ -86,29 +86,57 @@ substitutions: # https://esphome.io/components/packages.html ########################################################################################## packages: + ################################################## + # MANDATORY packages (won't compile without them!) + ################################################## + + #### 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}" + + ################################################## + # OPTIONAL packages (comment if you don't want them) + ################################################## + + #### 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}" + + #### WEB PORTAL #### #common_webportal: !include # file: common/webportal_common.yaml + + #### MQTT #### common_mqtt: !include file: common/mqtt_common.yaml vars: local_device_name: "${device_name}" + + #### SNTP (Only use if you want/need accurate timeclocks) #### common_sntp: !include file: common/sntp_common.yaml + + #### DIAGNOSTICS Lite Sensors #### + # Option of everything, or a lite version + common_lite_sensors: !include + file: common/sensors_common_lite.yaml + + #### DIAGNOSTICS General Sensors #### + # Option of everything, or a lite version common_general_sensors: !include file: common/sensors_common.yaml - vars: - local_friendly_name: "${friendly_name}" - local_update_interval: "${update_interval}" + + #### DEBUGGING Sensors #### + # A count of various resets + #common_resetcount_debugging: !include + # file: common/resetcount_common.yaml # Device Specific included packages common_athompowermonV3: !include diff --git a/esphome/esp-weatherstation.temp b/esphome/esp-weatherstation.temp index b255825..d42518e 100644 --- a/esphome/esp-weatherstation.temp +++ b/esphome/esp-weatherstation.temp @@ -1,18 +1,19 @@ -############################################# -############################################# +####################################################################################################### +####################################################################################################### # Opengreen energy weather station board, with ESP8266 D1 Mini # https://www.opengreenenergy.com/solar-powered-wifi-weather-station-v2-0/ -# zorruno 2024-05-04 V0.0 Untested -# zorruno 2024-05-23 V1.0 Tidied up sensor info and added BMP280 -# zorruno 2024-05-24 V1.1 Tested ok and added some V calibration for mine -# zorruno 2024-05-25 V2 A bunch more sensors added (wind direction not yet sorted) -# zorruno 2024-05-25 V3 Shuffled some sensors, rain count added -# zorruno 2024-06-03 V4 tweaks to battery and wind +# zorruno 2025-09-12 V6 fixes: AP SSID sub; ADC calibrate indent; NaN-safe temp avg; empty-string publishes; improved 10min gust (3s avg -> 10m max); comment tidy-ups # zorruno 2024-06-14 V5 added wind heading, and sorted breaking a change for OTA -############################################# -############################################# +# zorruno 2024-06-03 V4 tweaks to battery and wind +# zorruno 2024-05-25 V3 Shuffled some sensors, rain count added +# zorruno 2024-05-25 V2 A bunch more sensors added (wind direction not yet sorted) +# zorruno 2024-05-24 V1.1 Tested ok and added some V calibration for mine +# zorruno 2024-05-23 V1.0 Tidied up sensor info and added BMP280 +# zorruno 2024-05-04 V0.0 Untested +####################################################################################################### +####################################################################################################### -############################################# +####################################################################################################### # SENSORS INCLUDED # # TESTED WORKING @@ -55,43 +56,43 @@ # soil temp # soil moisture # -############################################# +####################################################################################################### -############################################# +####################################################################################################### # WEMOS D1 Mini GPIO, and Weather station Use -############################################# +####################################################################################################### # PIN HEADERS on Weatherstation board: -# D1, GPIO5 Used as SCL (I2C) -# D2, GPIO4 Used as SDA (I2C) -# D3, GPIO0 connected to FLASH button, boot fails if pulled LOW -# D4, GPIO2 connected to on-board LED, boot fails if pulled LOW -# D5, GPIO14 Used as Wind Speed Count -# D6, GPIO12 Used as Rainfall count -# D7, GPIO13 Used for DHT -# D8, GPIO15 (Boot fails if pulled HIGH) +# D1, GPIO5 Used as SCL (I2C) +# D2, GPIO4 Used as SDA (I2C) +# D3, GPIO0 connected to FLASH button, boot fails if pulled LOW +# D4, GPIO2 connected to on-board LED, boot fails if pulled LOW +# D5, GPIO14 Used as Wind Speed Count +# D6, GPIO12 Used as Rainfall count +# D7, GPIO13 Used for DHT +# D8, GPIO15 (Boot fails if pulled HIGH) # # NO HEADERS on Weatherstation board: -# D0, GPIO16, (used to wake up from deep sleep) -# RX, GPIO3 (HIGH at boot) -# TX, GPIO1 (HIGH at boot, boot fails if pulled LOW) -# A0, ADC0 Used here to measure battery V +# D0, GPIO16 (used to wake up from deep sleep) +# RX, GPIO3 (HIGH at boot) +# TX, GPIO1 (HIGH at boot, boot fails if pulled LOW) +# A0, ADC0 Used here to measure battery V # -############################################# +####################################################################################################### -############################################# +####################################################################################################### # Pulse Meter Issues 202405 # https://github.com/esphome/issues/issues/4807 # https://github.com/esphome/issues/issues/3143 -############################################# +####################################################################################################### external_components: - source: github://TrentHouliston/esphome@loop_pulse_isr components: [ pulse_meter ] -############################################# +####################################################################################################### # Variable Substitutions # Give this a useful name & description here # and change values accordingly -############################################# +####################################################################################################### substitutions: devicename: "esp-weatherstation" friendly_name: "Weather Station" @@ -109,22 +110,22 @@ substitutions: mqtt_topic: "esphome" #main topic for the mqtt server, call it what you like update_time: 30s #update time for for various temp sensors etc -############################################# +####################################################################################################### # ESPHome # https://esphome.io/components/esphome.html -############################################# +####################################################################################################### esphome: name: ${devicename} comment: ${description_comment} #appears on the esphome page in HA #on_boot: #Initial Setting stuff - #priority: -200 - #then: + # priority: -200 + # then: -############################################# +####################################################################################################### # ESP Platform and Framework # https://esphome.io/components/esp32.html OR # https://esphome.io/components/esp8266.html -############################################# +####################################################################################################### #esp32: # board: # framework: @@ -135,12 +136,12 @@ esphome: esp8266: board: d1_mini -############################################# +####################################################################################################### # i2c bus # https://esphome.io/components/i2c.html # Put the relevant pins here, 4/5 are default for ESP8266 # and 21/22 for esp32 (although esp32 can have two buses) -############################################# +####################################################################################################### i2c: sda: GPIO4 scl: GPIO5 @@ -194,11 +195,11 @@ wifi: password: ${wifi_pass} #power_save_mode: LIGHT #https://esphome.io/components/wifi.html#wifi-power-save-mode #manual_ip: #optional static IP address - #static_ip: 192.168.x.x - #gateway: 192.168.X.x - #subnet: 255.255.255.0 + # static_ip: 192.168.x.x + # gateway: 192.168.X.x + # subnet: 255.255.255.0 ap: #Details for fallback hotspot in case wifi connection fails https://esphome.io/components/wifi.html#access-point-mode - ssid: $devicename fallback AP + ssid: "${devicename} fallback AP" password: !secret fallback_ap_password ap_timeout: 30min #default is 1min @@ -214,14 +215,14 @@ wifi: #web_server: # port: 80 # version: 1 #V1 occasionally works better, V2 The nicer page -# username: !secret web_server_username #probably a good idea to secure it -# password: !secret web_server_password +# username: !secret web_server_username #probably a good idea to secure it +# password: !secret web_server_password -############################################# +####################################################################################################### # MQTT Monitoring # https://esphome.io/components/mqtt.html?highlight=mqtt # MUST also have api enabled if you enable MQTT -############################################# +####################################################################################################### mqtt: broker: ${mqtt_server} topic_prefix: ${mqtt_topic}/${devicename} @@ -230,14 +231,14 @@ mqtt: #Method to prevent deep sleep using MQTT command #on_message: - # - topic: ${mqtt_topic}/${devicename}/deepsleep - # payload: 'ON' - # then: - # - deep_sleep.prevent: deep_sleep_1 - # - topic: ${mqtt_topic}/${devicename}/deepsleep - # payload: 'ON' - # then: - # - deep_sleep.enter: deep_sleep_1 + # - topic: ${mqtt_topic}/${devicename}/deepsleep + # payload: 'ON' + # then: + # - deep_sleep.prevent: deep_sleep_1 + # - topic: ${mqtt_topic}/${devicename}/deepsleep + # payload: 'ON' + # then: + # - deep_sleep.enter: deep_sleep_1 ######################################## # Deep Sleep @@ -248,29 +249,30 @@ mqtt: # sleep_duration: 10min # id: deep_sleep_1 -############################################# +####################################################################################################### # General espHome status LED # Not needed, but can be useful to see issues # https://esphome.io/components/status_led.html -############################################# +# If the LED appears inverted (on when it should be off), set 'inverted: true'. +####################################################################################################### status_led: pin: number: GPIO2 #Wemos ESP32 and ESP8266 Onboard LEDs use GPIO2 inverted: false #ignore_strapping_warning: True #https://esphome.io/guides/faq.html#why-am-i-getting-a-warning-about-strapping-pins -################################ +########################################################################################## # 4 Input A->D sensor # Analog sensor for voltage reading # https://esphome.io/components/sensor/ads1115.html -################################ +########################################################################################## ads1115: - address: 0x48 #On the ADS1115 pull address pin to VCC for 0x49. Default is 0x48 -############################################# +####################################################################################################### # Text Sensors # https://esphome.io/components/text_sensor/index.html -############################################# +####################################################################################################### text_sensor: - platform: template name: ${friendly_name} Beaufort Wind Scale @@ -283,17 +285,17 @@ text_sensor: id: wind_dir_heading update_interval: never -############################################# -############################################# +####################################################################################################### +####################################################################################################### # General Sensors # https://esphome.io/components/sensor/index.html -############################################# -############################################# +####################################################################################################### +####################################################################################################### sensor: ################################ # UPTIME - # Device uptime info + # Device uptime info # https://esphome.io/components/sensor/uptime.html ################################ - platform: uptime @@ -316,7 +318,7 @@ sensor: ################################ - platform: bmp280 address: 0x76 - iir_filter: 16X + iir_filter: 16x update_interval: ${update_time} temperature: name: ${friendly_name} BMP280 Temperature @@ -327,7 +329,7 @@ sensor: pressure: name: ${friendly_name} BMP280 Pressure unit_of_measurement: "hPa" - oversampling: 16X + oversampling: 16x #retain: true #MQTT retain useful if sleeping ################################ @@ -356,11 +358,14 @@ sensor: icon: "mdi:thermometer" unit_of_measurement: "°C" lambda: |- - return ( - id(bmp280_temp).state - + - id(dht_temp).state - ) / 2; + const float a = id(bmp280_temp).state; + const float b = id(dht_temp).state; + const bool va = !isnan(a); + const bool vb = !isnan(b); + if (va && vb) return (a + b) / 2.0f; + if (va) return a; + if (vb) return b; + return NAN; ################################ # BATTERY VOLTAGE @@ -384,9 +389,9 @@ sensor: send_every: 1 send_first_at: 1 - calibrate_linear: - - 3.321 -> 3.125 - - 3.89 -> 3.90 - - 4.09 -> 4.14 + - 3.321 -> 3.125 + - 3.89 -> 3.90 + - 4.09 -> 4.14 ################################ # A Calculation of recent Av V @@ -398,30 +403,30 @@ sensor: unit_of_measurement: "V" accuracy_decimals: 6 update_interval: 30s #updates every 30s - lambda: > + lambda: |- return id(battery_voltage).state; //grabs values from the battery_voltage filters: - sliding_window_moving_average: - window_size: 60 #40x 30s = 20 min average period + window_size: 60 #60x 30s = 30 min average period send_every: 1 send_first_at: 1 - ################################ - # A Calculation of recent Max V + ########################################################################################## + # A Calculation of 10 min Max V # So we can see if battery is charging/discharging - ################################ + ########################################################################################## - platform: template name: ${friendly_name} Max Battery Voltage over recent period id: battery_voltage_recentmax unit_of_measurement: "V" accuracy_decimals: 6 - update_interval: 30s #updates every 10s - lambda: > + update_interval: 30s #updates every 30s + lambda: |- return id(battery_voltage).state; //grabs values from the battery_voltage filters: - heartbeat: 30s - max: - window_size: 60 #60x 30s = 30 min max value + window_size: 20 # 20 x 30s = 10 min max value (was 60 = 30 min) send_every: 1 send_first_at: 1 @@ -431,8 +436,8 @@ sensor: unit_of_measurement: "V" accuracy_decimals: 5 update_interval: 30s #updates every 30s - lambda: > - return {id(battery_voltage_recentmax).state - id(battery_voltage).state}; //recent av voltage - now + lambda: |- + return id(battery_voltage_recentmax).state - id(battery_voltage).state; //recent max voltage - now - platform: duty_time id: battery_discharge_time @@ -444,17 +449,17 @@ sensor: filters: - multiply: 0.01666666 - round: 0 - lambda: > - return { id(battery_voltage).state <= id(battery_voltage_recentav).state }; + lambda: |- + return id(battery_voltage).state <= id(battery_voltage_recentav).state; - ################################ +########################################################################################## # RAIN SENSOR # Pulse Meter for measuring rainfall # https://esphome.io/components/sensor/pulse_meter.html - ################################ +########################################################################################## # CALIBRATION HINTS # https://forum.mysensors.org/topic/9594/misol-rain-gauge-tipping-bucket-rain-amount/2 - ################################ +########################################################################################## - platform: pulse_meter id: rainfall_meter pin: @@ -483,20 +488,20 @@ sensor: # max_value: 250 #if anything over that, we have debounce issues or are going to die # ignore_out_of_range: true - ################################ +########################################################################################## # Pulse Meter for measuring wind speed # Analog sensor for voltage reading # https://esphome.io/components/sensor/pulse_meter.html # - # This Anenometer https://www.sparkfun.com/datasheets/Sensors/Weather/Weather%20Sensor%20Assembly..pdf + # This Anemometer https://www.sparkfun.com/datasheets/Sensors/Weather/Weather%20Sensor%20Assembly..pdf # The cup-type anemometer measures wind speed by closing a contact # as a magnet moves past a reed switch. A wind speed of 1.492mph (2.4km/h) (0.667m/s) # causes the switch to close once per second. The reed switch closes twice per revolution. # 1rps = 60rpm = 2.4kmh # 1pulse/min = 2.4kmh/60 = 0.04kmh - # Pulse meter component measures the time between rising edges on a pin, + # Pulse meter component measures the time between rising edges on a pin, # for each pulse it outputs the frequency in pulses/min. - ################################ +########################################################################################## # USEFUL LINKS # https://community.home-assistant.io/t/measuring-wind-speed/395693/20 # https://community.home-assistant.io/t/measuring-wind-speed/395693/21 @@ -530,44 +535,55 @@ sensor: max_value: 250 #if anything over that, we have debounce issues or are going to die ignore_out_of_range: true - ################################ - # WIND 10 MINUTE MAX - # Template for wind gust measurements - # To measure GUST properly, the World Meteorological Organization - # recommends gust measurement should be wind speed averages over - # 3 second periods and report the maximum of these averages - # within a sliding window of the last 10 minutes. - # This needs a wind speed history buffer of 200 samples - # (200 x 3 second average = 10 minutes) - ################################ - # Useful links - # https://community.home-assistant.io/t/adding-an-old-marine-anemometer-to-ha/337842 - ################################ +########################################################################################## + # WIND GUSTS – WMO-style 10-minute gust + # Per WMO guidance, "gust" ≈ maximum of 3-second mean wind over the last 10 minutes. + # Implementation: + # 1) Compute a 3 s sliding average (updated every 1 s) from wind_meter. + # 2) Take the rolling maximum of that 3 s average over the last 10 minutes. +########################################################################################## + - platform: template + id: wind_speed_3s_avg + name: ${friendly_name} Wind speed (3s avg) + unit_of_measurement: "km/h" + icon: "mdi:weather-windy" + update_interval: 1s + lambda: |- + return id(wind_meter).state; // sample latest wind speed + filters: + - filter_out: nan + - sliding_window_moving_average: + window_size: 3 # 3 samples x 1s = 3 s average + send_every: 1 + send_first_at: 1 + - platform: template name: ${friendly_name} Windspeed Gust (Max last 10m) unit_of_measurement: "km/h" - update_interval: 3s #updates every 2s - lambda: > - return id(wind_meter).state; //grabs values from the wind_sensor + icon: "mdi:weather-windy" + update_interval: 1s + lambda: |- + return id(wind_speed_3s_avg).state; # use the 3 s average stream filters: - - filter_out: nan #in case there is no value + - filter_out: nan - max: - window_size: 200 #10 Min window as 3s update x 200 = 10min - send_every: 1 #updates on the 1st 2s check - send_first_at: 1 #on restart, send after first 10s + window_size: 600 # 600 x 1s = 10 minutes rolling window + send_every: 1 + send_first_at: 3 # after first 3 s average becomes meaningful - ################################ +########################################################################################## # BEAUFORT SCALE # Template for Beaufort for measuring wind speed # The output updates the Text Sensor ID wind_scale # https://windy.app/blog/wind-speed-beaufort-scale.html - ################################ +########################################################################################## - platform: template #name: ${friendly_name} Windspeed Scale internal: true # no need to show this externally as it is just for calculating Beaufort #icon: "mdi:weather-windy" id: wind_meter_scale - lambda: return id(wind_meter).state; + lambda: |- + return id(wind_meter).state; #unit_of_measurement: "m/s" update_interval: 5s filters: @@ -602,7 +618,7 @@ sensor: } else if (id(wind_meter_scale).state >= 33) { id(wind_scale).publish_state("Hurricane Force"); } else { - id(wind_scale).publish_state({""}); + id(wind_scale).publish_state(""); } ############################################# @@ -614,69 +630,114 @@ sensor: # Wind Direction, Analogue on ADS115 # Analog sensor for voltage reading # https://esphome.io/components/sensor/ads1115.html - ################################ +########################################################################################## # Using a analog wind direction sensor that has an output # of 0 - 5 V for 360° representing the position of the arrow # from the north pole. This is 5V divided by 8 resistor # values giving the output voltage value for each azimuth. # We could also report a compass direction. - ################################ + ########################################################################################## # Useful Links # https://community.home-assistant.io/t/davis-wind-direction-equipment-and-esp-home/508764 - ################################ + ########################################################################################## - platform: ads1115 multiplexer: 'A0_GND' gain: 4.096 name: ${friendly_name} Wind Direction Voltage id: wind_direction_voltage update_interval: 2s - - ################################################################ - # CONVERT VOLTAGE TO HEADING - # Version used here only has 8 heading points, some have 16. - # - # Bear R Head Meas'd Max Min - # (deg) (Ohms) Volts V+1% V-1% - # --------------------------------------------- - # 0 33000 N 2.900 2.9290 2.8710 - # 22.5 6570 NNE - # 45 8200 NE 2.108 2.1290 2.0869 - # 67.5 891 ENE - # 90 1000 E 0.584 0.5898 0.5782 - # 112.5 688 ESE - # 135 2200 SE 1.061 1.0716 1.0504 - # 157.5 1410 SSE - # 180 3900 S 1.506 1.5211 1.4910 - # 202.5 3140 SSW - # 225 16000 SW 2.560 2.5856 2.5344 - # 247.5 14120 WSW - # 270 120000 W 3.186 3.2178 3.1541 - # 292.5 42120 WNW - # 315 64900 NW 3.088 3.1188 3.0571 - # 337.5 21880 NNW - ################################################################ on_value: lambda: |- - if (id(wind_direction_voltage).state >= 0.57816 && id(wind_direction_voltage).state <= 0.58984) { + const float v = x; // use current reading from on_value + if (v >= 0.57816 && v <= 0.58984) { id(wind_dir_heading).publish_state("E"); - } else if (id(wind_direction_voltage).state >= 1.05039 && id(wind_direction_voltage).state <= 1.07161) { + } else if (v >= 1.05039 && v <= 1.07161) { id(wind_dir_heading).publish_state("SE"); - } else if (id(wind_direction_voltage).state >= 1.49094 && id(wind_direction_voltage).state <= 1.52106) { + } else if (v >= 1.49094 && v <= 1.52106) { id(wind_dir_heading).publish_state("S"); - } else if (id(wind_direction_voltage).state >= 2.08692 && id(wind_direction_voltage).state <= 2.12908) { + } else if (v >= 2.08692 && v <= 2.12908) { id(wind_dir_heading).publish_state("NE"); - } else if (id(wind_direction_voltage).state >= 2.5344 && id(wind_direction_voltage).state <= 2.5856) { + } else if (v >= 2.53440 && v <= 2.58560) { id(wind_dir_heading).publish_state("SW"); - } else if (id(wind_direction_voltage).state >= 2.871 && id(wind_direction_voltage).state <= 2.929) { + } else if (v >= 2.87100 && v <= 2.92900) { id(wind_dir_heading).publish_state("N"); - } else if (id(wind_direction_voltage).state >= 3.05712 && id(wind_direction_voltage).state <= 3.11888) { + } else if (v >= 3.05712 && v <= 3.11888) { id(wind_dir_heading).publish_state("NW"); - } else if (id(wind_direction_voltage).state >= 3.15414 && id(wind_direction_voltage).state <= 3.21786) { + } else if (v >= 3.15414 && v <= 3.21786) { id(wind_dir_heading).publish_state("W"); } else { - id(wind_dir_heading).publish_state({}); + id(wind_dir_heading).publish_state(""); } + ########################################################################################## + # BME280 temp/humidity/pressure (Note, BME and BMP Are different...) + # https://esphome.io/cookbook/bme280_environment.html + ########################################################################################## + #- platform: bme280_i2c + # address: 0x76 + # update_interval: ${update_time} + # temperature: + # name: ${friendly_name} BME280 Temp + # accuracy_decimals: 1 + # oversampling: 2x + # unit_of_measurement: "°C" + # #retain: true #MQTT retain useful if sleeping + # pressure: + # name: ${friendly_name} BME280 Pressure + # oversampling: 2x + # unit_of_measurement: "hPa" + # #retain: true #MQTT retain useful if sleeping + # humidity: + # name: ${friendly_name} BME280 Humidity + # accuracy_decimals: 1 + # oversampling: 2x + # unit_of_measurement: "%" + # #retain: true #MQTT retain useful if sleeping + + + ########################################################################################## + # TSL2561 ambient light sensor + # https://esphome.io/components/sensor/tsl2561.html + ########################################################################################## + #- platform: tsl2561 + # name: ${friendly_name} Ambient Light + # address: 0x39 + # update_interval: 60s + + ########################################################################################## + # LTR390 Ambient Light and UV Sensor + # https://esphome.io/components/sensor/ltr390.html + ########################################################################################## + #- platform: ltr390 + # address: 0x35 + # uv_index: + # name: ${friendly_name} 390 UV Index + # uv: + # name: ${friendly_name} 390 UV Sensor Counts + # light: + # name: ${friendly_name} 390 Light + # ambient_light: + # name: ${friendly_name} 390 UV Sensor Counts + # update_interval: 60s + + ########################################################################################## + # PMSX003 Particulate sensor + # NEEDS UART SET FIRST + # https://esphome.io/components/sensor/pmsx003.html + ########################################################################################## + #- platform: pmsx003 + # type: PMSX003 + # pm_1_0: + # name: ${friendly_name} Particulate Matter <1.0µm + # pm_2_5: + # name: ${friendly_name} <2.5µm + # pm_10_0: + # name: ${friendly_name} <10.0µm Concentration + + + + + # filters: # lambda: |- @@ -797,88 +858,6 @@ sensor: # return {}; -# - platform: template -# name: "Roof Wind Run daily" -# icon: "mdi:weather-windy" -# unit_of_measurement: "km" -# accuracy_decimals: 3 -# update_interval: 30s -# internal: true -# lambda: |- -# return id(g_wind_run_count)*0.000666982; -# id: id_wind_run_daily_g - -# - platform: template -# name: "Roof Wind gust 10s" -# unit_of_measurement: 'km/h' -# update_interval: 10s -# id: id_wind_gust_10s -# lambda: return id(g_WindGust_10s); -# -# - platform: template -# name: "Roof Wind gust 60s" -# unit_of_measurement: 'km/h' -# state_class: "measurement" -# update_interval: 60s -# id: id_wind_gust_60s -# lambda: return id(g_WindGust_60s); - -# - platform: template -# name: "Roof Wind gust 10min" -# unit_of_measurement: 'km/h' -# state_class: "measurement" -# update_interval: 10min -# id: id_wind_gust_10min -# lambda: return id(g_WindGust_10min); - -# - platform: template -# name: "Roof Wind speed avg 1s" -# unit_of_measurement: 'km/h' -# update_interval: 333ms -# lambda: |- -# return id(id_wind_speed).state; -# filters: -# throttle_average: 1s -# id: id_wind_speed_avg_1s -# -# - platform: template -# name: "Roof Wind speed avg 10s" -# unit_of_measurement: 'km/h' -# update_interval: 333ms -# lambda: |- -# return id(id_wind_speed).state; -# filters: -# throttle_average: 10s -# id: id_wind_speed_avg_10s -# -# - platform: template -# name: "Roof Wind Speed Min 10s" -# unit_of_measurement: 'km/h' -# id: id_wind_speed_min_10s -# -# - platform: template -# name: "Roof Wind speed avg 1min" -# unit_of_measurement: 'km/h' -# state_class: "measurement" -# update_interval: '1s' -# lambda: |- -# return id(id_wind_speed).state; -# filters: -# - throttle_average: 1min -# id: id_wind_speed_avg_1min -# -# - platform: template -# name: "Roof Wind speed avg 10min" -# unit_of_measurement: 'km/h' -# state_class: "measurement" -# update_interval: 1s -# lambda: |- -# return id(id_wind_speed).state; -# filters: -# - throttle_average: 10min -# id: id_wind_speed_avg_10min - - # - platform: pulse_meter # pin: # number: GPIO13 #This is Pin D7 on the D1 Mini ESP8266 @@ -979,69 +958,3 @@ sensor: # type: float # restore_value: no # initial_value: '0.0' - - - ################################ - # BME280 temp/humidity/pressure (Note, BME and BMP Are different...) - # https://esphome.io/cookbook/bme280_environment.html - ################################ - #- platform: bme280_i2c - # address: 0x76 - # update_interval: ${update_time} - # temperature: - # name: ${friendly_name} BME280 Temp - # accuracy_decimals: 1 - # oversampling: 2x - # unit_of_measurement: "°C" - # #retain: true #MQTT retain useful if sleeping - # pressure: - # name: ${friendly_name} BME280 Pressure - # oversampling: 2x - # unit_of_measurement: "hPa" - # #retain: true #MQTT retain useful if sleeping - # humidity: - # name: ${friendly_name} BME280 Humidity - # accuracy_decimals: 1 - # oversampling: 2x - # unit_of_measurement: "%" - # #retain: true #MQTT retain useful if sleeping - - - ################################ - # TSL2561 ambient light sensor - # https://esphome.io/components/sensor/tsl2561.html - ################################ - #- platform: tsl2561 - # name: ${friendly_name} Ambient Light - # address: 0x39 - # update_interval: 60s - - ################################ - # LTR390 Ambient Light and UV Sensor - # https://esphome.io/components/sensor/ltr390.html - ################################ - #- platform: ltr390 - # address: 0x35 - # uv_index: - # name: ${friendly_name} 390 UV Index - # uv: - # name: ${friendly_name} 390 UV Sensor Counts - # light: - # name: ${friendly_name} 390 Light - # ambient_light: - # name: ${friendly_name} 390 UV Sensor Counts - # update_interval: 60s - - ################################ - # PMSX003 Particulate sensor - # NEEDS UART SET FIRST - # https://esphome.io/components/sensor/pmsx003.html - ################################ - #- platform: pmsx003 - # type: PMSX003 - # pm_1_0: - # name: ${friendly_name} Particulate Matter <1.0µm - # pm_2_5: - # name: ${friendly_name} <2.5µm - # pm_10_0: - # name: ${friendly_name} <10.0µm Concentration \ No newline at end of file diff --git a/packages/simulation_lights.yaml b/packages/simulation_lights.yaml index 90e40ac..d37aee6 100644 --- a/packages/simulation_lights.yaml +++ b/packages/simulation_lights.yaml @@ -2,13 +2,13 @@ group: simulation_lights_all: name: Simulation Lights (All) entities: + - group.downstairs_flat_lights # ← now valid here - switch.esp_centralstairs_top_relay_2_stair_footer_lights - switch.esp_centralstairs_bottom_relay_1_main_stair_lights_lower - switch.esp_entrancebathrmlights_relay_1_main_lights - - group.downstairs_flat_lights # ← now valid here - - switch.tasmo_ks811d_1242_entrance_a - - switch.tasmo_ks811d_6110_kitchen_a - - switch.tasmo_ks811d_6110_kitchen_b + - switch.esp_entrancelightswitch_relay_1_entrance_lights + - switch.esp_mainkitchenlights_relay_1_main_lights + - switch.esp_mainkitchenlights_relay_2_benchtop_lights - switch.main_hallway_lightswitch_tasmo_ks811s_2940_hallway_1a - switch.tasmo_ks811t_0702_lounge_3a - switch.tasmo_ks811t_0702_lounge_3b