From 08f7332b7e7251c9d070a809ed3854f491d7daff Mon Sep 17 00:00:00 2001 From: Matthew Grove Date: Mon, 3 Aug 2026 22:00:57 +0100 Subject: [PATCH] Smoothly handle double click detection for Philips Hue Tap Dials - allow fast pressing of different buttons --- philips-hue-tap-dial-z2m-light-control.yaml | 156 ++++++++++---------- 1 file changed, 81 insertions(+), 75 deletions(-) diff --git a/philips-hue-tap-dial-z2m-light-control.yaml b/philips-hue-tap-dial-z2m-light-control.yaml index dded3ea..414766d 100644 --- a/philips-hue-tap-dial-z2m-light-control.yaml +++ b/philips-hue-tap-dial-z2m-light-control.yaml @@ -11,6 +11,8 @@ blueprint: - 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. @@ -160,9 +162,9 @@ blueprint: selector: action: {} -# Single mode allows our Wait For Trigger double click logic to operate natively. -mode: single -max_exceeded: silent +# Parallel mode allows rapid sequential button presses across different buttons +mode: parallel +max: 10 trigger: - platform: mqtt @@ -187,11 +189,21 @@ action: # Extract action string from MQTT payload for clean condition checks action_type: "{{ trigger.payload_json.action }}" - # Determine which light to dim (Last used helper, OR Fallback Light) + # 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: >- - {% set helper_val = states(helper_last_light) %} - {% if helper_val not in ['unknown', 'unavailable', ''] and helper_val != 'none' %} - {{ helper_val }} + {% if prev_light not in ['none', 'unknown', 'unavailable', ''] %} + {{ prev_light }} {% elif fallback_light != '' and fallback_light != none %} {{ fallback_light }} {% else %} @@ -206,18 +218,21 @@ action: - condition: template value_template: "{{ action_type == 'button_1_press_release' }}" sequence: - # First, update the memory helper so the dial is primed immediately - choose: - conditions: - condition: template - value_template: "{{ light_1 != '' and light_1 != none }}" + value_template: "{{ is_double_click_secondary }}" sequence: - - service: input_text.set_value - target: - entity_id: "{{ helper_last_light }}" - data: - value: "{{ light_1 }}" - # Await potential Double-Click directly via MQTT + - 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 @@ -225,19 +240,18 @@ action: 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: - # DOUBLE CLICK TRIGGERED - choose: [] default: !input button_1_double_press - conditions: - condition: template value_template: "{{ wait.trigger is none }}" sequence: - # SINGLE CLICK TRIGGERED - choose: - conditions: - condition: template @@ -253,16 +267,11 @@ action: - condition: template value_template: "{{ action_type == 'button_1_hold' }}" sequence: - - choose: - - conditions: - - condition: template - value_template: "{{ light_1 != '' and light_1 != none }}" - sequence: - - service: input_text.set_value - target: - entity_id: "{{ helper_last_light }}" - data: - value: "{{ light_1 }}" + - 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 @@ -277,13 +286,16 @@ action: - choose: - conditions: - condition: template - value_template: "{{ light_2 != '' and light_2 != none }}" + value_template: "{{ is_double_click_secondary }}" sequence: - - service: input_text.set_value - target: - entity_id: "{{ helper_last_light }}" - data: - value: "{{ light_2 }}" + - 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 @@ -291,6 +303,7 @@ action: value_template: "{{ value_json.action | default('') }}" timeout: "00:00:00.500" continue_on_timeout: true + - choose: - conditions: - condition: template @@ -317,16 +330,11 @@ action: - condition: template value_template: "{{ action_type == 'button_2_hold' }}" sequence: - - choose: - - conditions: - - condition: template - value_template: "{{ light_2 != '' and light_2 != none }}" - sequence: - - service: input_text.set_value - target: - entity_id: "{{ helper_last_light }}" - data: - value: "{{ light_2 }}" + - 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 @@ -341,13 +349,16 @@ action: - choose: - conditions: - condition: template - value_template: "{{ light_3 != '' and light_3 != none }}" + value_template: "{{ is_double_click_secondary }}" sequence: - - service: input_text.set_value - target: - entity_id: "{{ helper_last_light }}" - data: - value: "{{ light_3 }}" + - 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 @@ -355,6 +366,7 @@ action: value_template: "{{ value_json.action | default('') }}" timeout: "00:00:00.500" continue_on_timeout: true + - choose: - conditions: - condition: template @@ -381,16 +393,11 @@ action: - condition: template value_template: "{{ action_type == 'button_3_hold' }}" sequence: - - choose: - - conditions: - - condition: template - value_template: "{{ light_3 != '' and light_3 != none }}" - sequence: - - service: input_text.set_value - target: - entity_id: "{{ helper_last_light }}" - data: - value: "{{ light_3 }}" + - 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 @@ -405,13 +412,16 @@ action: - choose: - conditions: - condition: template - value_template: "{{ light_4 != '' and light_4 != none }}" + value_template: "{{ is_double_click_secondary }}" sequence: - - service: input_text.set_value - target: - entity_id: "{{ helper_last_light }}" - data: - value: "{{ light_4 }}" + - 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 @@ -419,6 +429,7 @@ action: value_template: "{{ value_json.action | default('') }}" timeout: "00:00:00.500" continue_on_timeout: true + - choose: - conditions: - condition: template @@ -445,16 +456,11 @@ action: - condition: template value_template: "{{ action_type == 'button_4_hold' }}" sequence: - - choose: - - conditions: - - condition: template - value_template: "{{ light_4 != '' and light_4 != none }}" - sequence: - - service: input_text.set_value - target: - entity_id: "{{ helper_last_light }}" - data: - value: "{{ light_4 }}" + - 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