From 2ed35af3cb2bc149dfecb09607e54fe54c4409e8 Mon Sep 17 00:00:00 2001 From: OpenClaw Date: Thu, 21 May 2026 20:54:05 -0500 Subject: [PATCH] DRV-41: fix esphome_fan DESIGNATE_PRESET handler The Composer "Designate Preset" command was broken end-to-end: - The handler read tParams.SPEED but the C4 fan proxy sends tParams.PRESET, so PRESET_SPEED was always nil. - The captured value was never applied; on() ignored it and the user could not later issue "Turn On Fan" at the designated speed. - The value was not persisted, so driver restarts dropped it. - The proxy was never notified, so Navigator/Composer never reflected the designated preset. Fixes: - Read tParams.PRESET, clamp to [1, DISCRETE_LEVELS], persist via lib.persist, and NOTIFY the proxy on every change. - Hydrate PRESET_SPEED in OnDriverLateInit and re-emit the NOTIFY. - Apply the preset in on(): when PRESET_SPEED is set, the ENTITY_COMMAND includes has_speed_level/speed_level so all entry points (RFP.ON, TOGGLE from off, top-button, ON_BINDING DO_CLICK, BUTTON_ACTION) start at the designated speed. - Re-emit the PRESET_SPEED NOTIFY in GET_CURRENT_STATE so Navigator stays in sync when reconnecting. Reported by Dwayne Newsome via finitelabs/control4-esphome#56. --- CHANGELOG.md | 5 ++++ README.md | 5 ++++ drivers/esphome_fan/driver.lua | 47 ++++++++++++++++++++++++++++++---- 3 files changed, 52 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a71d45c..5c48f83 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,11 @@ BTHome boolean sensors) staying as `False` in the Variables Agent even when the underlying state was changing. Variables now serialize as `"0"`/`"1"` matching what Control4 expects. +- Fixed ESPHome fan `Designate Preset` command: the handler now reads the + correct `PRESET` param (was `SPEED`), clamps to the driver's speed count, + persists the value across driver restarts, notifies the proxy so Composer and + Navigator reflect the designated preset, and applies the preset when the fan + is turned on so `Turn On Fan` runs at the designated speed. ## v20260512 - 2026-05-12 diff --git a/README.md b/README.md index d57c732..8f64417 100644 --- a/README.md +++ b/README.md @@ -882,6 +882,11 @@ can file an issue on GitHub: BTHome boolean sensors) staying as `False` in the Variables Agent even when the underlying state was changing. Variables now serialize as `"0"`/`"1"` matching what Control4 expects. +- Fixed ESPHome fan `Designate Preset` command: the handler now reads the + correct `PRESET` param (was `SPEED`), clamps to the driver's speed count, + persists the value across driver restarts, notifies the proxy so Composer and + Navigator reflect the designated preset, and applies the preset when the fan + is turned on so `Turn On Fan` runs at the designated speed. ## v20260512 - 2026-05-12 diff --git a/drivers/esphome_fan/driver.lua b/drivers/esphome_fan/driver.lua index 4ef2c83..ce7d9d6 100644 --- a/drivers/esphome_fan/driver.lua +++ b/drivers/esphome_fan/driver.lua @@ -15,6 +15,7 @@ require("drivers-common-public.global.timer") JSON = require("JSON") local log = require("lib.logging") +local persist = require("lib.persist") local constants = require("constants") @@ -47,6 +48,25 @@ local function getCurrentSpeed() return math.max(1, math.min(DISCRETE_LEVELS, speed_level)) end +--- Clamp a raw preset value into the valid range, or nil if not a positive integer. +--- @param raw any +--- @return integer|nil +local function clampPresetSpeed(raw) + local n = tointeger(raw) + if n == nil or n <= 0 then + return nil + end + return math.max(1, math.min(DISCRETE_LEVELS, n)) +end + +--- Notify the fan proxy of the currently designated preset speed, if any. +local function notifyPresetSpeed() + if PRESET_SPEED == nil then + return + end + SendToProxy(PROXY_BINDING, "PRESET_SPEED", { SPEED = tostring(PRESET_SPEED) }, "NOTIFY") +end + function OnDriverInit() --#ifdef DRIVERCENTRAL require("cloud-client-byte") @@ -76,9 +96,13 @@ function OnDriverLateInit() end end + -- Restore persisted state + PRESET_SPEED = clampPresetSpeed(persist:get("PRESET_SPEED")) + gInitialized = true UpdateProperty("Driver Status", "Disconnected") SendToProxy(PROXY_BINDING, "ONLINE_CHANGED", { STATE = false }, "NOTIFY") + notifyPresetSpeed() SendToProxy(ESPHOME_BINDING, "REFRESH_STATE", {}, "NOTIFY") end @@ -131,11 +155,16 @@ end local function on() log:trace("on()") + local body = { + has_state = true, + state = true, + } + if PRESET_SPEED ~= nil then + body.has_speed_level = true + body.speed_level = PRESET_SPEED + end SendToProxy(ESPHOME_BINDING, "ENTITY_COMMAND", { - body = SerializeSafe({ - has_state = true, - state = true, - }), + body = SerializeSafe(body), }) end @@ -288,8 +317,15 @@ function RFP.DESIGNATE_PRESET(idBinding, strCommand, tParams) if idBinding ~= PROXY_BINDING then return end - PRESET_SPEED = tointeger(Select(tParams, "SPEED")) + local preset = clampPresetSpeed(Select(tParams, "PRESET")) + if preset == nil then + log:warn("DESIGNATE_PRESET ignored: invalid PRESET param %s", tParams) + return + end + PRESET_SPEED = preset + persist:set("PRESET_SPEED", PRESET_SPEED) log:debug("Preset speed set to %s", PRESET_SPEED) + notifyPresetSpeed() end function RFP.GET_CURRENT_STATE(idBinding, strCommand) @@ -311,6 +347,7 @@ function RFP.GET_CURRENT_STATE(idBinding, strCommand) SendToProxy(PROXY_BINDING, "CURRENT_SPEED", { SPEED = tostring(speed) }, "NOTIFY") end SendToProxy(PROXY_BINDING, "DIRECTION", { DIRECTION = direction == 0 and "forward" or "reverse" }, "NOTIFY") + notifyPresetSpeed() end function RFP.UPDATE_DISCONNECT(idBinding, strCommand, tParams, args)