blueprint: name: Z2M - Philips Hue Tap Dial Switch - Light Control (MQTT Direct) description: > Control up to 4 lights and run custom actions with a Philips Hue Tap Dial Switch via Zigbee2MQTT. **Why use this version?** Zigbee2MQTT 2.0+ deprecated and removed the legacy `action` sensor. This blueprint connects directly to the MQTT topic, meaning it works flawlessly with the newest Z2M updates without needing legacy workarounds! **Features:** - Assign a Light to each button to Toggle it. - Set Custom Actions for Single, Double, and Long Presses on all 4 buttons. - Automatically updates an `input_text` helper to remember the last used light. - Rotating the dial dims the most recently interacted light smoothly. - **No Dropped Clicks:** Uses `mode: parallel` and timestamping so you can press button 1, 2, 3 in rapid succession and *all* of them will register instantly without being blocked by double-click delays. - **Native MQTT:** Connects directly to the MQTT topic (bypassing legacy Z2M action sensors). - **Fallback Light**: A default light to dim if the dial is turned before any buttons are pressed. **REQUIREMENT:** Create an `input_text` helper in Home Assistant (Settings > Devices & Services > Helpers) and select it below in the "Last Light Helper" input. domain: automation input: mqtt_topic: name: MQTT Topic description: "The base MQTT topic of the Tap Dial. Usually `zigbee2mqtt/Friendly_Name` (e.g., `zigbee2mqtt/Living Room Dial`). You can find this in Z2M under the device's 'MQTT Info' tab." default: "zigbee2mqtt/Hue Tap Dial" helper_last_light: name: Last Light Helper (input_text) description: "Required. An input_text helper to remember the last activated light for the dial to dim." selector: entity: domain: input_text fallback_light: name: Fallback Light (Optional) description: "A default light to dim if the dial is rotated before any buttons have been pressed." default: "" selector: entity: domain: light dial_step_pct: name: Dial Step Size (%) description: Brightness percentage change for a slow/single step rotation. default: 10 selector: number: min: 1 max: 50 unit_of_measurement: "%" mode: slider dial_fast_pct: name: Dial Fast Step Size (%) description: Brightness percentage change for a fast rotation. default: 25 selector: number: min: 1 max: 100 unit_of_measurement: "%" mode: slider # ================= # BUTTON 1 (1 Dot) # ================= light_1: name: Button 1 - Target Light description: Light to toggle on Single Press and control via the Dial. (Leave empty if only using custom actions). default: "" selector: entity: domain: light button_1_single_press: name: Button 1 - Extra Single Press Actions description: Additional custom actions to run alongside the light toggle. default: [] selector: action: {} button_1_double_press: name: Button 1 - Double Press Actions default: [] selector: action: {} button_1_long_press: name: Button 1 - Long Press Actions default: [] selector: action: {} # ================= # BUTTON 2 (2 Dots) # ================= light_2: name: Button 2 - Target Light default: "" selector: entity: domain: light button_2_single_press: name: Button 2 - Extra Single Press Actions default: [] selector: action: {} button_2_double_press: name: Button 2 - Double Press Actions default: [] selector: action: {} button_2_long_press: name: Button 2 - Long Press Actions default: [] selector: action: {} # ================= # BUTTON 3 (3 Dots) # ================= light_3: name: Button 3 - Target Light default: "" selector: entity: domain: light button_3_single_press: name: Button 3 - Extra Single Press Actions default: [] selector: action: {} button_3_double_press: name: Button 3 - Double Press Actions default: [] selector: action: {} button_3_long_press: name: Button 3 - Long Press Actions default: [] selector: action: {} # ================= # BUTTON 4 (4 Dots) # ================= light_4: name: Button 4 - Target Light default: "" selector: entity: domain: light button_4_single_press: name: Button 4 - Extra Single Press Actions default: [] selector: action: {} button_4_double_press: name: Button 4 - Double Press Actions default: [] selector: action: {} button_4_long_press: name: Button 4 - Long Press Actions default: [] selector: action: {} # Parallel mode allows rapid sequential button presses across different buttons mode: parallel max: 10 trigger: - platform: mqtt topic: !input mqtt_topic condition: # Verify that the MQTT payload contains a valid action (ignores battery/linkquality pings) - condition: template value_template: "{{ trigger.payload_json is not none and trigger.payload_json.action is defined and trigger.payload_json.action != '' }}" action: - variables: light_1: !input light_1 light_2: !input light_2 light_3: !input light_3 light_4: !input light_4 helper_last_light: !input helper_last_light fallback_light: !input fallback_light dial_step_pct: !input dial_step_pct dial_fast_pct: !input dial_fast_pct # Extract action string from MQTT payload for clean condition checks action_type: "{{ trigger.payload_json.action }}" # Read helper memory to check for overlapping double-clicks and target lights helper_state: "{{ states(helper_last_light) }}" helper_parts: "{{ helper_state.split('|') if helper_state not in ['unknown', 'unavailable', 'none'] else [] }}" prev_light: "{{ helper_parts[0] if helper_parts | length > 0 else 'none' }}" prev_btn: "{{ helper_parts[1] if helper_parts | length > 1 else 'none' }}" prev_time: "{{ helper_parts[2] | float(0) if helper_parts | length > 2 else 0 }}" # Identifies if THIS trigger is the second click of an ongoing double-click window is_double_click_secondary: "{{ action_type == prev_btn and (as_timestamp(now()) - prev_time) <= 0.5 }}" # Determine which light to dim seamlessly target_dim_light: >- {% if prev_light not in ['none', 'unknown', 'unavailable', ''] %} {{ prev_light }} {% elif fallback_light != '' and fallback_light != none %} {{ fallback_light }} {% else %} none {% endif %} - choose: # ================================== # BUTTON 1 # ================================== - conditions: - condition: template value_template: "{{ action_type == 'button_1_press_release' }}" sequence: - choose: - conditions: - condition: template value_template: "{{ is_double_click_secondary }}" sequence: - stop: "Secondary click detected. Halting this parallel run to let the primary trace execute Double Click." # If not stopped, this is a fresh primary click. Update memory bank. - service: input_text.set_value target: entity_id: "{{ helper_last_light }}" data: value: "{{ (light_1 if light_1 != none else '') ~ '|button_1_press_release|' ~ as_timestamp(now()) }}" # Await potential Double-Click via native MQTT - wait_for_trigger: - platform: mqtt topic: !input mqtt_topic payload: "button_1_press_release" value_template: "{{ value_json.action | default('') }}" timeout: "00:00:00.500" continue_on_timeout: true - choose: - conditions: - condition: template value_template: "{{ wait.trigger is not none }}" sequence: - choose: [] default: !input button_1_double_press - conditions: - condition: template value_template: "{{ wait.trigger is none }}" sequence: - choose: - conditions: - condition: template value_template: "{{ light_1 != '' and light_1 != none }}" sequence: - service: light.toggle target: entity_id: "{{ light_1 }}" - choose: [] default: !input button_1_single_press - conditions: - condition: template value_template: "{{ action_type == 'button_1_hold' }}" sequence: - service: input_text.set_value target: entity_id: "{{ helper_last_light }}" data: value: "{{ (light_1 if light_1 != none else '') ~ '|button_1_hold|0' }}" - choose: [] default: !input button_1_long_press # ================================== # BUTTON 2 # ================================== - conditions: - condition: template value_template: "{{ action_type == 'button_2_press_release' }}" sequence: - choose: - conditions: - condition: template value_template: "{{ is_double_click_secondary }}" sequence: - stop: "Secondary click detected." - service: input_text.set_value target: entity_id: "{{ helper_last_light }}" data: value: "{{ (light_2 if light_2 != none else '') ~ '|button_2_press_release|' ~ as_timestamp(now()) }}" - wait_for_trigger: - platform: mqtt topic: !input mqtt_topic payload: "button_2_press_release" value_template: "{{ value_json.action | default('') }}" timeout: "00:00:00.500" continue_on_timeout: true - choose: - conditions: - condition: template value_template: "{{ wait.trigger is not none }}" sequence: - choose: [] default: !input button_2_double_press - conditions: - condition: template value_template: "{{ wait.trigger is none }}" sequence: - choose: - conditions: - condition: template value_template: "{{ light_2 != '' and light_2 != none }}" sequence: - service: light.toggle target: entity_id: "{{ light_2 }}" - choose: [] default: !input button_2_single_press - conditions: - condition: template value_template: "{{ action_type == 'button_2_hold' }}" sequence: - service: input_text.set_value target: entity_id: "{{ helper_last_light }}" data: value: "{{ (light_2 if light_2 != none else '') ~ '|button_2_hold|0' }}" - choose: [] default: !input button_2_long_press # ================================== # BUTTON 3 # ================================== - conditions: - condition: template value_template: "{{ action_type == 'button_3_press_release' }}" sequence: - choose: - conditions: - condition: template value_template: "{{ is_double_click_secondary }}" sequence: - stop: "Secondary click detected." - service: input_text.set_value target: entity_id: "{{ helper_last_light }}" data: value: "{{ (light_3 if light_3 != none else '') ~ '|button_3_press_release|' ~ as_timestamp(now()) }}" - wait_for_trigger: - platform: mqtt topic: !input mqtt_topic payload: "button_3_press_release" value_template: "{{ value_json.action | default('') }}" timeout: "00:00:00.500" continue_on_timeout: true - choose: - conditions: - condition: template value_template: "{{ wait.trigger is not none }}" sequence: - choose: [] default: !input button_3_double_press - conditions: - condition: template value_template: "{{ wait.trigger is none }}" sequence: - choose: - conditions: - condition: template value_template: "{{ light_3 != '' and light_3 != none }}" sequence: - service: light.toggle target: entity_id: "{{ light_3 }}" - choose: [] default: !input button_3_single_press - conditions: - condition: template value_template: "{{ action_type == 'button_3_hold' }}" sequence: - service: input_text.set_value target: entity_id: "{{ helper_last_light }}" data: value: "{{ (light_3 if light_3 != none else '') ~ '|button_3_hold|0' }}" - choose: [] default: !input button_3_long_press # ================================== # BUTTON 4 # ================================== - conditions: - condition: template value_template: "{{ action_type == 'button_4_press_release' }}" sequence: - choose: - conditions: - condition: template value_template: "{{ is_double_click_secondary }}" sequence: - stop: "Secondary click detected." - service: input_text.set_value target: entity_id: "{{ helper_last_light }}" data: value: "{{ (light_4 if light_4 != none else '') ~ '|button_4_press_release|' ~ as_timestamp(now()) }}" - wait_for_trigger: - platform: mqtt topic: !input mqtt_topic payload: "button_4_press_release" value_template: "{{ value_json.action | default('') }}" timeout: "00:00:00.500" continue_on_timeout: true - choose: - conditions: - condition: template value_template: "{{ wait.trigger is not none }}" sequence: - choose: [] default: !input button_4_double_press - conditions: - condition: template value_template: "{{ wait.trigger is none }}" sequence: - choose: - conditions: - condition: template value_template: "{{ light_4 != '' and light_4 != none }}" sequence: - service: light.toggle target: entity_id: "{{ light_4 }}" - choose: [] default: !input button_4_single_press - conditions: - condition: template value_template: "{{ action_type == 'button_4_hold' }}" sequence: - service: input_text.set_value target: entity_id: "{{ helper_last_light }}" data: value: "{{ (light_4 if light_4 != none else '') ~ '|button_4_hold|0' }}" - choose: [] default: !input button_4_long_press # ================================== # DIAL ROTATE LEFT (DIM DOWN) # ================================== - conditions: - condition: template value_template: "{{ action_type in ['dial_rotate_left_step', 'dial_rotate_left_slow', 'dial_rotate_left_fast'] }}" sequence: - condition: template value_template: "{{ target_dim_light != 'none' }}" - service: light.turn_on target: entity_id: "{{ target_dim_light }}" data: brightness_step_pct: >- {% if 'fast' in action_type %} {{ -1 * (dial_fast_pct | int) }} {% else %} {{ -1 * (dial_step_pct | int) }} {% endif %} continue_on_error: true # ================================== # DIAL ROTATE RIGHT (DIM UP) # ================================== - conditions: - condition: template value_template: "{{ action_type in ['dial_rotate_right_step', 'dial_rotate_right_slow', 'dial_rotate_right_fast'] }}" sequence: - condition: template value_template: "{{ target_dim_light != 'none' }}" - service: light.turn_on target: entity_id: "{{ target_dim_light }}" data: brightness_step_pct: >- {% if 'fast' in action_type %} {{ dial_fast_pct | int }} {% else %} {{ dial_step_pct | int }} {% endif %} continue_on_error: true