Z-wave switchLevel handler overrides dimming duration configured on device

The function below is from st.zwave.defaults.switchLevel:

--- Issue a level-set command to the specified device.
---
--- @param driver st.zwave.Driver
--- @param device st.zwave.Device
--- @param command table ST level capability command
function capability_handlers.switch_level_set(driver, device, command)
  local set
  local get
  local delay = constants.MIN_DIMMING_GET_STATUS_DELAY -- delay in seconds
  local level = utils.round(command.args.level)
  level = utils.clamp_value(level, 1, 99)

  if device:is_cc_supported(cc.SWITCH_MULTILEVEL) then
    local dimmingDuration = command.args.rate or constants.DEFAULT_DIMMING_DURATION -- dimming duration in seconds
    -- delay shall be at least 5 sec.
    delay = math.max(dimmingDuration + constants.DEFAULT_POST_DIMMING_DELAY, delay) -- delay in seconds
    get = SwitchMultilevel:Get({})
    set = SwitchMultilevel:Set({ value=level, duration=dimmingDuration })
  elseif device:is_cc_supported(cc.BASIC) then
    get = Basic:Get({})
    set = Basic:Set({ value=level})
  end
  device:send_to_component(set, command.component)
  local query_level = function()
    device:send_to_component(get, command.component)
  end
  device.thread:call_with_delay(delay, query_level)
end

The line below sets dimming duration to either the rate passed by the switchLevel capability or a default of 1 second. However, many dimmers have default dimming rates built in. Those device-specific default dimming rates would be prefereable to defaulting to a 1-second duration since they may be user configurable. Having the handler for switchLevel send a 1-second duration overrides those dimming settings programmed on the device.

local dimmingDuration = command.args.rate or constants.DEFAULT_DIMMING_DURATION -- dimming duration in seconds

To compound matters, there is currently no way for a user to specify that the device’s default dimming duration should be used. The setLevel capability allows a second argument (rate, a non-negative integer) to be passed. However, the ST app currently provides no way through device controls, routines, or scenes to specify what that rate should be.

Even if the rate argument could be passed, the duration parameter for z-wave commands (see here as an example) is set up to expect the string “default” to be passed in order for the device to use its default rate settings. Since setLevel’s rate argument is an integer, there is no way for the user to send “default”.

@nayelyz Could the default handler be changed to use “default” as the dimming duration, instead of 1 second, when rate is not specified in the setLevel command? If a user desired a 1 second dimming duration, they would still be able to specify that using the rate argument (assuming the app is modified to allow that argument to be used).

4 Likes

Hi, @philh30.

Thank you for sharing this, I’ll create a report for the team to look at. In the meantime, are you using a custom handler?

Hi, @philh30!

Just following up on this issue, the team mentioned the fix is included in the v46 of the firmware which was recently released for the Beta group. Are you part of it? If so, can you help us check if it’s solved, please?

@nayelyz I do have the v46 firmware. Unfortunately that firmware seems to have further constrained the amount of memory available to Edge drivers (or perhaps it added to the amount of memory used by each driver?). I’ve found I can only run 11 drivers before the proactive driver reboots kick in. I’m sorry but I simply don’t have room on my hub to spin up a driver to test the new default handler. I’ve had to drop several integrations and get creative with combining others just to keep my hub stable and running what I need in my house.

I hope the team is also looking at the hub memory issues and developing some tools to help users view and manage their hubs better. I’ve certainly noticed an uptick in the number of forum users running into (or blowing past) the memory caps.

I’ve created reports about that for users that have memory issues active because that’s where the team can get data from. Unfortunately, I don’t have more information but thanks for letting us know about your experience, I’ll add this as a note to the report on this issue.

The z-wave specs shows that value 0xFF is a valid duration value for the Switch Multilevel Set command so why does the platform change it to 240?

I was hoping that might be a workaround to the original issues…

Hi, @krlaframboise
Are you using the libraries from v46? The constant used for the dimming duration was modified and also the default libraries were modified accordingly.

I’m using the latest version available and I’m aware of those other changes because they broke some of my tests, but this is something different.

If you look at those screenshots you’ll see that the test is written to expect a duration of 0xFF/255, but the platform is altering the test and expecting 240 instead. It’s not just the test because if you send a duration of 255 to a device it also gets changed to 240 before it’s sent.

I was just pointing out that 255/0xFF is a valid duration value according to the command class specs and wondering why the platform is changing it…

@nayelyz Just FYI, the Z-Wave libraries are still doing this. If I send the duration as 255, it gets converted to 240. If I try to not include a duration, it sends the duration as 0. There doesn’t seem to be a way to use the device’s default dimming duration.

device:send(SwitchMultilevel:Set({ value=level }))

2025-08-28T21:40:36.372875336Z INFO Z-Wave Switch <ZwaveDevice: 17ddcb96-c669-44d0-aec2-3f7460a80422 [52] (Inovelli mmWave Red Series 1)> sending Z-Wave command: {args={duration=0, value=44}, cmd_class=“SWITCH_MULTILEVEL”, cmd_id=“SET”, dst_channels={}, encap=“AUTO”, payload=“\x2C\x00”, src_channel=0, version=2}

device:send(SwitchMultilevel:Set({ value=level, duration=255 }))

2025-08-28T21:42:59.258045020Z INFO Z-Wave Switch <ZwaveDevice: 17ddcb96-c669-44d0-aec2-3f7460a80422 [52] (Inovelli mmWave Red Series 1)> sending Z-Wave command: {args={duration=240, value=77}, cmd_class=“SWITCH_MULTILEVEL”, cmd_id=“SET”, dst_channels={}, encap=“AUTO”, payload=“\x4D\x83”, src_channel=0, version=2}

Try this

device:send(SwitchMultilevel:Set({ value=level, duration="default" }))

1 Like

@nayelyz I think the bug is buf.lua here (which is called by SwitchMultilevel:Set and many other command classes)

function Writer:write_actuator_duration_set(duration)
  if duration == "default" then
    duration = 0xFF
  elseif duration > 127 then
    duration = utils.round(duration * (1.0 / 60)) -- minutes encoding
    duration = duration + 127
  else
    duration = utils.round(duration) -- seconds encoding
  end
  self:write_u8(duration)
end

Note sure why SwitchMultilevel:Set is calling write_actuator_duration_set in the first place. Whatever the duration specified will follow the specs, if it’s > 127 then it’s specified in minutes, I don’t see what’s the point of the code above other than to try and convert it to minutes but then you’re creating confusion in the API’s. Are the API’s following the Z-Wave specs (in which case it should pass the value on without processing and assume the developers are following the specs) or if you’re trying to get the developers to pass the duration in minutes, that’s not clear and it seems to violate the principles of the Z-Wave API classes (which are basically an implementation of the Z-Wave specs and not a custom developer API)

2 Likes

Well that works, thanks!

2025-08-29T04:07:56.733061252Z INFO Z-Wave Switch <ZwaveDevice: ab09663b-325a-428c-9b34-29b51810994d [56] (Inovelli mmWave Dimmer Red Series)> sending Z-Wave command: {args={duration=“default”, value=17}, cmd_class=“SWITCH_MULTILEVEL”, cmd_id=“SET”, dst_channels={}, encap=“AUTO”, payload=“\x11\xFF”, src_channel=0, version=2}

I also don’t understand what the write_actuator is trying to accomplish. It seems like the UI could ask the user what duration they would like to use (0-127 seconds and 1-127 minutes). Then, if the user chose minutes, the function could add their selection to 127 (127 + minutes). Obviously a few different ways it could be done and maybe I am missing something with the acturator . . . it just seems strange.

Hi, @RBoy , @erocm1231

I asked the engineering team, and they mentioned the library is following the spec:

The “default” option was created to be able to use what the device had configured by default per the report from the first post here.

So, this means it’s currently working as expected but I already shared your improvements comments with the engineering team. Thanks!

1 Like

Ah ok - I see what they mean.

I guess the improvement would be to modify this line

if duration == "default" or duration == 0xFF then

To my eyes the duration is being interpreted in seconds in the API and theWriter:write_actuator_duration_set(duration)function pasted earlier is converting that to Z-Wave specs. So 255 or 0xFF ends up as 4 minutes (240 seconds when converted back from Z-Wave format) and can’t reasonably be treated differently.

I would hope that duration > 7620 (or maybe 7649 to allow for rounding down) is being dealt with somewhere before it hits that function.