From 40ff88e0f59a43048677d8c0fb7342dd69e6ed58 Mon Sep 17 00:00:00 2001 From: root Date: Mon, 18 Aug 2025 18:47:23 +1200 Subject: [PATCH] esphome led controller V1.2 --- esphome/esp-downstairskitchleds.yaml | 301 ++++++++++++++++----------- 1 file changed, 175 insertions(+), 126 deletions(-) diff --git a/esphome/esp-downstairskitchleds.yaml b/esphome/esp-downstairskitchleds.yaml index 8b82bb1..7130315 100644 --- a/esphome/esp-downstairskitchleds.yaml +++ b/esphome/esp-downstairskitchleds.yaml @@ -1,22 +1,24 @@ ########################################################################################## ########################################################################################## -# DOWNSTAIRS KITCHEN - OVER PANTRY LEDS -# -# Controlled by a Sinilink Mosfet Board (ESP8266) +# Title: DOWNSTAIRS KITCHEN - OVER PANTRY LEDS +# Hardware: Sinilink Mosfet Board XY-VFMS (ESP8266) +# 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 # # V1.1 - 2025-08-18 Full tidyup as general purpose LED strip controller # V1.0 - 2025-08-17 First Setup (and replacement of Tasmota) # -# DEVICE GPIO # ------------------------------------------ -# Sinilink Board -# https://devices.esphome.io/devices/Sinilink-XY-VFMS +# DEVICE GPIO Sinilink XY-VFMS +# ------------------------------------------ # GPIO02 Blue LED (We'll use this for ESPHome status) # GPIO04 Mosfet Output (0V when switched) and Red LED # GPIO12 Toggle Button # GPIO13 Green LED (We'll use this to display fading status) # +# ------------------------------------------ # OPERATION (as at V1.1) +# ------------------------------------------ # 1. General Purpuse LED controller # 2. Designed for a Sinilink XY-VFMS board that has a mosfet output and supposedly will handle # 5A and a DC input of 5V-36V. @@ -29,8 +31,23 @@ # 6. Min and Max output settings aren't set in Home assistant/MQTT, but you could do this if # needed. With a 1MB flash, it is starting to get tight. I have done minimal optimising # at this stage though. -# 7. There are PACKAGES included for a bunch of things such as the common network -# items, diagnostic entities, MQTT and SNTP (if needed) +# 7. There are PACKAGES included for the common things such as the network +# items, diagnostic entities, MQTT and SNTP (if needed, get them from the repo or use your own) +# 8. Default config is to always fade slowly up to full when powered up (so can be deployed with +# no network etc) +# 9. The green LED on the sinilink flashes whilst fading (differently for up/down). The red LED +# follows the output (it is the same GPIO as the MOSFET) +# 10.Timing of fades should be based on settings, or a percentage of them if eg already half brightness. +# 11.A useful 3D printed case: https://cults3d.com/en/3d-model/tool/snapfit-enclosure-for-esp8266-sinilink-xy-wfms-5v-36v-mosfet-switch-module +# 12.Some things you can change in Home Assistant/MQTT +# - Start up function +# - Up/Down/Stop fade buttons +# - A fade up/fade down switch +# - Normal on/off switch (quick ramp up/down) +# - Setting for fade up and fade times (0-60 seconds) +# - Output display of % PWM output +# - Ability to set output to any value (1-100, but respects min/max) +# - Default has a bunch of device diagnostic in the PACKAGE included (Sensors_Common) # ########################################################################################### ########################################################################################## @@ -48,7 +65,7 @@ substitutions: # Project Naming project_name: "Sinilink.XY-WFMS" # 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 @@ -59,20 +76,27 @@ substitutions: # Device Settings log_level: "INFO" # Define logging level: NONE, ERROR, WARN, INFO, DEBUG (Default), VERBOSE, VERY_VERBOSE - update_interval: "10s" # update time for for general sensors etc + update_interval: "20s" # update time for for general sensors etc # MQTT LOCAL 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_local_device_name: "downstairskitchen-pantryleds" + 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" # MQTT REMOTE Controls + #mqtt_remote_device_name: "downstairskitchen-pantryleds" + #mqtt_remote_device_command_topic: "${mqtt_local_command_main_topic}/${mqtt_remote_device_name}/light/set" + #mqtt_remote_device_command1: "+" + #mqtt_remote_device_command2: "-" + #mqtt_remote_device_command3: "0" + #mqtt_local_status_topic: "${mqtt_local_status_main_topic}/${mqtt_remote_device_name}/speed/state" # Topic we will use to view status locally without HA # Button Naming & Icons # Switch/Relay Naming & Icons - ########################################################################################## # PACKAGES: Included Common Packages # https://esphome.io/components/packages.html @@ -103,7 +127,7 @@ packages: local_update_interval: "${update_interval}" ########################################################################################## -# ESPHome +# ESPHome CORE CONFIGURATION # https://esphome.io/components/esphome.html ########################################################################################## esphome: @@ -122,7 +146,6 @@ esphome: 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 (respect min/max & ramp time) - if: condition: @@ -130,7 +153,6 @@ esphome: then: - lambda: 'id(ramp_switch_target_on) = true;' - script.execute: ramp_on_script - # Mode 1: Restore Brightness quickly - if: condition: @@ -164,7 +186,6 @@ esphome: } - delay: 300ms - lambda: 'id(suppress_slider_sync) = false;' - # Mode 2: Remain Off - if: condition: @@ -178,11 +199,12 @@ esphome: - lambda: 'id(ramp_switch_target_on) = false;' ########################################################################################## -# ESP Platform and Framework +# ESP PLATFORM AND FRAMEWORK +# https://esphome.io/components/esp8266.html # https://esphome.io/components/esp32.html ########################################################################################## esp8266: - board: esp01_1m # The original sonoff basic + board: esp01_1m restore_from_flash: true # restore some values on reboot preferences: @@ -191,50 +213,90 @@ preferences: mdns: disabled: false # Disabling will make the build file smaller (and it is still available via static IP) -########################################################################################## -# ESPHome Logging Enable -# https://esphome.io/components/logger.html ########################################################################################## +# 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: '3' # start/finish at X% + # Maximum Brightness % for LEDs (should never go beyond this) + - id: max_brightness_pct + type: int + restore_value: false + initial_value: '90' # hard cap; never exceed this + # 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 tirection. + # true when you’ve asked the light to end up ON (ramp up) + # and false when you’ve asked it to end up OFF + - 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' + +########################################################################################## +# 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) ########################################################################################## -# GLOBALS: ramp times (milliseconds) +# MQTT COMMANDS +# This adds device-specific MQTT command triggers to the common MQTT configuration. ########################################################################################## -globals: - - id: min_brightness_pct - type: int - restore_value: true - initial_value: '3' # start/finish at X% - - id: max_brightness_pct - type: int - restore_value: false - initial_value: '90' # hard cap; never exceed this - - id: ramp_up_ms # fade-in when turned ON - type: int - restore_value: true - initial_value: '5000' # 5 s - - id: ramp_down_ms # fade-out when turned OFF - type: int - restore_value: true - initial_value: '10000' # 10 s - - id: ramp_switch_target_on - type: bool - restore_value: true - initial_value: 'false' - - id: suppress_slider_sync - type: bool - restore_value: false - initial_value: 'false' - - id: restart_mode # 0=Fade full, 1=Restore brightness, 2=Remain off - type: int - restore_value: true - initial_value: '0' # default = Ramp to full (so can be deployed with no other setup) - - id: last_brightness_pct # actual 0..100 seen last time - type: float - restore_value: true - initial_value: '0.0' +mqtt: + 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 up + - topic: "${mqtt_local_command_topic}/light/set" + payload: "${mqtt_local_device_command_OFF}" + then: + - switch.turn_off: mosfet_ramp_switch ######################################################################################### # STATUS LED @@ -247,10 +309,6 @@ status_led: number: GPIO2 inverted: true -########################################################################################## -# SWITCH COMPONENT -# https://esphome.io/components/switch/ -########################################################################################## ########################################################################################## # SWITCH COMPONENT # https://esphome.io/components/switch/ @@ -326,8 +384,8 @@ button: } ######################################################################################### -# SELECT SENSORS -# +# SELECT COMPONENT +# https://esphome.io/components/select/index.html ######################################################################################### select: - platform: template @@ -387,7 +445,8 @@ binary_sensor: - script.execute: ramp_on_script ########################################################################################## -# SENSOR: LED / PWM output percentage (0–100 %) +# SENSOR COMPONENT +# https://esphome.io/components/sensor/ ########################################################################################## sensor: - platform: template @@ -396,36 +455,40 @@ sensor: unit_of_measurement: "%" icon: mdi:percent accuracy_decimals: 0 - update_interval: 100ms + update_interval: 100ms # consider 200ms if you want fewer updates lambda: |- const auto &cv = id(mosfet_leds).current_values; - if (cv.is_on()) { - return cv.get_brightness() * 100.0f; // actual 0..100 - } else { - return 0.0f; - } + return cv.is_on() ? (cv.get_brightness() * 100.0f) : 0.0f; on_value: then: - lambda: |- // Remember latest actual output (0..100) for "Restore Brightness" id(last_brightness_pct) = x; - - if: - condition: - lambda: 'return !id(suppress_slider_sync);' - then: - - lambda: |- - float actual = x; // actual % - 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; - id(led_output_set_pct).publish_state((int) floorf(pos + 0.5f)); -################################################################################ -# TEMPLATE OUTPUTS: drive the real relays when the states change -################################################################################ + // If not suppressing sync, update the 0..100 slider only when its INT changes + if (!id(suppress_slider_sync)) { + float actual = x; // actual % + 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); + } + } + +########################################################################################## +# 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 @@ -451,8 +514,16 @@ light: icon: mdi:led-strip-variant gamma_correct: 1.2 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;' 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;' on_state: - lambda: |- @@ -461,16 +532,14 @@ light: 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); // clamp to max - call.set_transition_length(0); // snap to cap + call.set_brightness(cap); + call.set_transition_length(0); call.perform(); - } + } ########################################################################################## -# NUMBERS: adjust ramp/smoothing from Home Assistant (seconds) -########################################################################################## -########################################################################################## -# NUMBERS: adjust ramp times and direct output set (0..100 mapped to min..max) +# NUMBER COMPONENT +# https://esphome.io/components/number/ ########################################################################################## number: - platform: template @@ -575,10 +644,11 @@ number: - delay: 400ms - lambda: 'id(suppress_slider_sync) = false;' -################################################################################################# +########################################################################################## # 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 @@ -596,7 +666,6 @@ script: - delay: 100ms - output.turn_off: green_led_out - delay: 400ms - # Blink pattern while ramping DOWN: steady slow blink - id: led_flash_down mode: restart @@ -610,6 +679,7 @@ script: - output.turn_off: green_led_out - delay: 250ms + # Script: ramp up from current level. Obey global max. - id: ramp_on_script mode: restart then: @@ -640,22 +710,12 @@ script: float frac = (cap - curr) / (cap - floor); if (frac < 0.0f) frac = 0.0f; if (frac > 1.0f) frac = 1.0f; - return (uint32_t)(id(ramp_up_ms) * frac); - - delay: !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; - float curr = cv.is_on() ? cv.get_brightness() : 0.0f; - if (curr < floor) curr = floor; - if (curr > cap) curr = cap; - float frac = (cap - curr) / (cap - floor); - if (frac < 0.0f) frac = 0.0f; - if (frac > 1.0f) frac = 1.0f; - return (uint32_t)(id(ramp_up_ms) * frac); + 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 - # Script: ramp down from current level to floor, then cleanly cut to OFF - id: ramp_off_script mode: restart @@ -663,7 +723,6 @@ script: - script.stop: ramp_on_script - script.stop: led_flash_up - script.execute: led_flash_down - # Ramp from current to floor; time scales with distance - light.turn_on: id: mosfet_leds brightness: !lambda 'return id(min_brightness_pct) / 100.0f;' @@ -675,27 +734,17 @@ script: float frac = (curr - floor) / (1.0f - floor); if (frac < 0.0f) frac = 0.0f; if (frac > 1.0f) frac = 1.0f; - return (uint32_t)(id(ramp_down_ms) * frac); - # Keep LED blinking for that duration - - delay: !lambda |- - const auto &cv = id(mosfet_leds).current_values; - const float floor = id(min_brightness_pct) / 100.0f; - float curr = cv.is_on() ? cv.get_brightness() : 0.0f; - if (curr < floor) curr = floor; - float frac = (curr - floor) / (1.0f - floor); - if (frac < 0.0f) frac = 0.0f; - if (frac > 1.0f) frac = 1.0f; - return (uint32_t)(id(ramp_down_ms) * frac); - # Finish with a short fade to black + 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 - # Prepare the "next-on" brightness so the plain light entity doesn't come back at 5% - lambda: |- auto call = id(mosfet_leds).make_call(); - call.set_state(false); // remain OFF - call.set_brightness(id(max_brightness_pct) / 100.0f); // remember cap for next ON - call.perform(); + call.set_state(false); + call.set_brightness(id(max_brightness_pct) / 100.0f); + call.perform(); \ No newline at end of file