########################################################################################## ########################################################################################## # Title: LOUNGE - PELMET LEDS (H801, 2x Monochrome PWM) # Repo: (new) # # v2.1-dual - 2025-09-30 Per-channel Min/Max caps moved internal (YAML-only), no HA sliders. # v2.0-dual - 2025-09-30 Based on single MOSFET version; adapted to H801 dual-monochrome (A/B). # v1.x (history from single-channel base maintained in comments below) # # ------------------------------------------ # OPERATION (as of v2.0) # ------------------------------------------ # 1. General-purpose LED controller. # 2. Designed here for the H801 5-MOSFET board (ESP8266). Using 2 channels as independent # monochrome outputs (A and B). The remaining 3 MOSFETs are ignored. # 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 some ESP8266 boards. 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 fading LEDs flash while fading (different patterns for up/down). Channel A uses the **RED** LED on GPIO5. # Channel B uses the **GREEN** LED on GPIO1 (UART TX; logger baud=0 to free it). # 10. Fade timing scales with the configured values (proportionally when starting mid-brightness). # 11. Exposed in Home Assistant/MQTT (duplicated for A and B): # - Startup action (On,Off,Restore) # - Fade Up / Fade Down / Fade Stop buttons # - 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). 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: H801 (ESP8266, 5 MOSFET outputs) # ------------------------------------------ # DEVICE GPIO (H801, as used here) # ------------------------------------------ # GPIO02 Blue Status LED, active-low (ESPHOME status) # GPIO12 MOSFET output A (PWM) # GPIO13 MOSFET output B (PWM) # GPIO05 Red LED (used to display fading status for Channel A) # GPIO01 Green LED (used to display fading status for Channel B) [UART TX, logger baud=0] # ########################################################################################## ########################################################################################## ########################################################################################## # 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-loungepelmetleds" # yaml file should be device_name.yaml friendly_name: "Lounge Pelmet LEDs" description_comment: "Lounge Pelmet LEDs :: H801 dual-channel monochrome" device_area: "Lounge" # Allows the ESP device to be automatically linked to an 'Area' in Home Assistant. # Project Naming project_name: "H801.5xMOSFET" # Project details. Manufacturer before the dot, device after the dot. project_version: "v2.1" # 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-loungepelmetleds_ip # CHANGE THIS # MQTT LOCAL Controls mqtt_local_device_name_A: "lounge-pelmetleds_A" mqtt_local_device_name_B: "lounge-pelmetleds_B" 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_A: "${mqtt_local_command_main_topic}/${mqtt_local_device_name_A}" mqtt_local_status_topic_A: "${mqtt_local_status_main_topic}/${mqtt_local_device_name_A}" mqtt_local_command_topic_B: "${mqtt_local_command_main_topic}/${mqtt_local_device_name_B}" mqtt_local_status_topic_B: "${mqtt_local_status_main_topic}/${mqtt_local_device_name_B}" mqtt_local_device_command_ON: "ON" mqtt_local_device_command_OFF: "OFF" # Device Specific Settings (shared defaults applied per channel below) log_level: "NONE" update_interval: "20s" led_gamma: "1.2" pwm_frequency: "1000hz" max_on_default_hours: "24" # Per-channel output caps (%), internal-only (no HA sliders) minimum_led_output_A: "1" maximum_led_output_A: "90" minimum_led_output_B: "1" maximum_led_output_B: "90" # Device Specific GPIO device_status_led: GPIO02 device_mosfet_out_A: GPIO12 device_mosfet_out_B: GPIO13 device_fading_led_A: GPIO05 device_fading_led_B: GPIO01 ########################################################################################## # PACKAGES ########################################################################################## 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_mqtt: !include file: common/mqtt_common.yaml vars: local_device_name: "${device_name}" #common_webportal: !include common/webportal_common.yaml #common_sntp: !include common/sntp_common.yaml 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 ########################################################################################## esphome: name: "${device_name}" friendly_name: "${friendly_name}" comment: "${description_comment}" area: "${device_area}" on_boot: priority: 200 then: - 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; } - if: condition: lambda: 'return id(restart_mode) == 0;' then: - lambda: 'id(ramp_switch_target_on_A) = true;' - lambda: 'id(ramp_switch_target_on_B) = true;' - script.execute: ramp_on_script_A - script.execute: ramp_on_script_B - if: condition: lambda: 'return id(restart_mode) == 1;' then: - script.execute: deferred_restore_brightness_A - script.execute: deferred_restore_brightness_B - if: condition: lambda: 'return id(restart_mode) == 2;' then: - script.stop: ramp_on_script_A - script.stop: ramp_off_script_A - lambda: 'id(ramp_switch_target_on_A) = false;' - light.turn_off: id: mosfet_leds_A transition_length: 0s - script.stop: ramp_on_script_B - script.stop: ramp_off_script_B - lambda: 'id(ramp_switch_target_on_B) = false;' - light.turn_off: id: mosfet_leds_B transition_length: 0s - lambda: 'id(booting) = false;' ########################################################################################## # ESP PLATFORM AND FRAMEWORK ########################################################################################## esp8266: board: esp01_1m restore_from_flash: true mdns: disabled: false preferences: flash_write_interval: 5sec ########################################################################################## # GLOBAL VARIABLES ########################################################################################## globals: # Per-channel brightness caps (internal; set via YAML substitutions) - id: min_brightness_pct_A type: int restore_value: true initial_value: "${minimum_led_output_A}" - id: max_brightness_pct_A type: int restore_value: false initial_value: "${maximum_led_output_A}" - id: min_brightness_pct_B type: int restore_value: true initial_value: "${minimum_led_output_B}" - id: max_brightness_pct_B type: int restore_value: false initial_value: "${maximum_led_output_B}" # Max on hours (shared) - id: max_on_hours type: int restore_value: true initial_value: "${max_on_default_hours}" # Per-channel Fade Up/Down times (seconds -> ms) - id: ramp_up_ms_A type: int restore_value: true initial_value: '5000' - id: ramp_down_ms_A type: int restore_value: true initial_value: '10000' - id: ramp_up_ms_B type: int restore_value: true initial_value: '5000' - id: ramp_down_ms_B type: int restore_value: true initial_value: '10000' # Restart Action. (0=Fade full, 1=Restore brightness, 2=Remain off) - id: restart_mode type: int restore_value: true initial_value: '0' # Per-channel runtime state (A) - id: ramp_switch_target_on_A type: bool restore_value: true initial_value: 'false' - id: suppress_slider_sync_A type: bool restore_value: false initial_value: 'false' - id: last_brightness_pct_A type: float restore_value: true initial_value: '0.0' - id: last_set_pos_A type: int restore_value: false initial_value: '-1' - id: last_ramp_ms_A type: int restore_value: false initial_value: '0' - id: last_nonzero_brightness_pct_A type: float restore_value: true initial_value: '0.0' - id: restore_target_pct_A type: float restore_value: false initial_value: '0.0' - id: is_ramping_A type: bool restore_value: false initial_value: 'false' - id: last_nonzero_tmp_A type: float restore_value: false initial_value: '0.0' - id: ramping_for_off_A type: bool restore_value: false initial_value: 'false' # Per-channel runtime state (B) - id: ramp_switch_target_on_B type: bool restore_value: true initial_value: 'false' - id: suppress_slider_sync_B type: bool restore_value: false initial_value: 'false' - id: last_brightness_pct_B type: float restore_value: true initial_value: '0.0' - id: last_set_pos_B type: int restore_value: false initial_value: '-1' - id: last_ramp_ms_B type: int restore_value: false initial_value: '0' - id: last_nonzero_brightness_pct_B type: float restore_value: true initial_value: '0.0' - id: restore_target_pct_B type: float restore_value: false initial_value: '0.0' - id: is_ramping_B type: bool restore_value: false initial_value: 'false' - id: last_nonzero_tmp_B type: float restore_value: false initial_value: '0.0' - id: ramping_for_off_B type: bool restore_value: false initial_value: 'false' # Guard for startup behaviour - id: booting type: bool restore_value: false initial_value: 'true' ########################################################################################## # LOGGER ########################################################################################## logger: level: "${log_level}" baud_rate: 0 ######################################################################################### # STATUS LED ######################################################################################### #status_led: # pin: # number: ${device_status_led} # inverted: true ########################################################################################## # MQTT COMMANDS ########################################################################################## mqtt: on_message: - topic: "${mqtt_local_command_topic_A}/lightA/set" payload: "${mqtt_local_device_command_ON}" then: - switch.turn_on: mosfet_ramp_switch_a - topic: "${mqtt_local_command_topic_A}/lightA/set" payload: "${mqtt_local_device_command_OFF}" then: - switch.turn_off: mosfet_ramp_switch_a - topic: "${mqtt_local_command_topic_B}/lightB/set" payload: "${mqtt_local_device_command_ON}" then: - switch.turn_on: mosfet_ramp_switch_b - topic: "${mqtt_local_command_topic_B}/lightB/set" payload: "${mqtt_local_device_command_OFF}" then: - switch.turn_off: mosfet_ramp_switch_b ########################################################################################## # SWITCHES ########################################################################################## switch: - platform: template id: mosfet_ramp_switch_a name: "${friendly_name} A Fade Up/Down" icon: mdi:led-strip-variant lambda: |- return id(ramp_switch_target_on_A); turn_on_action: - lambda: |- id(ramp_switch_target_on_A) = true; - script.stop: ramp_off_script_A - script.execute: ramp_on_script_A turn_off_action: - lambda: |- id(ramp_switch_target_on_A) = false; - script.stop: ramp_on_script_A - script.execute: ramp_off_script_A - platform: template id: mosfet_ramp_switch_b name: "${friendly_name} B Fade Up/Down" icon: mdi:led-strip-variant lambda: |- return id(ramp_switch_target_on_B); turn_on_action: - lambda: |- id(ramp_switch_target_on_B) = true; - script.stop: ramp_off_script_B - script.execute: ramp_on_script_B turn_off_action: - lambda: |- id(ramp_switch_target_on_B) = false; - script.stop: ramp_on_script_B - script.execute: ramp_off_script_B ################################################################################################# # BUTTONS (none) ################################################################################################# ######################################################################################### # SELECTS ######################################################################################### 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; } ########################################################################################## # SENSORS ########################################################################################## sensor: # Channel A: Output % (pre-gamma) - platform: template id: mosfet_output_pct_A name: "${friendly_name} A Output (%)" unit_of_measurement: "%" icon: mdi:percent accuracy_decimals: 0 update_interval: 250ms lambda: |- const auto &cv = id(mosfet_leds_A).current_values; return cv.is_on() ? (cv.get_brightness() * 100.0f) : 0.0f; on_value: then: - lambda: |- const auto &cv_now = id(mosfet_leds_A).current_values; if (cv_now.is_on()) { if (x > 0.5f) { if (!id(is_ramping_A)) id(last_nonzero_brightness_pct_A) = x; id(last_nonzero_tmp_A) = x; } } else { if (x > 0.5f) id(last_nonzero_tmp_A) = x; } if (!id(suppress_slider_sync_A)) { float actual = x; float minp = (float) id(min_brightness_pct_A); float maxp = (float) id(max_brightness_pct_A); 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_A)) { id(last_set_pos_A) = pos_i; id(led_output_set_pct_A).publish_state(pos_i); } } # Channel A: Output PWM % (post-gamma approx) - platform: template id: mosfet_output_pwm_pct_A name: "${friendly_name} A Output PWM (%)" unit_of_measurement: "%" icon: mdi:square-wave accuracy_decimals: 1 update_interval: 250ms lambda: |- const auto &cv = id(mosfet_leds_A).current_values; if (!cv.is_on()) return 0.0f; const float lin = cv.get_brightness(); // 0..1 linear brightness const float gamma = (float) atof("${led_gamma}"); float pwm = powf(lin, gamma); if (pwm < 0.0f) pwm = 0.0f; if (pwm > 1.0f) pwm = 1.0f; return pwm * 100.0f; # Channel B: Output % (pre-gamma) - platform: template id: mosfet_output_pct_B name: "${friendly_name} B Output (%)" unit_of_measurement: "%" icon: mdi:percent accuracy_decimals: 0 update_interval: 250ms lambda: |- const auto &cv = id(mosfet_leds_B).current_values; return cv.is_on() ? (cv.get_brightness() * 100.0f) : 0.0f; on_value: then: - lambda: |- const auto &cv_now = id(mosfet_leds_B).current_values; if (cv_now.is_on()) { if (x > 0.5f) { if (!id(is_ramping_B)) id(last_nonzero_brightness_pct_B) = x; id(last_nonzero_tmp_B) = x; } } else { if (x > 0.5f) id(last_nonzero_tmp_B) = x; } if (!id(suppress_slider_sync_B)) { float actual = x; float minp = (float) id(min_brightness_pct_B); float maxp = (float) id(max_brightness_pct_B); 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_B)) { id(last_set_pos_B) = pos_i; id(led_output_set_pct_B).publish_state(pos_i); } } # Channel B: Output PWM % (post-gamma approx) - platform: template id: mosfet_output_pwm_pct_B name: "${friendly_name} B Output PWM (%)" unit_of_measurement: "%" icon: mdi:square-wave accuracy_decimals: 1 update_interval: 250ms lambda: |- const auto &cv = id(mosfet_leds_B).current_values; if (!cv.is_on()) return 0.0f; const float lin = cv.get_brightness(); const float gamma = (float) atof("${led_gamma}"); float pwm = powf(lin, gamma); if (pwm < 0.0f) pwm = 0.0f; if (pwm > 1.0f) pwm = 1.0f; return pwm * 100.0f; ########################################################################################## # OUTPUTS ########################################################################################## output: - platform: esp8266_pwm id: mosfet_pwm_A pin: ${device_mosfet_out_A} frequency: ${pwm_frequency} - platform: esp8266_pwm id: mosfet_pwm_B pin: ${device_mosfet_out_B} frequency: ${pwm_frequency} - platform: gpio id: fading_led_out_A pin: number: ${device_fading_led_A} inverted: false - platform: gpio id: fading_led_out_B pin: number: ${device_fading_led_B} inverted: false ########################################################################################## # LIGHTS ########################################################################################## light: - platform: monochromatic id: mosfet_leds_A name: "${friendly_name} A" output: mosfet_pwm_A 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_A}/lightA/state" payload: "${mqtt_local_device_command_ON}" retain: true - lambda: |- if (!id(ramping_for_off_A)) { id(ramp_switch_target_on_A) = true; } - script.stop: max_on_watchdog_A - if: condition: lambda: 'return id(max_on_hours) > 0;' then: - script.execute: max_on_watchdog_A on_turn_off: - if: condition: lambda: 'return !id(booting);' then: - mqtt.publish: topic: "${mqtt_local_status_topic_A}/lightA/state" payload: "${mqtt_local_device_command_OFF}" retain: true - lambda: 'id(ramp_switch_target_on_A) = false;' - script.stop: max_on_watchdog_A - lambda: |- id(last_brightness_pct_A) = 0.0f; id(last_set_pos_A) = 0; id(led_output_set_pct_A).publish_state(0); on_state: - lambda: |- const float cap = id(max_brightness_pct_A) / 100.0f; const auto &cv = id(mosfet_leds_A).current_values; if (cv.is_on() && cv.get_brightness() > cap + 0.001f) { auto call = id(mosfet_leds_A).make_call(); call.set_state(true); call.set_brightness(cap); call.set_transition_length(0); call.perform(); } - platform: monochromatic id: mosfet_leds_B name: "${friendly_name} B" output: mosfet_pwm_B 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_B}/lightB/state" payload: "${mqtt_local_device_command_ON}" retain: true - lambda: |- if (!id(ramping_for_off_B)) { id(ramp_switch_target_on_B) = true; } - script.stop: max_on_watchdog_B - if: condition: lambda: 'return id(max_on_hours) > 0;' then: - script.execute: max_on_watchdog_B on_turn_off: - if: condition: lambda: 'return !id(booting);' then: - mqtt.publish: topic: "${mqtt_local_status_topic_B}/lightB/state" payload: "${mqtt_local_device_command_OFF}" retain: true - lambda: 'id(ramp_switch_target_on_B) = false;' - script.stop: max_on_watchdog_B - lambda: |- id(last_brightness_pct_B) = 0.0f; id(last_set_pos_B) = 0; id(led_output_set_pct_B).publish_state(0); on_state: - lambda: |- const float cap = id(max_brightness_pct_B) / 100.0f; const auto &cv = id(mosfet_leds_B).current_values; if (cv.is_on() && cv.get_brightness() > cap + 0.001f) { auto call = id(mosfet_leds_B).make_call(); call.set_state(true); call.set_brightness(cap); call.set_transition_length(0); call.perform(); } ########################################################################################## # NUMBERS ########################################################################################## number: - platform: template id: cfg_ramp_up_s_A name: "${friendly_name} A 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_A) / 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_A) = secs * 1000; id(cfg_ramp_up_s_A).publish_state((float) secs); - platform: template id: cfg_ramp_down_s_A name: "${friendly_name} A 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_A) / 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_A) = secs * 1000; id(cfg_ramp_down_s_A).publish_state((float) secs); - platform: template id: cfg_ramp_up_s_B name: "${friendly_name} B 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_B) / 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_B) = secs * 1000; id(cfg_ramp_up_s_B).publish_state((float) secs); - platform: template id: cfg_ramp_down_s_B name: "${friendly_name} B 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_B) / 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_B) = secs * 1000; id(cfg_ramp_down_s_B).publish_state((float) secs); # Per-channel position slider (0..100 mapped into [min..max]) - platform: template id: led_output_set_pct_A name: "${friendly_name} A Output Set (0-100)" icon: mdi:tune mode: slider min_value: 0 max_value: 100 step: 1 lambda: |- const auto &cv = id(mosfet_leds_A).current_values; float actual = cv.is_on() ? (cv.get_brightness() * 100.0f) : 0.0f; // 0..100 actual float minp = (float) id(min_brightness_pct_A); float maxp = (float) id(max_brightness_pct_A); 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); set_action: - if: condition: lambda: 'return x <= 0.0f;' then: - lambda: 'id(suppress_slider_sync_A) = true;' - script.stop: ramp_on_script_A - script.stop: ramp_off_script_A - light.turn_off: id: mosfet_leds_A transition_length: 200ms - lambda: |- id(ramp_switch_target_on_A) = false; id(led_output_set_pct_A).publish_state(0); id(last_brightness_pct_A) = 0.0f; id(last_set_pos_A) = 0; - delay: 400ms - lambda: 'id(suppress_slider_sync_A) = false;' else: - lambda: |- id(suppress_slider_sync_A) = 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_A).publish_state((int) floorf(pos + 0.5f)); - script.stop: ramp_off_script_A - script.stop: ramp_on_script_A - light.turn_on: id: mosfet_leds_A brightness: !lambda |- float pos = id(led_output_set_pct_A).state; // 1..100 float minp = (float) id(min_brightness_pct_A); float maxp = (float) id(max_brightness_pct_A); 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_A) = true; float pos = id(led_output_set_pct_A).state; // 1..100 float minp = (float) id(min_brightness_pct_A); float maxp = (float) id(max_brightness_pct_A); 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_A) = out_pct; if (out_pct > 0.5f) { id(last_nonzero_brightness_pct_A) = out_pct; id(last_nonzero_tmp_A) = out_pct; } - delay: 400ms - lambda: 'id(suppress_slider_sync_A) = false;' - platform: template id: led_output_set_pct_B name: "${friendly_name} B Output Set (0-100)" icon: mdi:tune mode: slider min_value: 0 max_value: 100 step: 1 lambda: |- const auto &cv = id(mosfet_leds_B).current_values; float actual = cv.is_on() ? (cv.get_brightness() * 100.0f) : 0.0f; // 0..100 actual float minp = (float) id(min_brightness_pct_B); float maxp = (float) id(max_brightness_pct_B); 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); set_action: - if: condition: lambda: 'return x <= 0.0f;' then: - lambda: 'id(suppress_slider_sync_B) = true;' - script.stop: ramp_on_script_B - script.stop: ramp_off_script_B - light.turn_off: id: mosfet_leds_B transition_length: 200ms - lambda: |- id(ramp_switch_target_on_B) = false; id(led_output_set_pct_B).publish_state(0); id(last_brightness_pct_B) = 0.0f; id(last_set_pos_B) = 0; - delay: 400ms - lambda: 'id(suppress_slider_sync_B) = false;' else: - lambda: |- id(suppress_slider_sync_B) = 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_B).publish_state((int) floorf(pos + 0.5f)); - script.stop: ramp_off_script_B - script.stop: ramp_on_script_B - light.turn_on: id: mosfet_leds_B brightness: !lambda |- float pos = id(led_output_set_pct_B).state; // 1..100 float minp = (float) id(min_brightness_pct_B); float maxp = (float) id(max_brightness_pct_B); 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_B) = true; float pos = id(led_output_set_pct_B).state; // 1..100 float minp = (float) id(min_brightness_pct_B); float maxp = (float) id(max_brightness_pct_B); 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_B) = out_pct; // persist exact target now if (out_pct > 0.5f) { id(last_nonzero_brightness_pct_B) = out_pct; id(last_nonzero_tmp_B) = out_pct; } - delay: 400ms - lambda: 'id(suppress_slider_sync_B) = false;' # Shared "Max On" hours - 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_A).current_values.is_on();' then: - script.stop: max_on_watchdog_A - if: condition: lambda: 'return id(max_on_hours) > 0;' then: - script.execute: max_on_watchdog_A - if: condition: lambda: 'return id(mosfet_leds_B).current_values.is_on();' then: - script.stop: max_on_watchdog_B - if: condition: lambda: 'return id(max_on_hours) > 0;' then: - script.execute: max_on_watchdog_B ########################################################################################## # SCRIPTS ########################################################################################## script: # Blink patterns while ramping (A) - id: led_flash_up_A mode: restart then: - while: condition: lambda: 'return true;' then: - output.turn_on: fading_led_out_A - delay: 100ms - output.turn_off: fading_led_out_A - delay: 100ms - output.turn_on: fading_led_out_A - delay: 100ms - output.turn_off: fading_led_out_A - delay: 400ms - id: led_flash_down_A mode: restart then: - while: condition: lambda: 'return true;' then: - output.turn_on: fading_led_out_A - delay: 250ms - output.turn_off: fading_led_out_A - delay: 250ms # Blink patterns while ramping (B) - id: led_flash_up_B mode: restart then: - while: condition: lambda: 'return true;' then: - output.turn_on: fading_led_out_B - delay: 100ms - output.turn_off: fading_led_out_B - delay: 100ms - output.turn_on: fading_led_out_B - delay: 100ms - output.turn_off: fading_led_out_B - delay: 400ms - id: led_flash_down_B mode: restart then: - while: condition: lambda: 'return true;' then: - output.turn_on: fading_led_out_B - delay: 250ms - output.turn_off: fading_led_out_B - delay: 250ms # Ramps: Channel A - id: ramp_on_script_A mode: restart then: - lambda: 'id(is_ramping_A) = true;' - script.stop: ramp_off_script_A - script.stop: led_flash_down_A - script.execute: led_flash_up_A - if: condition: lambda: |- const auto &cv = id(mosfet_leds_A).current_values; const float floor = id(min_brightness_pct_A) / 100.0f; return (!cv.is_on()) || (cv.get_brightness() + 0.0005f < floor); then: - light.turn_on: id: mosfet_leds_A brightness: !lambda 'return id(min_brightness_pct_A) / 100.0f;' transition_length: 80ms - light.turn_on: id: mosfet_leds_A brightness: !lambda 'return id(max_brightness_pct_A) / 100.0f;' transition_length: !lambda |- const auto &cv = id(mosfet_leds_A).current_values; const float floor = id(min_brightness_pct_A) / 100.0f; const float cap = id(max_brightness_pct_A) / 100.0f; if (cap <= floor + 0.0005f) { id(last_ramp_ms_A) = 0; return (uint32_t) 0; } float curr = cv.is_on() ? cv.get_brightness() : 0.0f; if (curr + 0.0005f < floor) { id(last_ramp_ms_A) = id(ramp_up_ms_A); return (uint32_t) id(last_ramp_ms_A); } 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_A) = (int) (id(ramp_up_ms_A) * frac); return (uint32_t) id(last_ramp_ms_A); - delay: !lambda 'return (uint32_t) id(last_ramp_ms_A);' - script.stop: led_flash_up_A - output.turn_off: fading_led_out_A - lambda: |- id(is_ramping_A) = false; const auto &cv = id(mosfet_leds_A).current_values; if (cv.is_on()) { float pct = cv.get_brightness() * 100.0f; id(last_brightness_pct_A) = pct; if (pct > 0.5f) id(last_nonzero_brightness_pct_A) = pct; } - id: ramp_to_target_script_A mode: restart then: - lambda: 'id(is_ramping_A) = true;' - script.stop: ramp_off_script_A - script.stop: led_flash_down_A - script.execute: led_flash_up_A - if: condition: lambda: |- const auto &cv = id(mosfet_leds_A).current_values; const float floor = id(min_brightness_pct_A) / 100.0f; return (!cv.is_on()) || (cv.get_brightness() + 0.0005f < floor); then: - light.turn_on: id: mosfet_leds_A brightness: !lambda 'return id(min_brightness_pct_A) / 100.0f;' transition_length: 80ms - light.turn_on: id: mosfet_leds_A brightness: !lambda |- float cap_pct = id(restore_target_pct_A); float maxp = (float) id(max_brightness_pct_A); if (cap_pct > maxp) cap_pct = maxp; return cap_pct / 100.0f; transition_length: !lambda |- const auto &cv = id(mosfet_leds_A).current_values; const float floor = id(min_brightness_pct_A) / 100.0f; float cap = id(restore_target_pct_A) / 100.0f; const float maxp = id(max_brightness_pct_A) / 100.0f; if (cap > maxp) cap = maxp; if (cap <= floor + 0.0005f) { id(last_ramp_ms_A) = 0; return (uint32_t) 0; } float curr = cv.is_on() ? cv.get_brightness() : 0.0f; if (curr + 0.0005f < floor) { id(last_ramp_ms_A) = id(ramp_up_ms_A); return (uint32_t) id(last_ramp_ms_A); } 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_A) = (int) (id(ramp_up_ms_A) * frac); return (uint32_t) id(last_ramp_ms_A); - delay: !lambda 'return (uint32_t) id(last_ramp_ms_A);' - script.stop: led_flash_up_A - output.turn_off: fading_led_out_A - lambda: |- id(is_ramping_A) = false; const auto &cv = id(mosfet_leds_A).current_values; if (cv.is_on()) { float pct = cv.get_brightness() * 100.0f; id(last_brightness_pct_A) = pct; if (pct > 0.5f) id(last_nonzero_brightness_pct_A) = pct; } - id: ramp_off_script_A mode: restart then: - lambda: |- id(is_ramping_A) = true; id(ramping_for_off_A) = true; - script.stop: ramp_on_script_A - script.stop: led_flash_up_A - script.execute: led_flash_down_A - light.turn_on: id: mosfet_leds_A brightness: !lambda 'return id(min_brightness_pct_A) / 100.0f;' transition_length: !lambda |- const auto &cv = id(mosfet_leds_A).current_values; const float floor = id(min_brightness_pct_A) / 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_A) = 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_A) = (int) (id(ramp_down_ms_A) * frac); return (uint32_t) id(last_ramp_ms_A); - delay: !lambda 'return (uint32_t) id(last_ramp_ms_A);' - light.turn_off: id: mosfet_leds_A transition_length: 150ms - delay: 150ms - script.stop: led_flash_down_A - output.turn_off: fading_led_out_A - lambda: |- id(is_ramping_A) = false; id(ramping_for_off_A) = false; id(last_brightness_pct_A) = 0.0f; id(last_set_pos_A) = 0; id(led_output_set_pct_A).publish_state(0); - id: max_on_watchdog_A 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_A).current_values.is_on();' then: - lambda: |- id(ramp_switch_target_on_A) = false; id(mosfet_ramp_switch_a).publish_state(false); - script.stop: ramp_on_script_A - script.execute: ramp_off_script_A - id: deferred_restore_brightness_A mode: restart then: - delay: 5s - lambda: |- float target = id(last_brightness_pct_A); float minp = (float) id(min_brightness_pct_A); float maxp = (float) id(max_brightness_pct_A); if (target > 0.0f && target < minp) target = minp; if (target > maxp) target = maxp; id(restore_target_pct_A) = target; - if: condition: lambda: 'return id(restore_target_pct_A) > 0.5f;' then: - lambda: |- id(ramp_switch_target_on_A) = true; id(suppress_slider_sync_A) = true; - script.stop: ramp_off_script_A - script.execute: ramp_to_target_script_A - delay: !lambda 'return (uint32_t) id(last_ramp_ms_A);' - lambda: 'id(suppress_slider_sync_A) = false;' else: - lambda: 'id(ramp_switch_target_on_A) = false;' - light.turn_off: id: mosfet_leds_A transition_length: 0s # Ramps: Channel B - id: ramp_on_script_B mode: restart then: - lambda: 'id(is_ramping_B) = true;' - script.stop: ramp_off_script_B - script.stop: led_flash_down_B - script.execute: led_flash_up_B - if: condition: lambda: |- const auto &cv = id(mosfet_leds_B).current_values; const float floor = id(min_brightness_pct_B) / 100.0f; return (!cv.is_on()) || (cv.get_brightness() + 0.0005f < floor); then: - light.turn_on: id: mosfet_leds_B brightness: !lambda 'return id(min_brightness_pct_B) / 100.0f;' transition_length: 80ms - light.turn_on: id: mosfet_leds_B brightness: !lambda 'return id(max_brightness_pct_B) / 100.0f;' transition_length: !lambda |- const auto &cv = id(mosfet_leds_B).current_values; const float floor = id(min_brightness_pct_B) / 100.0f; const float cap = id(max_brightness_pct_B) / 100.0f; if (cap <= floor + 0.0005f) { id(last_ramp_ms_B) = 0; return (uint32_t) 0; } float curr = cv.is_on() ? cv.get_brightness() : 0.0f; if (curr + 0.0005f < floor) { id(last_ramp_ms_B) = id(ramp_up_ms_B); return (uint32_t) id(last_ramp_ms_B); } 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_B) = (int) (id(ramp_up_ms_B) * frac); return (uint32_t) id(last_ramp_ms_B); - delay: !lambda 'return (uint32_t) id(last_ramp_ms_B);' - script.stop: led_flash_up_B - output.turn_off: fading_led_out_B - lambda: |- id(is_ramping_B) = false; const auto &cv = id(mosfet_leds_B).current_values; if (cv.is_on()) { float pct = cv.get_brightness() * 100.0f; id(last_brightness_pct_B) = pct; if (pct > 0.5f) id(last_nonzero_brightness_pct_B) = pct; } - id: ramp_to_target_script_B mode: restart then: - lambda: 'id(is_ramping_B) = true;' - script.stop: ramp_off_script_B - script.stop: led_flash_down_B - script.execute: led_flash_up_B - if: condition: lambda: |- const auto &cv = id(mosfet_leds_B).current_values; const float floor = id(min_brightness_pct_B) / 100.0f; return (!cv.is_on()) || (cv.get_brightness() + 0.0005f < floor); then: - light.turn_on: id: mosfet_leds_B brightness: !lambda 'return id(min_brightness_pct_B) / 100.0f;' transition_length: 80ms - light.turn_on: id: mosfet_leds_B brightness: !lambda |- float cap_pct = id(restore_target_pct_B); float maxp = (float) id(max_brightness_pct_B); if (cap_pct > maxp) cap_pct = maxp; return cap_pct / 100.0f; transition_length: !lambda |- const auto &cv = id(mosfet_leds_B).current_values; const float floor = id(min_brightness_pct_B) / 100.0f; float cap = id(restore_target_pct_B) / 100.0f; const float maxp = id(max_brightness_pct_B) / 100.0f; if (cap > maxp) cap = maxp; if (cap <= floor + 0.0005f) { id(last_ramp_ms_B) = 0; return (uint32_t) 0; } float curr = cv.is_on() ? cv.get_brightness() : 0.0f; if (curr + 0.0005f < floor) { id(last_ramp_ms_B) = id(ramp_up_ms_B); return (uint32_t) id(last_ramp_ms_B); } 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_B) = (int) (id(ramp_up_ms_B) * frac); return (uint32_t) id(last_ramp_ms_B); - delay: !lambda 'return (uint32_t) id(last_ramp_ms_B);' - script.stop: led_flash_up_B - output.turn_off: fading_led_out_B - lambda: |- id(is_ramping_B) = false; const auto &cv = id(mosfet_leds_B).current_values; if (cv.is_on()) { float pct = cv.get_brightness() * 100.0f; id(last_brightness_pct_B) = pct; if (pct > 0.5f) id(last_nonzero_brightness_pct_B) = pct; } - id: ramp_off_script_B mode: restart then: - lambda: |- id(is_ramping_B) = true; id(ramping_for_off_B) = true; - script.stop: ramp_on_script_B - script.stop: led_flash_up_B - script.execute: led_flash_down_B - light.turn_on: id: mosfet_leds_B brightness: !lambda 'return id(min_brightness_pct_B) / 100.0f;' transition_length: !lambda |- const auto &cv = id(mosfet_leds_B).current_values; const float floor = id(min_brightness_pct_B) / 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_B) = 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_B) = (int) (id(ramp_down_ms_B) * frac); return (uint32_t) id(last_ramp_ms_B); - delay: !lambda 'return (uint32_t) id(last_ramp_ms_B);' - light.turn_off: id: mosfet_leds_B transition_length: 150ms - delay: 150ms - script.stop: led_flash_down_B - output.turn_off: fading_led_out_B - lambda: |- id(is_ramping_B) = false; id(ramping_for_off_B) = false; id(last_brightness_pct_B) = 0.0f; id(last_set_pos_B) = 0; id(led_output_set_pct_B).publish_state(0); - id: max_on_watchdog_B 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_B).current_values.is_on();' then: - lambda: |- id(ramp_switch_target_on_B) = false; id(mosfet_ramp_switch_b).publish_state(false); - script.stop: ramp_on_script_B - script.execute: ramp_off_script_B - id: deferred_restore_brightness_B mode: restart then: - delay: 5s - lambda: |- float target = id(last_brightness_pct_B); float minp = (float) id(min_brightness_pct_B); float maxp = (float) id(max_brightness_pct_B); if (target > 0.0f && target < minp) target = minp; if (target > maxp) target = maxp; id(restore_target_pct_B) = target; - if: condition: lambda: 'return id(restore_target_pct_B) > 0.5f;' then: - lambda: |- id(ramp_switch_target_on_B) = true; id(suppress_slider_sync_B) = true; - script.stop: ramp_off_script_B - script.execute: ramp_to_target_script_B - delay: !lambda 'return (uint32_t) id(last_ramp_ms_B);' - lambda: 'id(suppress_slider_sync_B) = false;' else: - lambda: 'id(ramp_switch_target_on_B) = false;' - light.turn_off: id: mosfet_leds_B transition_length: 0s