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
- Developer Tools > YAML > Check Configuration.
- 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.