Leviton ZW6HD + Home Assistant: "Off" command overwrites last-known dim level (turns on to 1%)

I have 67 ZW6HD dimmers running in Home Assistant via Z-Wave, and they work well overall. However, there is a consistent reproducible bug (or configuration issue) I cannot resolve.

Steps to reproduce:

  1. Set any ZW6HD dimmer to any level (20%, 50%, 80%, 100%) via Home Assistant.
  2. Turn the switch off from within Home Assistant (not manually at the wall).
  3. Walk to the wall switch and turn it back on manually.

Result: The switch comes on at 1% regardless of what the prior dim level was.

Expected result: The switch should restore to the last active dim level, exactly as it does when you turn it off and on manually at the wall.

What works correctly:

If I set the dimmer to any level (e.g., 55%) and turn it off manually at the wall, then turn it back on, it correctly restores to 55%. This behavior works every time.

Root cause hypothesis:

I believe the Z-Wave implementation in Home Assistant is sending a “dim to 0%” command rather than a true Binary Switch Off. This overwrites the switch’s last-known level memory with ~0%, so when the switch is powered back on it restores to that near-zero value instead of the previous setting.

A proper off command should toggle the relay state without touching the dim level register.

Environment:

  • Switch: Leviton ZW6HD (x67)
  • Integration: Home Assistant Z-Wave (JS or ZWave2MQTT, whichever applies)
  • Behavior: 100% reproducible

Has anyone seen this and found a fix, whether a parameter setting on the switch, a Z-Wave command class configuration in HA, or something else?

The light entity in HA corresponds to Command Class Multilevel Switch, not Binary Switch. In order to turn off a Z-Wave dimmer switch you do send a Multilevel Switch Set command with the value 0% (which is also what HA is doing).

None of my Jasco nor Zooz dimmers behave the way you are describing.

To confirm the behavior yourself, turn on debug logging in the integration page, turn the light off in HA and watch the Websocket messages HA sends to the driver, and then view the subsequent debug logs from the driver sending the command to the device. Then you could additionally turn on the light and see what it reports.

Thanks for the detailed response and the spec reference. I followed your suggestion exactly and captured Z-Wave JS debug and Silly level logs. Here is what I found.

HA behavior confirmed

You are correct. I read the HA source code at the link you provided. async_turn_off calls _async_set_brightness(0) which sends MultilevelSwitchCCSet(0). I verified this in my logs. The command is spec-compliant per Table 2.403.

What the logs show

I ran a controlled sequence covering all combinations of HA vs wall for both on and off. Summary:

Off method On method Prior level Restored to
Wall Wall 48 48 (correct)
HA (0x00) HA (0xFF) 48 48 (correct)
HA (0x00) Wall paddle 50 1 (wrong)

The only failing combination is HA off followed by physical paddle on.

What the Silly log adds

When HA sends the off command, Z-Wave JS logs: calling SET_VALUE API MultilevelSwitchCCAPI: property: targetValue, optimistic: true. This is the only behavioral difference between an HA off and a wall off at the Z-Wave level. The wall off produces no outbound command at all. The switch simply reports its own state changing via unsolicited MultilevelSwitchCCReport frames with targetValue: 0.

So the switch receives two different signals for “off”: an explicit MultilevelSwitchCCSet(0) from HA, and nothing from a wall press (it just self-reports). The restore behavior differs depending on which path was used.

Per the spec

Table 2.403 designates 0x00 as a state control command indicating Off, not a level assignment. The switch should preserve its last non-zero level regardless of how it received the off signal. When turned back on via the physical paddle after receiving MultilevelSwitchCCSet(0), my ZW6HD reports currentValue: 1, targetValue: 1 every time. When turned back on via HA (0xFF) after the same command, it correctly restores to the prior level.

Device details

  • Switch: Leviton ZW6HD (x67 units)
  • Firmware: 1.1.0.9 (latest available, released September 2025)
  • Configuration Parameter 5 (Initial Dim Level): set to “Last dim level” (default)
  • ZW6HD does not advertise Basic CC or Binary Switch CC, only Multilevel Switch CC

Question for the community

The spec is clear that 0x00 MUST be treated as a state control command, not a level assignment. The switch correctly honors this when HA sends 0xFF to turn back on, restoring the prior level. But it does not honor it when the physical paddle is used to turn back on after receiving 0x00 from Z-Wave JS.

This leaves an open question: is there a way for Z-Wave JS or HA to send the off command in a way that the switch’s internal paddle restore path treats identically to a local wall off? Other manufacturers’ dimmers apparently do not exhibit this split behavior. Has anyone seen this specific issue with Leviton dimmers, and is there a known workaround at the Z-Wave JS or HA configuration level?

I have only one Leviton ZW6HD, which I’ve bought out of curiosity. All my other wall switches are Zooz. They offer many options and work well. The Leviton ZW6WD one offers just few options and my unit never returns to its previous dim level. I use mostly its paddle and, after turning it on, it always goes back to 99%.

Hmm…. all 67 of my units from paddle always go back to their previous DIM level.
That maybe could indicate that your configuration is set to go that level VS previous setting.

I wonder what I'm missing. Here's my configuration.

try this test: (all from switch, nothing from HA)

  1. Turn on from switch
  2. Set dim to 50%
  3. Turn off
  4. Turn on

Also reminder: if you double tap to turn on, it will always go back to 100%.
Single Tap takes to last level
Double Tap overrides and goes to 100%

Works in reverse too, where you double tap off, and it is instant off VS slowly turning it off.

Checking in with any Gurus here :slight_smile: ...
Any way we can solve this?
Wondering if HA is sending off in a different manner than how you turn it off manually?
OR do we need to ask Leviton to update the firmware?

If latter, can anyone help with that?

You could create a workaround with an automation and some templates.

First setup a triggered template to capture the dim value. Maybe ignore it going to 1. Probably triggered on the light entity dim attribute.

Create an automation that detects the switch being turn on from the wall (hint listen to the zwave scene control events) and adjust the brightness to the value stored in the template.

Thanks Pete, yes that was along the lines I was thinking. Can be done.
Just was trying to find a more holistic approach via HA or Firmware reporting to Leviton (if its on their side).

But I'll work on that template and triggers to see if that can be done. So many edge cases come to mind.

It appears that HA / zwavejs is doing the right thing. You could certainly verify this by asking a question on zwavejs Sign in to GitHub · GitHub - if they verify its a device issue then at least its documented on the zwavejs site for other to find. If it's a device issue, you may want to report it to Leviton, let them know you have hundreds of devices and point them to that discussion that shows it's a bug. If that report makes it to their product manager they may device to fix it.

Thanks, yes.
With 67 devices, I tried that solution (recording state and then remembering) before coming to this thread (pretty good with scripting) and it works, but adds way too much zwave traffic. And then there are edge cases. I know I can work through those edge cases.

But yes, I'll go to zwavejs github to report this over there too.

it would add one command when the scene event gets delivered and the automation set the dim level, and that only happen when a person turns the light on manually

hahah, you'd think :slight_smile: ... I've got a few more people in my home, and lights turn on off / get dimmed all the time :slight_smile:

Arg... My wife just raised this to me yday... I was setting up scenes and activation in 4-button switches just for testing, but only on a couple of circuits and she was reporting it in most locations. She complained that despite loving everything else, she hates this behavior. I didn't believe her at first , showed her several locations worked fine. Then realized my kids have been issuing Alexa commands frequently for fun, especially in the common areas like the kitchen and one of her odd complaints was master closet which i had just replaced and toggled on / off with Home Assistant to test it.

I'm in the middle of replacing my ViziaRf (original not +) setup from 2008 with the Leviton 800s. I've got 11 of ~30 dimmers done so far. I like consistency and i like the look/layout of the Levitons. So i really don't want to abandon and i really don't want to script it (not just the work, but wasted traffic).

Of particular note, the Vizia devices do not behave like this. In fact, if you watch the dimmer LEDS, as soon as gets to 0/OFF, the LED strip immediately goes back to last dim level (ViziaRF have dimmer level always on). Was hoping if i set LED strip to "always on" like the ViziaRf devices, it might magically fix, but it does not. Level still hits 0, then stays @ :frowning:

Guessing no progress with Leviton in reporting this / getting a firmware revision as a bug fix? I just filed a ticket via email. Really hope the person who reads it gets the problem and pushes forth a firmware fix.

Hey all, so if you are still hitting the annoyance where turning this specific model dimmer ZW6HD off from Home Assistant and then back on at the wall paddle brings it up at ~1% instead of your last dim level. I poked at it for a while but could not fully pin down what the firmware is doing inside. Rather than keep chasing that, I wrote a Home Assistant fix that sidesteps the whole thing and gives me the behavior I actually want: no matter where you turn the dimmer off from, turning it back on returns it to its last dim level. Sharing the whole thing below.

What I think is happening (and I genuinely cannot be sure)

Fair warning: this section is my best guess from watching the behavior, not something I confirmed against Leviton's internals. I found nothing in the manual or the spec sheet that explains the restore mechanism, so treat the "why" as a theory.

Here is the part I am sure of, because I tested it repeatedly:

  • Off at the wall, on at the wall: comes back to the last level. Fine.
  • Off from HA, on from HA (no brightness): comes back to the last level, e.g. set it to 50%, off from HA, on from HA, back to 50%. Fine.
  • Off from HA, on at the wall paddle: comes up at ~1% (HA brightness 3). This is the bug.
  • Set the level at the paddle, then off from HA, then on at the paddle: still ~1%. So an HA off seems to be what wrecks it, when turning back on at paddle only, not from HA, regardless of what the level was.

The confusing part is cases two and three together. The same HA off leaves the HA restore working but breaks the paddle restore. So whatever holds the "last level" is not shared between those two paths after an HA off.

What I cannot tell from the outside is which of these it is:

  • The dimmer keeps two separate last-level memories inside it, one used when HA asks it to restore and one used by the paddle, and an HA off corrupts only the paddle's.
  • Or the paddle has one memory and the value HA restores to is held separately (somewhere on the device's controller-restore path, or conceivably on the HA side), and again only the paddle's side is the one an HA off corrupts.

Both produce exactly what I see, and I cannot separate them without firmware-level detail I do not have. So I am not claiming either as fact.

I did do another direct test too. I sent a raw, spec-correct Multilevel Switch Set 0 straight to the node with zwave_js.set_value and got the identical result, so makes me lean that it is firmware fault, but cannot be sure, as I haven’t looked at how exactly zwavejs is implementing it. There is also no Central Scene on this device, so there is no "paddle pressed" event to hook. The only signal HA gets is the light coming on at the junk value, and that is what my fix keys on.

Bottom line: I could not prove the exact cause, it smells like firmware, and instead of trying to nail it down I just wrote a work-around that gives me back the behavior I want.

The fix

A Home Assistant package with:

  • one input_number helper per dimmer that remembers its last healthy brightness,
  • a capture automation that updates that helper whenever a dimmer is on at a real level,
  • a restore automation that, when a dimmer comes on at the junk value, re-applies the stored level.

It is reactive, so you will see the light sit at ~1% for a millisecond (hardly noticiable, unless you pay super attention) before it jumps to the right level. The device physically lands at 1% first and HA can only react after that, so the brief blink is unavoidable. Double-tap to full is untouched, since that goes straight to 100% and never hits the junk band.

1. Enable packages (skip if you already have this)

In configuration.yaml:

homeassistant:
  packages: !include_dir_named packages

That loads every file in /homeassistant/packages/ as a package.

2. The package file

Create /homeassistant/packages/zw6hd_dim_restore.yaml:

# =============================================================================
# Leviton ZW6HD - restore last dim level after the paddle "comes on at 1%" bug
# packages/zw6hd_dim_restore.yaml
# =============================================================================
# The behavior: turn a ZW6HD off from Home Assistant, then on at the wall
# paddle, and it comes up at the junk value (Z-Wave level 1 = HA brightness 3,
# about 1%) instead of your last level. The exact firmware cause is unclear,
# but a raw, spec-correct Multilevel Switch Set 0 corrupts it the same way
# light.turn_off does, and no config parameter fixes it. So this is a
# work-around, not a root-cause fix.
#
# WHAT IT DOES
#   CAPTURE  - whenever a dimmer is on at a real level, store it per device.
#   RESTORE  - when a dimmer comes on at the junk value, re-apply that level.
#
# NUMBERS
#   Junk value          = HA brightness 3    (Z-Wave level 1)
#   Min Dim Level floor = HA brightness ~26  (Param 3 default 10)
#   Threshold           = 6  -> above the junk, below the floor.
#
# DOUBLE-TAP TO FULL is safe: it goes straight to 255 and never touches the
# junk band, so the restore never sees it.
#
# HELPERS have no "initial:" on purpose, so each one survives restarts. Before
# a helper is ever seeded it reads 0, and the restore falls back to full (255).
#
# SCALE IT: list YOUR ZW6HD light entities in BOTH automations, and create one
# helper per dimmer named  zw6hd_last_brightness_<that light's object id>.
# =============================================================================

input_number:
  # One per dimmer. Name MUST be zw6hd_last_brightness_<your light's object id>.
  # Example: light.kitchen -> zw6hd_last_brightness_kitchen
  zw6hd_last_brightness_kitchen:
    name: ZW6HD Last Brightness - kitchen
    min: 0
    max: 255
    step: 1
  zw6hd_last_brightness_living_room:
    name: ZW6HD Last Brightness - living_room
    min: 0
    max: 255
    step: 1
  zw6hd_last_brightness_hallway:
    name: ZW6HD Last Brightness - hallway
    min: 0
    max: 255
    step: 1

automation:
  # CAPTURE: store the last healthy brightness per dimmer. This never commands a
  # light, it only writes a helper, so it cannot interfere with your other
  # automations.
  - id: zw6hd_capture_last_brightness
    alias: ZW6HD Capture Last Brightness
    mode: parallel
    max: 40
    triggers:
      - trigger: state
        entity_id:
          # >>> LIST YOUR ZW6HD LIGHTS HERE <<<
          - light.kitchen
          - light.living_room
          - light.hallway
    conditions:
      - condition: template
        value_template: >-
          {{ trigger.to_state is not none
             and trigger.to_state.state == 'on'
             and trigger.to_state.attributes.brightness is not none
             and (trigger.to_state.attributes.brightness | int(0)) > 6 }}
    actions:
      - action: input_number.set_value
        target:
          entity_id: >-
            input_number.zw6hd_last_brightness_{{ trigger.entity_id.split('.')[1] }}
        data:
          value: "{{ trigger.to_state.attributes.brightness | int(0) }}"

  # RESTORE: when a dimmer comes on at the junk value, re-apply its stored level.
  # Requires a real off -> on into the junk band (<= 6), so it never fires on a
  # fade-up to a normal level or on a double-tap to full. Falls back to 255 if
  # the helper was never seeded.
  - id: zw6hd_restore_after_paddle
    alias: ZW6HD Restore After Paddle Bug
    mode: parallel
    max: 40
    triggers:
      - trigger: state
        entity_id:
          # >>> SAME LIST AS ABOVE <<<
          - light.kitchen
          - light.living_room
          - light.hallway
    conditions:
      - condition: template
        value_template: >-
          {{ trigger.from_state is not none
             and trigger.to_state is not none
             and trigger.from_state.state == 'off'
             and trigger.to_state.state == 'on'
             and trigger.to_state.attributes.brightness is not none
             and (trigger.to_state.attributes.brightness | int(0)) <= 6 }}
    actions:
      - action: light.turn_on
        target:
          entity_id: "{{ trigger.entity_id }}"
        data:
          brightness: >-
            {% set v = states('input_number.zw6hd_last_brightness_' ~ trigger.entity_id.split('.')[1]) | int(0) %}
            {{ v if v > 6 else 255 }}

3. Make it yours

  • Replace the example light.kitchen / light.living_room / light.hallway entries in both automations with your own ZW6HD light entities.
  • Create one helper per dimmer, named zw6hd_last_brightness_<that light's object id>. So light.kitchen needs zw6hd_last_brightness_kitchen, light.master_bedroom needs zw6hd_last_brightness_master_bedroom, and so on. The names must line up, that is how one pair of rules serves every dimmer.
  • Add as many dimmers as you have, just keep the trigger lists and the helpers in sync.

Quick way to list your ZW6HDs by model (Developer Tools > Template):

{% set ns = namespace(rows=[]) %}
{% for s in states.light %}
  {% set did = device_id(s.entity_id) %}
  {% set model = device_attr(did, 'model') if did else none %}
  {% if model and 'ZW6HD' in model %}
    {% set ns.rows = ns.rows + [s.name ~ '  |  ' ~ s.entity_id] %}
  {% endif %}
{% endfor %}
Count: {{ ns.rows | count }}
{% for r in ns.rows %}
- {{ r }}
{% endfor %}

4. Deploy

  1. Developer Tools > YAML > Check Configuration.
  2. Restart Home Assistant.

5. Test

Set a dimmer to 50% from HA, turn it off from HA, then turn it on at the wall paddle. It should land back at ~50% instead of 1%.

Notes

  • The helpers deliberately have no initial:, so they survive restarts. Before a dimmer's helper is ever seeded it reads 0, and the restore falls back to full brightness rather than the junk value.
  • My junk value was HA brightness 3 (Z-Wave level 1) and the threshold is 6. Confirm yours (off from HA, tap on at the wall, check the brightness attribute) and adjust the 6 if your device differs.
  • The capture/restore rules are mode: parallel, max: 40 so a scene that flips many dimmers at once does not drop any of them.

Not claiming this is the "right" fix or that I understand the firmware. It just works and gives me the behavior I wanted. Hope it saves someone else the headache.

Also hoping that someone at LEVITON and ZWAVEJS can see this, and figure out if this is a bug in firmware or implementation, since other devices don’t have this bug.