diff --git a/philips-hue-tap-dial-z2m-light-control.yaml b/philips-hue-tap-dial-z2m-light-control.yaml new file mode 100644 index 0000000..dded3ea --- /dev/null +++ b/philips-hue-tap-dial-z2m-light-control.yaml @@ -0,0 +1,503 @@ +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. + - **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: {} + +# Single mode allows our Wait For Trigger double click logic to operate natively. +mode: single +max_exceeded: silent + +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 }}" + + # Determine which light to dim (Last used helper, OR Fallback Light) + target_dim_light: >- + {% set helper_val = states(helper_last_light) %} + {% if helper_val not in ['unknown', 'unavailable', ''] and helper_val != 'none' %} + {{ helper_val }} + {% 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: + # First, update the memory helper so the dial is primed immediately + - 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 }}" + # Await potential Double-Click directly via 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: + # 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 + 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: + - 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 }}" + - 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: "{{ light_2 != '' and light_2 != none }}" + sequence: + - service: input_text.set_value + target: + entity_id: "{{ helper_last_light }}" + data: + value: "{{ light_2 }}" + - 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: + - 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 }}" + - 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: "{{ light_3 != '' and light_3 != none }}" + sequence: + - service: input_text.set_value + target: + entity_id: "{{ helper_last_light }}" + data: + value: "{{ light_3 }}" + - 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: + - 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 }}" + - 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: "{{ light_4 != '' and light_4 != none }}" + sequence: + - service: input_text.set_value + target: + entity_id: "{{ helper_last_light }}" + data: + value: "{{ light_4 }}" + - 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: + - 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 }}" + - 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