Files
zorruno-homeassistant/esphome/esp-downstairskitchleds.yaml
2025-08-18 17:07:49 +12:00

702 lines
27 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

##########################################################################################
##########################################################################################
# DOWNSTAIRS KITCHEN - OVER PANTRY LEDS
#
# Controlled by a Sinilink Mosfet Board (ESP8266)
#
# 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
# 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.
# 3. Has global setting for MAX % PWM output for the LEDs so you can give them a longer life.
# 4. Has a min setting for the LEDs and it will switch off if it goes below that to stop any
# flicker at very low PWM outputs.
# 5. PWM freq is set to 2kHz, but you could potentially ramp it up. I was getting resets at
# higher values with this device, but other devices may be better. Obviously if yoou use
# an esp32 you can set it much higher (40kHz I think is the max?)
# 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)
#
###########################################################################################
##########################################################################################
##########################################################################################
# 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"
friendly_name: "Downstairs Kitchen LEDs"
description_comment: "Downstairs Kitchen Over Pantry LEDs :: Sinilink XY-WFMS"
device_area: "Downstairs Kitchen" # Allows ESP device to be automatically linked to an 'Area' in Home Assistant.
# 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
# Passwords & Secrets
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 secrets names
mqtt_local_command_main_topic: !secret mqtt_local_command_main_topic
mqtt_local_status_main_topic: !secret mqtt_local_status_main_topic
# 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
# 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 REMOTE Controls
# Button Naming & Icons
# Switch/Relay Naming & Icons
##########################################################################################
# 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}"
on_boot:
priority: -200
then:
- lambda: |-
ESP_LOGI("boot", "Last reset reason: %s", ESP.getResetReason().c_str());
// Keep the HA dropdown in sync with the stored mode
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 (respect min/max & ramp time)
- 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 quickly
- if:
condition:
lambda: 'return id(restart_mode) == 1;'
then:
- lambda: |-
// Clamp the remembered brightness to valid bounds
float target = id(last_brightness_pct);
if (target < 0.0f) target = 0.0f;
if (target > 100.0f) target = 100.0f;
float minp = (float) id(min_brightness_pct);
float maxp = (float) id(max_brightness_pct);
if (target > 0.0f) {
if (target < minp) target = minp;
if (target > maxp) target = maxp;
}
id(suppress_slider_sync) = true;
if (target <= 0.0f) {
auto call = id(mosfet_leds).make_call();
call.set_state(false);
call.set_transition_length(0);
call.perform();
id(ramp_switch_target_on) = false;
} else {
auto call = id(mosfet_leds).make_call();
call.set_state(true);
call.set_brightness(target / 100.0f);
call.set_transition_length(150);
call.perform();
id(ramp_switch_target_on) = true;
}
- delay: 300ms
- lambda: 'id(suppress_slider_sync) = false;'
# Mode 2: Remain Off
- if:
condition:
lambda: 'return id(restart_mode) == 2;'
then:
- script.stop: ramp_on_script
- script.stop: ramp_off_script
- light.turn_off:
id: mosfet_leds
transition_length: 0s
- lambda: 'id(ramp_switch_target_on) = false;'
##########################################################################################
# ESP Platform and Framework
# https://esphome.io/components/esp32.html
##########################################################################################
esp8266:
board: esp01_1m # The original sonoff basic
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)
##########################################################################################
# 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, Serial control)
##########################################################################################
# GLOBALS: ramp times (milliseconds)
##########################################################################################
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'
#########################################################################################
# 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/
##########################################################################################
##########################################################################################
# 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 turn_off)
- 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: |-
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();
}
#########################################################################################
# SELECT SENSORS
#
#########################################################################################
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: GPIO12
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: LED / PWM output percentage (0100 %)
##########################################################################################
sensor:
- platform: template
id: mosfet_output_pct
name: "${friendly_name} Output (%)"
unit_of_measurement: "%"
icon: mdi:percent
accuracy_decimals: 0
update_interval: 100ms
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;
}
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
################################################################################
output:
- platform: esp8266_pwm
id: mosfet_pwm
pin: GPIO4
frequency: 2000 Hz # high frequency to avoid audible/visible artifacts
- platform: gpio
id: green_led_out # Green LED
pin:
number: GPIO13
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: 1.2
on_turn_on:
- lambda: 'id(ramp_switch_target_on) = true;'
on_turn_off:
- lambda: 'id(ramp_switch_target_on) = false;'
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); // clamp to max
call.set_transition_length(0); // snap to cap
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:
- 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);
- 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: 250ms
- lambda: 'id(ramp_switch_target_on) = true;'
- delay: 400ms
- lambda: 'id(suppress_slider_sync) = false;'
#################################################################################################
# SCRIPT COMPONENT
# https://esphome.io/components/script.html
#################################################################################################
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
- id: ramp_on_script
mode: restart
then:
- script.stop: ramp_off_script
- script.stop: led_flash_down
- script.execute: led_flash_up
- 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() < floor);
then:
- light.turn_on:
id: mosfet_leds
brightness: !lambda 'return id(min_brightness_pct) / 100.0f;'
transition_length: 0s
- 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;
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);
- 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);
- 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
then:
- 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;'
transition_length: !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);
# 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
- 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();