Files
zorruno-homeassistant/esphome/esp-downstairskitchleds.yaml
2025-08-22 00:28:23 +12:00

828 lines
32 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.

##########################################################################################
##########################################################################################
# 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
#
# v1.3 - 2025-08-22 Added a “max on time” setting (148 h, 0 = no limit)
# v1.2 - 2025-08-21 Added defaults to “Device Specific Settings” in substitutions & a PWM % view
# v1.1 - 2025-08-18 Full tidy-up as general-purpose LED strip controller
# 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.3)
# ------------------------------------------
# 1. General-purpose LED controller.
# 2. Designed for the Sinilink XY-WFMS board with a MOSFET output (claimed ~5 A, 536 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.
# 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 is to fade slowly up to full at power-up (so it can run with no network).
# 9. The green LED flashes while fading (different patterns for up/down). The red LED follows the
# output (it shares the MOSFET GPIO).
# 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
# - Fade Up / Fade Down / Fade Stop buttons
# - Fade Up/Down switch
# - Normal On/Off switch (quick ramp up/down)
# - Fade up/down times (060 s)
# - Output % (pre-gamma) and PWM % (post-gamma)
# - Output Set (1100, respects min/max)
# - Device diagnostics (from the included package)
# - Maximum “on” time before automatic fade-down (148 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-downstairskitchleds"
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.3" # 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-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
# MQTT LOCAL Controls
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"
# 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.23 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
##########################################################################################
# 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
file: common/api_common_noencryption.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
file: common/sensors_common_lite.yaml
vars:
local_friendly_name: "${friendly_name}"
local_update_interval: "${update_interval}"
##########################################################################################
# ESPHome CORE CONFIGURATION
# 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;'
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
#preferences:
# flash_write_interval: 5min
mdns:
disabled: false # Disabling will make the build file smaller (and it is still available via static IP)
##########################################################################################
# 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'
##########################################################################################
# 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)
##########################################################################################
# MQTT COMMANDS
# This adds device-specific MQTT command triggers to the common MQTT configuration.
##########################################################################################
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
# 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:
# 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 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: 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 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: 250ms # consider 200ms 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: |-
// Remember latest actual output (0..100) for "Restore Brightness"
id(last_brightness_pct) = 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 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: 250ms
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: GPIO4
frequency: 500 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: "${led_gamma}"
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;'
- script.stop: max_on_watchdog
- if:
condition:
lambda: 'return id(max_on_hours) > 0;'
then:
- script.execute: max_on_watchdog
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
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);
- 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;'
- 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:
- 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:
- 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;
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
then:
- 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.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;
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: |-
auto call = id(mosfet_leds).make_call();
call.set_state(false);
call.set_brightness(id(max_brightness_pct) / 100.0f);
call.perform();
- 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