Files
ha-blueprints/philips-hue-tap-dial-z2m-light-control.yaml

510 lines
18 KiB
YAML

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