Bathroom fan timer with LED progress indicators using the Zooz ZEN32 scene controller

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. :scream: :disappointed: 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:

  1. 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
  1. 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
  1. 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
  1. 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
  1. 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 :slight_smile: