#:########################################################################################:# # TITLE: MASTER BEDROOM BEDSIDE LAMP BULB # zorruno.com layout v1.1 2026 #:########################################################################################:# # REPO: # https://home.fox.co.nz/gitea/zorruno/zorruno-homeassistant/src/branch/master/esphome/esp-bedrm1arlecbulb.yaml #:########################################################################################:# # VERSIONS: # v1.3 2026-07-20 Simplified local MQTT command and status topics to the device root # v1.2 2026-02-25 Updated yaml to zorruno layout V1.1 # v1.1 2026-02-21 Add "Jump Fade Up/Down" switch + configurable Jump Fade (%) # v1.0 2026-02-21 Initial version (ported from Athom bulb YAML to Arlec GLD120HA) #:########################################################################################:# # HARDWARE: # - Arlec Grid Connect Smart LED Globe RGB (GLD120HA) - B22 # - ESP8266 (PWM, 5x channels) RGB + Warm White + Cold White (RGBWW) # - PWM channels: # - Red GPIO4 # - Green GPIO12 # - Blue GPIO14 # - Warm White GPIO13 # - Cold White GPIO5 #:########################################################################################:# # OPERATION NOTES: # - Keeps the "preferred control" ramping / min-max / watchdog / restore logic. # - All ramping and "Output Set (0-100)" controls FORCE the bulb into Warm White endpoint # (everyday dimming uses Warm White channel only, not mixed CT). # - Full RGB and CT control is still available from HA using the normal light entity. # - Max On watchdog turns the whole bulb OFF at the time limit (all channels). # - Jump Fade switch behavior: # - ON: jump to (jump% of max cap), then fade up to full (normal fade-up). # - OFF: jump down to (current - current*jump%), then fade down to OFF (normal fade-down). #:########################################################################################:# # OFFLINE NOTES: # a) Home Assistant OFFLINE, but Network and MQTT ONLINE # - Bulb continues local operation and restores per configured restart action. # - MQTT state publishing may not be visible in HA until HA returns. # b) MQTT OFFLINE (or broker unreachable) # - Bulb still operates via HA API (if available) and local logic. # - MQTT local command topics will not work while MQTT is down. # c) Entire WiFi/Network OFFLINE # - Bulb still operates locally (last state/restore behavior as configured). # - Accurate time is NOT needed (SNTP not needed). #:########################################################################################:# #:########################################################################################:# # SUBSTITUTIONS: 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-bedrm1arlecbulb" # yaml file should be device_name.yaml friendly_name: "Master Bedroom Bedside Lamp Arlec Bulb" description_comment: "Bedside lightbulb using gridconnect Arlec RGB bulb (Layout V1.1)" device_area: "Master Bedroom" # Project Naming project_name: "Grid Connect GLD120HA RGBWW.Arlec" project_version: "v1.3" # Passwords & Secrets (Unfortunately, you can't use substitutions inside secret names) # Re-using your existing bulb flasher secrets (adjust if you prefer per-device secrets) api_key: !secret esp-api_key ota_pass: !secret esp-ota_pass static_ip_address: !secret esp-bedrm1arlecbulb_ip # If we are changing IP addresses, you must update the current IP address here, otherwise it remains # Don't forget to switch it back when changed. current_ip_address: ${static_ip_address} # MQTT LOCAL Controls (optional local control method) mqtt_local_device_name: "bed1-bedsidelamp" mqtt_local_command_main_topic: !secret mqtt_command_main_topic mqtt_local_status_main_topic: !secret mqtt_status_main_topic mqtt_local_command_topic: "${mqtt_local_command_main_topic}/${mqtt_local_device_name}" mqtt_local_status_topic: "${mqtt_local_status_main_topic}/${mqtt_local_device_name}" mqtt_local_device_command_ON: "ON" mqtt_local_device_command_OFF: "OFF" # Device Specific Settings log_level: "NONE" # NONE, ERROR, WARN, INFO, DEBUG, VERBOSE, VERY_VERBOSE update_interval: "20s" led_gamma: "2.8" minimum_led_output: "1" # % If at this value or below, we'll switch it completely off maximum_led_output: "80" # % Hard cap max_on_default_hours: "24" # 0 = no automatic turn-off pwm_frequency: "1000" # Hz # Warm/cold endpoints (reported range around 3000K-5700K) bulb_warm_kelvin: "3000 K" bulb_cold_kelvin: "5700 K" # Arlec GLD120HA GPIO mapping (PWM) bulb_red_pin: GPIO4 bulb_green_pin: GPIO12 bulb_blue_pin: GPIO14 bulb_warm_white_pin: GPIO13 bulb_cold_white_pin: GPIO5 #:########################################################################################:# # PACKAGES: Included Common Packages # https://esphome.io/components/packages.html #:########################################################################################:# packages: #### WIFI, Network (Static/DHCP/IPV6 etc), Fallback AP, Safemode #### common_wifi: !include file: common/network_common.yaml vars: local_device_name: "${device_name}" local_static_ip_address: "${static_ip_address}" local_ota_pass: "${ota_pass}" local_current_ip_address: "${current_ip_address}" #### HOME ASSISTANT API #### common_api: !include file: common/api_common.yaml vars: local_api_key: "${api_key}" #### MQTT #### common_mqtt: !include file: common/mqtt_common.yaml vars: local_device_name: "${device_name}" #### DIAGNOSTICS Sensors #### diag_basic: !include common/include_basic_diag_sensors.yaml diag_more: !include common/include_more_diag_sensors.yaml #diag_debug: !include common/include_debug_diag_sensors.yaml #:########################################################################################:# # ESPHOME CORE CONFIGURATION # https://esphome.io/components/esphome.html #:########################################################################################:# esphome: name: "${device_name}" friendly_name: "${friendly_name}" comment: "${description_comment}" area: "${device_area}" project: name: "${project_name}" version: "${project_version}" on_boot: # Run later so everything is initialised before we touch states. priority: 200 then: # Keep the HA dropdown in sync with the stored mode - lambda: |- switch (id(restart_mode)) { case 0: id(restart_action).publish_state("Fade up to full"); break; case 1: id(restart_action).publish_state("Restore Brightness"); break; case 2: default: id(restart_action).publish_state("Remain Off"); break; } # Mode 0: Fade up to full (obeys fade settings; forces Warm endpoint) - if: condition: lambda: "return id(restart_mode) == 0;" then: - lambda: "id(ramp_switch_target_on) = true;" - script.execute: ramp_on_script # Mode 1: Restore Brightness (deferred; forces Warm endpoint) - if: condition: lambda: "return id(restart_mode) == 1;" then: - script.execute: deferred_restore_brightness # Mode 2: Remain Off - if: condition: lambda: "return id(restart_mode) == 2;" then: - script.stop: ramp_on_script - script.stop: ramp_off_script - lambda: "id(ramp_switch_target_on) = false;" - light.turn_off: id: bulb_light transition_length: 0s # Boot complete: allow OFF handlers again - lambda: "id(booting) = false;" #:########################################################################################:# # ESP PLATFORM AND FRAMEWORK # https://esphome.io/components/esp8266.html #:########################################################################################:# esp8266: board: esp01_1m restore_from_flash: true mdns: disabled: true preferences: flash_write_interval: 1min #:########################################################################################:# # GLOBAL VARIABLES # https://esphome.io/components/globals.html #:########################################################################################:# globals: - id: min_brightness_pct type: int restore_value: true initial_value: "${minimum_led_output}" - id: max_brightness_pct type: int restore_value: false initial_value: "${maximum_led_output}" - id: max_on_hours type: int restore_value: true initial_value: "${max_on_default_hours}" - id: led_gamma_f type: float restore_value: false initial_value: ${led_gamma} - id: ramp_up_ms type: int restore_value: true initial_value: "5000" - id: ramp_down_ms type: int restore_value: true initial_value: "20000" - id: restart_mode type: int restore_value: true initial_value: "0" # 0=Fade full, 1=Restore brightness, 2=Remain off - 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: last_brightness_pct type: float restore_value: true initial_value: "0.0" - id: last_set_pos type: int restore_value: false initial_value: "-1" - id: last_ramp_ms type: int restore_value: false initial_value: "0" - id: restore_target_pct type: float restore_value: false initial_value: "0.0" - id: is_ramping type: bool restore_value: false initial_value: "false" - id: booting type: bool restore_value: false initial_value: "true" - id: ramping_for_off type: bool restore_value: false initial_value: "false" # Jump Fade percentage - id: jump_fade_pct type: int restore_value: true initial_value: "15" #:########################################################################################:# # LOGGING # https://esphome.io/components/logger.html #:########################################################################################:# logger: level: "${log_level}" baud_rate: 0 #:########################################################################################:# # MQTT COMMANDS (adds device-specific triggers to common MQTT) #:########################################################################################:# mqtt: on_message: - topic: "${mqtt_local_command_topic}" payload: "${mqtt_local_device_command_ON}" then: - switch.turn_on: bulb_ramp_switch - topic: "${mqtt_local_command_topic}" payload: "${mqtt_local_device_command_OFF}" then: - switch.turn_off: bulb_ramp_switch #:########################################################################################:# # SWITCH # https://esphome.io/components/switch/ #:########################################################################################:# switch: - platform: template id: bulb_ramp_switch name: "${friendly_name} Fade Up-Down" icon: mdi:lightbulb-on-outline 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 - platform: template id: bulb_jump_ramp_switch name: "${friendly_name} Jump Fade Up-Down" icon: mdi:lightbulb-auto lambda: |- return id(ramp_switch_target_on); turn_on_action: - lambda: |- id(ramp_switch_target_on) = true; - script.execute: jump_ramp_on_script turn_off_action: - lambda: |- id(ramp_switch_target_on) = false; - script.execute: jump_ramp_off_script #:########################################################################################:# # BUTTON # https://esphome.io/components/button/index.html #:########################################################################################:# button: - 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(bulb_ramp_switch).publish_state(true); - script.stop: ramp_off_script - script.execute: ramp_on_script - 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(bulb_ramp_switch).publish_state(false); - script.stop: ramp_on_script - script.execute: ramp_off_script - platform: template id: fade_stop_button name: "${friendly_name} Fade Stop" icon: mdi:pause on_press: - script.stop: ramp_on_script - script.stop: ramp_off_script - lambda: |- id(ramping_for_off) = false; id(is_ramping) = false; const auto &cv = id(bulb_light).current_values; if (cv.is_on()) { auto call = id(bulb_light).make_call(); call.set_state(true); call.set_brightness(cv.get_brightness()); call.set_transition_length(0); call.perform(); } if (cv.is_on()) { float pct = cv.get_brightness() * 100.0f; id(last_brightness_pct) = pct; } #:########################################################################################:# # SELECT # 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; } #:########################################################################################:# # SENSORS # https://esphome.io/components/sensor/ #:########################################################################################:# sensor: - platform: template id: bulb_output_pct name: "${friendly_name} Output (%)" unit_of_measurement: "%" icon: mdi:percent accuracy_decimals: 0 update_interval: 2s lambda: |- const auto &cv = id(bulb_light).current_values; return cv.is_on() ? (float) (int)(cv.get_brightness() * 100.0f + 0.5f) : 0.0f; on_value: then: - lambda: |- if (!id(suppress_slider_sync)) { float actual = x; float minp = (float) id(min_brightness_pct); float maxp = (float) id(max_brightness_pct); if (maxp <= minp) maxp = minp + 1.0f; float pos = (actual <= 0.0f) ? 0.0f : ((actual - minp) * 100.0f / (maxp - minp)); if (pos < 0.0f) pos = 0.0f; if (pos > 100.0f) pos = 100.0f; int pos_i = (int) floorf(pos + 0.5f); if (pos_i != id(last_set_pos)) { id(last_set_pos) = pos_i; id(led_output_set_pct).publish_state(pos_i); } } - platform: template id: bulb_output_pwm_pct name: "${friendly_name} Output PWM (%)" unit_of_measurement: "%" icon: mdi:square-wave accuracy_decimals: 1 update_interval: 2s lambda: |- const auto &cv = id(bulb_light).current_values; if (!cv.is_on()) return 0.0f; const float lin = cv.get_brightness(); float pwm = powf(lin, id(led_gamma_f)); if (pwm < 0.0f) pwm = 0.0f; if (pwm > 1.0f) pwm = 1.0f; return pwm * 100.0f; #:########################################################################################:# # OUTPUTS: 5 x PWM channels for the Arlec GLD120HA RGBWW bulb # https://esphome.io/components/output/ #:########################################################################################:# output: - platform: esp8266_pwm id: output_red pin: ${bulb_red_pin} frequency: ${pwm_frequency} - platform: esp8266_pwm id: output_green pin: ${bulb_green_pin} frequency: ${pwm_frequency} - platform: esp8266_pwm id: output_blue pin: ${bulb_blue_pin} frequency: ${pwm_frequency} - platform: esp8266_pwm id: output_warm_white pin: ${bulb_warm_white_pin} frequency: ${pwm_frequency} - platform: esp8266_pwm id: output_cold_white pin: ${bulb_cold_white_pin} frequency: ${pwm_frequency} #:########################################################################################:# # LIGHT: RGBWW (correct for this bulb) # https://esphome.io/components/light/ #:########################################################################################:# light: - platform: rgbww id: bulb_light name: "${friendly_name}" icon: mdi:lightbulb red: output_red green: output_green blue: output_blue warm_white: output_warm_white cold_white: output_cold_white warm_white_color_temperature: ${bulb_warm_kelvin} cold_white_color_temperature: ${bulb_cold_kelvin} color_interlock: true # Keep OFF at boot to avoid instant restore flashes; boot script handles behavior. restore_mode: ALWAYS_OFF default_transition_length: 2s gamma_correct: "${led_gamma}" on_turn_on: - mqtt.publish: topic: "${mqtt_local_status_topic}" payload: "${mqtt_local_device_command_ON}" retain: true - lambda: |- if (!id(ramping_for_off)) { id(ramp_switch_target_on) = true; } - script.stop: max_on_watchdog - if: condition: lambda: "return id(max_on_hours) > 0;" then: - script.execute: max_on_watchdog on_turn_off: - if: condition: lambda: "return !id(booting);" then: - mqtt.publish: topic: "${mqtt_local_status_topic}" payload: "${mqtt_local_device_command_OFF}" retain: true - lambda: "id(ramp_switch_target_on) = false;" - script.stop: max_on_watchdog - lambda: |- id(last_brightness_pct) = 0.0f; id(last_set_pos) = 0; id(led_output_set_pct).publish_state(0); on_state: - lambda: |- const float cap = id(max_brightness_pct) / 100.0f; const auto &cv = id(bulb_light).current_values; if (!cv.is_on()) return; const float b = cv.get_brightness(); if (b <= cap + 0.001f) return; auto call = id(bulb_light).make_call(); call.set_state(true); call.set_brightness(cap); call.set_transition_length(0); call.perform(); #:########################################################################################:# # NUMBER # 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: cfg_jump_fade_pct name: "${friendly_name} Jump Fade (%)" entity_category: config unit_of_measurement: "%" icon: mdi:percent mode: slider min_value: 0 max_value: 100 step: 1 lambda: |- return (float) id(jump_fade_pct); set_action: - lambda: |- int v = (int) floorf(x + 0.5f); if (v < 0) v = 0; if (v > 100) v = 100; id(jump_fade_pct) = v; id(cfg_jump_fade_pct).publish_state((float) v); - 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 lambda: |- const auto &cv = id(bulb_light).current_values; float actual = cv.is_on() ? (cv.get_brightness() * 100.0f) : 0.0f; float minp = (float) id(min_brightness_pct); float maxp = (float) id(max_brightness_pct); if (maxp <= minp) maxp = minp + 1.0f; if (actual <= 0.0f) return 0.0f; 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) = true;" - script.stop: ramp_on_script - script.stop: ramp_off_script - light.turn_off: id: bulb_light transition_length: 200ms - lambda: |- id(ramp_switch_target_on) = false; id(led_output_set_pct).publish_state(0); id(last_brightness_pct) = 0.0f; id(last_set_pos) = 0; - delay: 400ms - lambda: "id(suppress_slider_sync) = false;" else: - lambda: |- id(suppress_slider_sync) = true; float pos = x; if (pos < 1.0f) pos = 1.0f; 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: bulb_light color_temperature: "${bulb_warm_kelvin}" brightness: !lambda |- float pos = id(led_output_set_pct).state; 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; float pos = id(led_output_set_pct).state; float minp = (float) id(min_brightness_pct); float maxp = (float) id(max_brightness_pct); if (maxp <= minp) maxp = minp + 1.0f; float out_pct = minp + (pos * (maxp - minp) / 100.0f); if (out_pct > maxp) out_pct = maxp; id(last_brightness_pct) = out_pct; - 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(bulb_light).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 #:########################################################################################:# # SCRIPTS # https://esphome.io/components/script.html #:########################################################################################:# script: - id: ramp_on_script mode: restart then: - lambda: "id(is_ramping) = true;" - script.stop: ramp_off_script - if: condition: lambda: |- const auto &cv = id(bulb_light).current_values; const float floor = id(min_brightness_pct) / 100.0f; return (!cv.is_on()) || (cv.get_brightness() + 0.0005f < floor); then: - light.turn_on: id: bulb_light color_temperature: "${bulb_warm_kelvin}" brightness: !lambda "return id(min_brightness_pct) / 100.0f;" transition_length: 80ms - light.turn_on: id: bulb_light color_temperature: "${bulb_warm_kelvin}" brightness: !lambda "return id(max_brightness_pct) / 100.0f;" transition_length: !lambda |- const auto &cv = id(bulb_light).current_values; const float floor = id(min_brightness_pct) / 100.0f; const float cap = id(max_brightness_pct) / 100.0f; if (cap <= floor + 0.0005f) { id(last_ramp_ms) = 0; return (uint32_t) 0; } float curr = cv.is_on() ? cv.get_brightness() : 0.0f; if (curr + 0.0005f < floor) { id(last_ramp_ms) = id(ramp_up_ms); return (uint32_t) id(last_ramp_ms); } if (curr > cap) curr = cap; if (curr < floor) curr = floor; float frac = (cap - curr) / (cap - floor); if (frac < 0.0f) frac = 0.0f; if (frac > 1.0f) frac = 1.0f; id(last_ramp_ms) = (int) (id(ramp_up_ms) * frac); return (uint32_t) id(last_ramp_ms); - delay: !lambda "return (uint32_t) id(last_ramp_ms);" - lambda: |- id(is_ramping) = false; const auto &cv = id(bulb_light).current_values; if (cv.is_on()) { float pct = cv.get_brightness() * 100.0f; id(last_brightness_pct) = pct; } - id: ramp_to_target_script mode: restart then: - lambda: "id(is_ramping) = true;" - script.stop: ramp_off_script - if: condition: lambda: |- const auto &cv = id(bulb_light).current_values; const float floor = id(min_brightness_pct) / 100.0f; return (!cv.is_on()) || (cv.get_brightness() + 0.0005f < floor); then: - light.turn_on: id: bulb_light color_temperature: "${bulb_warm_kelvin}" brightness: !lambda "return id(min_brightness_pct) / 100.0f;" transition_length: 80ms - light.turn_on: id: bulb_light color_temperature: "${bulb_warm_kelvin}" brightness: !lambda |- float cap_pct = id(restore_target_pct); float maxp = (float) id(max_brightness_pct); if (cap_pct > maxp) cap_pct = maxp; return cap_pct / 100.0f; transition_length: !lambda |- const auto &cv = id(bulb_light).current_values; const float floor = id(min_brightness_pct) / 100.0f; float cap = id(restore_target_pct) / 100.0f; const float maxp = id(max_brightness_pct) / 100.0f; if (cap > maxp) cap = maxp; if (cap <= floor + 0.0005f) { id(last_ramp_ms) = 0; return (uint32_t) 0; } float curr = cv.is_on() ? cv.get_brightness() : 0.0f; if (curr + 0.0005f < floor) { id(last_ramp_ms) = id(ramp_up_ms); return (uint32_t) id(last_ramp_ms); } if (curr > cap) curr = cap; if (curr < floor) curr = floor; float frac = (cap - curr) / (cap - floor); if (frac < 0.0f) frac = 0.0f; if (frac > 1.0f) frac = 1.0f; id(last_ramp_ms) = (int) (id(ramp_up_ms) * frac); return (uint32_t) id(last_ramp_ms); - delay: !lambda "return (uint32_t) id(last_ramp_ms);" - lambda: |- id(is_ramping) = false; const auto &cv = id(bulb_light).current_values; if (cv.is_on()) { float pct = cv.get_brightness() * 100.0f; id(last_brightness_pct) = pct; } - id: ramp_off_script mode: restart then: - lambda: |- id(is_ramping) = true; id(ramping_for_off) = true; - script.stop: ramp_on_script - light.turn_on: id: bulb_light color_temperature: "${bulb_warm_kelvin}" brightness: !lambda "return id(min_brightness_pct) / 100.0f;" transition_length: !lambda |- const auto &cv = id(bulb_light).current_values; const float floor = id(min_brightness_pct) / 100.0f; float curr = cv.get_brightness(); if (curr < floor) curr = floor; const float denom = (1.0f - floor); if (denom <= 0.0005f) { id(last_ramp_ms) = 0; return (uint32_t) 0; } float frac = (curr - floor) / denom; if (frac < 0.0f) frac = 0.0f; if (frac > 1.0f) frac = 1.0f; id(last_ramp_ms) = (int) (id(ramp_down_ms) * frac); return (uint32_t) id(last_ramp_ms); - delay: !lambda "return (uint32_t) id(last_ramp_ms);" - light.turn_off: id: bulb_light transition_length: 150ms - delay: 150ms - lambda: |- id(is_ramping) = false; id(ramping_for_off) = false; id(last_brightness_pct) = 0.0f; id(last_set_pos) = 0; id(led_output_set_pct).publish_state(0); - id: max_on_watchdog mode: restart then: - if: condition: lambda: "return id(max_on_hours) > 0;" then: - delay: !lambda "return (uint32_t) (id(max_on_hours) * 3600000UL);" - if: condition: lambda: "return id(bulb_light).current_values.is_on();" then: - lambda: |- id(ramp_switch_target_on) = false; id(bulb_ramp_switch).publish_state(false); - script.stop: ramp_on_script - script.execute: ramp_off_script - id: deferred_restore_brightness mode: restart then: - delay: 5s - lambda: |- float target = id(last_brightness_pct); float minp = (float) id(min_brightness_pct); float maxp = (float) id(max_brightness_pct); if (target > 0.0f && target < minp) target = minp; if (target > maxp) target = maxp; id(restore_target_pct) = target; - if: condition: lambda: "return id(restore_target_pct) > 0.5f;" then: - lambda: |- id(ramp_switch_target_on) = true; id(suppress_slider_sync) = true; - script.stop: ramp_off_script - script.execute: ramp_to_target_script - delay: !lambda "return (uint32_t) id(last_ramp_ms);" - lambda: "id(suppress_slider_sync) = false;" else: - lambda: "id(ramp_switch_target_on) = false;" - light.turn_off: id: bulb_light transition_length: 0s # Jump Fade: ON = jump to (jump% of max cap) then normal fade up to full - id: jump_ramp_on_script mode: restart then: - script.stop: ramp_off_script - if: condition: lambda: |- const auto &cv = id(bulb_light).current_values; float maxp = (float) id(max_brightness_pct); float minp = (float) id(min_brightness_pct); if (maxp <= minp) maxp = minp + 1.0f; float jp = (float) id(jump_fade_pct); if (jp < 0.0f) jp = 0.0f; if (jp > 100.0f) jp = 100.0f; float target_pct = maxp * (jp / 100.0f); if (jp > 0.0f && target_pct < minp) target_pct = minp; if (target_pct > maxp) target_pct = maxp; float curr_pct = cv.is_on() ? (cv.get_brightness() * 100.0f) : 0.0f; return (!cv.is_on()) || (curr_pct + 0.1f < target_pct); then: - light.turn_on: id: bulb_light color_temperature: "${bulb_warm_kelvin}" brightness: !lambda |- float maxp = (float) id(max_brightness_pct); float minp = (float) id(min_brightness_pct); if (maxp <= minp) maxp = minp + 1.0f; float jp = (float) id(jump_fade_pct); if (jp < 0.0f) jp = 0.0f; if (jp > 100.0f) jp = 100.0f; float target_pct = maxp * (jp / 100.0f); if (jp > 0.0f && target_pct < minp) target_pct = minp; if (target_pct > maxp) target_pct = maxp; return target_pct / 100.0f; transition_length: 0s - script.execute: ramp_on_script # Jump Fade: OFF = jump down to (current - current*jump%) then normal fade down to OFF - id: jump_ramp_off_script mode: restart then: - script.stop: ramp_on_script - if: condition: lambda: "return id(bulb_light).current_values.is_on();" then: - light.turn_on: id: bulb_light color_temperature: "${bulb_warm_kelvin}" brightness: !lambda |- const auto &cv = id(bulb_light).current_values; float curr_pct = cv.get_brightness() * 100.0f; float jp = (float) id(jump_fade_pct); if (jp < 0.0f) jp = 0.0f; if (jp > 100.0f) jp = 100.0f; float factor = (100.0f - jp) / 100.0f; float target_pct = curr_pct * factor; float minp = (float) id(min_brightness_pct); float maxp = (float) id(max_brightness_pct); if (maxp <= minp) maxp = minp + 1.0f; if (target_pct < minp) target_pct = minp; if (target_pct > maxp) target_pct = maxp; return target_pct / 100.0f; transition_length: 0s - script.execute: ramp_off_script else: - light.turn_off: id: bulb_light transition_length: 0s