We recently remodeled our kids bathroom. I already have many zwave devices in the home, mostly from Zooz, and I wanted to replace our existing Home Depot fan timer with a Zooz ZEN32. I know HASS has a first-party timer helper, so this should all be fairly straightforward, right?
Narrator: It wasn’t.
My requirements:
- The big button (relay) operates as a traditional on/off switch for the fan. When pressed, it should also disable any timers, regardless of state.
- LEDs for all buttons should remain on at all times. Default to white color for each while inactive. Turn the color to green when active.
- Anytime the fan is running (by manual control or automation), indicate this by changing the relay button’s LED to green.
- The four scene control buttons should each run the bath fan for a pre-defined amount of time: 5, 10, 15, or 30 minutes.
- When a scene control button is pressed, its LED color should change to green, letting the user know how long the fan will be running.
- As the fan timer counts down, LEDs of the shorter timer buttons should update to let the user know roughly how much longer the fan will run. (Example below)
- A button press should immediately override an existing timer.
- When the timer finishes and the fan turns off, all LEDs should be white again.
Example:
- When the 15m button is pressed, its LED should change to green and the fan turns on. Relay LED should also be green because the fan is now on.
- When there’s 10 minutes left on this timer, turn the 10m LED green and set the 15m LED back to white.
- When there’s 5 minutes left, turn the 5m LED green and set the 10m LED back to white.
- When the timer finishes, turn off the fan and set all LEDs back to white.
As I started playing with the timer helper, I was informed of a critical limitation: the timer’s remaining
attribute is only updated when the timer is paused, canceled, or finishes. This is gonna get complicated.
This new limitation calls for some technical updates to the requirements:
- No time delays in automations. This ensures reliability when someone inevitably presses multiple scene buttons in rapid succession.
- All automations must be triggered by events or state changes. Daisy-chaining automations together can introduce race conditions, which leads to inconsistent behavior and is difficult to troubleshoot (among other things).
- The solution must survive a HASS restart. Tick that “Restore?” checkbox in the
timer
helper config!
I noodled on the design for awhile and tinkered with some ideas, eventually landing on what you see below. Since my desired timer presets are all divisible by 5, I decided the best approach was to exclusively use 5-minute timers and track the number of cycles remaining using a counter
helper. My solution uses the following five automations:
- Kids Bath Fan Scene Buttons Handler
alias: Kids Bath Fan Scene Buttons Handler
description: >-
Turns on the bath fan and sets a 5, 10, 15, or 30 minute timer, depending on
which button was pressed.
trigger:
- platform: event
event_type: zwave_js_value_notification
event_data:
node_id: 77
label: Scene 001
value: KeyPressed
id: 5m
- platform: event
event_type: zwave_js_value_notification
event_data:
node_id: 77
label: Scene 002
value: KeyPressed
id: 10m
- platform: event
event_type: zwave_js_value_notification
event_data:
node_id: 77
label: Scene 003
value: KeyPressed
id: 15m
- platform: event
event_type: zwave_js_value_notification
event_data:
node_id: 77
label: Scene 004
value: KeyPressed
id: 30m
condition: []
action:
- service: switch.turn_on
metadata: {}
data: {}
target:
entity_id: switch.kids_bath_fan_controller_zsc02
enabled: true
- choose:
- conditions:
- condition: trigger
id:
- 5m
sequence:
- service: counter.set_value
target:
entity_id: counter.kids_bath_fan_timer_cycles_remaining
data:
value: 1
enabled: true
- conditions:
- condition: trigger
id:
- 10m
sequence:
- service: counter.set_value
target:
entity_id: counter.kids_bath_fan_timer_cycles_remaining
data:
value: 2
enabled: true
- conditions:
- condition: trigger
id:
- 15m
sequence:
- service: counter.set_value
target:
entity_id: counter.kids_bath_fan_timer_cycles_remaining
data:
value: 3
enabled: true
- conditions:
- condition: trigger
id:
- 30m
sequence:
- service: counter.set_value
target:
entity_id: counter.kids_bath_fan_timer_cycles_remaining
data:
value: 6
enabled: true
mode: single
- Kids Bath Fan Timer: Iterate Cycles
I should probably add some if/then conditionals to the actions here so we only update the zwave parameters that actually need updating. A task for another day, perhaps.
alias: "Kids Bath Fan Timer: Iterate Cycles"
description: >-
Iterates through the specified number of cycles to keep the kids bath fan
timer running for the desired amount of time.
trigger:
- platform: state
entity_id:
- counter.kids_bath_fan_timer_cycles_remaining
to: null
enabled: true
condition: []
action:
- choose:
- conditions:
- condition: template
value_template: >-
{{ states.counter.kids_bath_fan_timer_cycles_remaining.state | int
== 1 }}
sequence:
- service: timer.start
target:
entity_id:
- timer.kids_bath_fan_timer
data:
duration: "00:05:00"
- service: zwave_js.set_config_parameter
metadata: {}
data:
parameter: "7"
value: "2"
endpoint: "0"
target:
device_id: 7ae2389f85f7fd07ac99547c324a3ff8
- service: zwave_js.set_config_parameter
metadata: {}
data:
parameter: "8"
value: "0"
endpoint: "0"
target:
device_id: 7ae2389f85f7fd07ac99547c324a3ff8
- service: zwave_js.set_config_parameter
metadata: {}
data:
parameter: "9"
value: "0"
endpoint: "0"
target:
device_id: 7ae2389f85f7fd07ac99547c324a3ff8
- service: zwave_js.set_config_parameter
metadata: {}
data:
parameter: "10"
value: "0"
endpoint: "0"
target:
device_id: 7ae2389f85f7fd07ac99547c324a3ff8
- conditions:
- condition: template
value_template: >-
{{ states.counter.kids_bath_fan_timer_cycles_remaining.state | int
== 2 }}
sequence:
- service: timer.start
target:
entity_id:
- timer.kids_bath_fan_timer
data:
duration: "00:05:00"
- service: zwave_js.set_config_parameter
metadata: {}
data:
parameter: "7"
value: "0"
endpoint: "0"
target:
device_id: 7ae2389f85f7fd07ac99547c324a3ff8
- service: zwave_js.set_config_parameter
metadata: {}
data:
parameter: "8"
value: "2"
endpoint: "0"
target:
device_id: 7ae2389f85f7fd07ac99547c324a3ff8
- service: zwave_js.set_config_parameter
metadata: {}
data:
parameter: "9"
value: "0"
endpoint: "0"
target:
device_id: 7ae2389f85f7fd07ac99547c324a3ff8
- service: zwave_js.set_config_parameter
metadata: {}
data:
parameter: "10"
value: "0"
endpoint: "0"
target:
device_id: 7ae2389f85f7fd07ac99547c324a3ff8
- conditions:
- condition: template
value_template: >-
{{ states.counter.kids_bath_fan_timer_cycles_remaining.state | int
== 3 }}
sequence:
- service: timer.start
target:
entity_id:
- timer.kids_bath_fan_timer
data:
duration: "00:05:00"
- service: zwave_js.set_config_parameter
metadata: {}
data:
parameter: "7"
value: "0"
endpoint: "0"
target:
device_id: 7ae2389f85f7fd07ac99547c324a3ff8
- service: zwave_js.set_config_parameter
metadata: {}
data:
parameter: "8"
value: "0"
endpoint: "0"
target:
device_id: 7ae2389f85f7fd07ac99547c324a3ff8
- service: zwave_js.set_config_parameter
metadata: {}
data:
parameter: "9"
value: "2"
endpoint: "0"
target:
device_id: 7ae2389f85f7fd07ac99547c324a3ff8
- service: zwave_js.set_config_parameter
metadata: {}
data:
parameter: "10"
value: "0"
endpoint: "0"
target:
device_id: 7ae2389f85f7fd07ac99547c324a3ff8
- conditions:
- condition: template
value_template: >-
{{ states.counter.kids_bath_fan_timer_cycles_remaining.state | int
== 6 }}
sequence:
- service: timer.start
target:
entity_id:
- timer.kids_bath_fan_timer
data:
duration: "00:05:00"
- service: zwave_js.set_config_parameter
metadata: {}
data:
parameter: "7"
value: "0"
endpoint: "0"
target:
device_id: 7ae2389f85f7fd07ac99547c324a3ff8
- service: zwave_js.set_config_parameter
metadata: {}
data:
parameter: "8"
value: "0"
endpoint: "0"
target:
device_id: 7ae2389f85f7fd07ac99547c324a3ff8
- service: zwave_js.set_config_parameter
metadata: {}
data:
parameter: "9"
value: "0"
endpoint: "0"
target:
device_id: 7ae2389f85f7fd07ac99547c324a3ff8
- service: zwave_js.set_config_parameter
metadata: {}
data:
parameter: "10"
value: "2"
endpoint: "0"
target:
device_id: 7ae2389f85f7fd07ac99547c324a3ff8
- conditions:
- condition: template
value_template: >-
{{ states.counter.kids_bath_fan_timer_cycles_remaining.state | int
== 0 }}
sequence:
- service: zwave_js.set_config_parameter
metadata: {}
data:
parameter: "7"
value: "0"
endpoint: "0"
target:
device_id: 7ae2389f85f7fd07ac99547c324a3ff8
- service: zwave_js.set_config_parameter
metadata: {}
data:
parameter: "8"
value: "0"
endpoint: "0"
target:
device_id: 7ae2389f85f7fd07ac99547c324a3ff8
enabled: true
- service: zwave_js.set_config_parameter
metadata: {}
data:
parameter: "9"
value: "0"
endpoint: "0"
target:
device_id: 7ae2389f85f7fd07ac99547c324a3ff8
enabled: true
- service: zwave_js.set_config_parameter
metadata: {}
data:
parameter: "10"
value: "0"
endpoint: "0"
target:
device_id: 7ae2389f85f7fd07ac99547c324a3ff8
enabled: true
- delay:
hours: 0
minutes: 0
seconds: 0
milliseconds: 300
- service: switch.turn_off
metadata: {}
data: {}
target:
entity_id: switch.kids_bath_fan_controller_zsc02
- conditions:
- condition: template
value_template: >-
{{ states.counter.kids_bath_fan_timer_cycles_remaining.state | int
in (4,5) }}
sequence:
- service: timer.start
target:
entity_id:
- timer.kids_bath_fan_timer
data:
duration: "00:05:00"
mode: single
- Kids Bath Fan Timer: Reset
alias: "Kids Bath Fan Timer: Reset"
description: Decrements the bath fan cycle counter when the 5-minute timer completes.
trigger:
- platform: event
event_type: timer.finished
event_data:
entity_id: timer.kids_bath_fan_timer
condition: []
action:
- service: counter.decrement
metadata: {}
data: {}
target:
entity_id: counter.kids_bath_fan_timer_cycles_remaining
mode: single
- Kids Bath Fan: Relay Button LED
alias: "Kids Bath Fan: Relay Button LED"
description: Adjusts the LED color of the relay button based on the fan's state.
trigger:
- platform: state
entity_id:
- switch.kids_bath_fan_controller_zsc02
from: "on"
to: "off"
id: Fan Off
- platform: state
entity_id:
- switch.kids_bath_fan_controller_zsc02
from: "off"
to: "on"
id: Fan On
condition: []
action:
- choose:
- conditions:
- condition: trigger
id:
- Fan On
sequence:
- service: zwave_js.set_config_parameter
target:
device_id: 7ae2389f85f7fd07ac99547c324a3ff8
data:
endpoint: "0"
parameter: "6"
value: "2"
- conditions:
- condition: trigger
id:
- Fan Off
sequence:
- service: zwave_js.set_config_parameter
target:
device_id: 7ae2389f85f7fd07ac99547c324a3ff8
data:
endpoint: "0"
parameter: "6"
value: "0"
mode: single
- Kids Bath Fan: Relay Toggle
alias: "Kids Bath Fan: Relay Toggle"
description: >-
Clears any fan timers and resets LED colors when the fan is manually turned on
or off.
trigger:
- platform: event
event_type: zwave_js_value_notification
event_data:
node_id: 77
label: Scene 005
value: KeyPressed
- platform: event
event_type: zwave_js_value_notification
event_data:
node_id: 77
label: Scene 005
value: KeyPressed2x
- platform: event
event_type: zwave_js_value_notification
event_data:
node_id: 77
label: Scene 005
value: KeyPressed3x
- platform: state
entity_id:
- switch.kids_bath_fan_controller_zsc02
from: "on"
to: "off"
condition: []
action:
- if:
- condition: state
entity_id: timer.kids_bath_fan_timer
state: active
then:
- service: timer.cancel
metadata: {}
data: {}
target:
entity_id: timer.kids_bath_fan_timer
- if:
- condition: template
value_template: >-
{{ states.counter.kids_bath_fan_timer_cycles_remaining.state | int !=
0 }}
then:
- service: counter.set_value
metadata: {}
data:
value: 0
target:
entity_id: counter.kids_bath_fan_timer_cycles_remaining
mode: single
And here are my ZEN32 config parameters:
- p1,2,3,4,5 (LED indicator) = Always On
- p6,7,8,9,10 (LED color) = White
- p23 (LED settings indicator) = Disable. (Enabling this creates a cool wavy animation with the scene button LEDs whenever they are updated. Was fun for a few days but I eventually grew tired of it.)
Whew! Not nearly as simple as expected, but it works reliably and really shouldn’t require maintenance. Hopefully this is helpful in some way for others. Happy config-ing