Make the Philips Hue Tap dial control anything in your home, using Node-RED

Wow pretty ! I may steal your idea!

Is there anyway to do this directly in Home Assistant without installing Node Red?

I suppose you could create an automation (and therefore a blueprint) that intercepts the events and implements the logic that my Node-RED flow implements for you, but it would require a bunch of helpers to remember the click timeout and state machine state.

I only did it in Node Red because I contemplated doing it in Home Assistant directly, and I failed.

Best of luck to you.

1 Like

@Rudd-O very nice and detail work, I just implanted for two bedside Tap dials, now I am trying to adjust brightness and RGB controls, just like how you had volume media control, do you have a subflow for that you can share?

1 Like

What to look for instead of ZHA events if I am using ZB2MQTT? Thanks.

You should be able to see the device inside the dropdown of a device(pink) node. Either that or the mqtt in and listen to it’s topic. Connect to a debug with it set to complete message object. Then use a switch node to break up the individual commands.

I don’t because I’m not using the Tap dial for lights, but it should be easy to have a function node that remembers the last level from 0 to 255 using context.get() and context.set(), adds or subtracts from that level based on the amount rotated (this is provided by my node), and sends that to a service call light.turn_on Home Assistant Node Red Websocket node.

Not sure how Z2M event packets look like. But I bet it’s a 1:1 transformation to what the subflow I made wants.

did anybody got this working with Zigbee2Mqtt so far?
i think i have to change something on the Events node but not sure what

Not sure why you would use an event node when it’s broadcasting to mqtt. There are dedicated mqtt nodes, you just need to set it to a topic. If z2m creates devices the device node should work as well.

Not to mention the specific z2m nodes.

That’s awesome! Been looking to replace the buttons with dedicated icons on mine and yours is the first post I’ve seen which does exactly that!.

Do you have an stl file or any pointers where I could find one?

Hi,
have someone got it to work with lights and dimming them up and down? I dont get it to work, for some reason.

Offtopic good sir.

Hi, meanwhile i got it working. But i dont know how to get the long-press-event for each button in ZHA …

for each button, that i press long, i got the same ZHA event in my Log

hue_tap_dial Step With On Off event was fired with parameters: {‘step_mode’: <StepMode.Down: 1>, ‘step_size’: 255, ‘transition_time’: 8}

any suggestion? Till now i switch the radio-stations with the doubleclick of button with 4dots

i will share my flow in next week for others who wants to do this with ZHA instead of deConz

regards

The Node-RED flow is supposed to process those double taps for you.

Hi,
first thanks for your answer. But i think u havent understand me. I wanted to create a 8th exit, that fires, when there is a longpress event for each button. I think the long-press-event is easy to get (transitione time= 8) but i dont get the button, that was long pressed …
when i see your payload, there is no difference between a longpress of button 1 or 2 that i could use …
i have changed the subflow for the additional longpress event, maybe someone can have a look, to add the right button

[{"id":"4e994db4b13653d5","type":"subflow","name":"Hue Tap Dial longpress","info":"","category":"","in":[{"x":180,"y":200,"wires":[{"id":"68fbdca716ada157"}]}],"out":[{"x":2120,"y":400,"wires":[{"id":"0bd68f511154ccc1","port":3}]},{"x":2130,"y":440,"wires":[{"id":"0bd68f511154ccc1","port":4}]},{"x":2130,"y":260,"wires":[{"id":"0bd68f511154ccc1","port":0}]},{"x":2130,"y":300,"wires":[{"id":"0bd68f511154ccc1","port":1}]},{"x":2130,"y":340,"wires":[{"id":"0bd68f511154ccc1","port":2}]},{"x":2140,"y":480,"wires":[{"id":"0bd68f511154ccc1","port":5}]},{"x":1050,"y":640,"wires":[{"id":"59b5d0e44c355dad","port":7}]}],"env":[],"meta":{},"color":"#C0DEED","inputLabels":["zha"],"outputLabels":["up","down","click","double","tripple","longpress","else"],"status":{"x":1280,"y":580,"wires":[{"id":"66b32dad9f5784b3","port":0}]}},{"id":"68fbdca716ada157","type":"function","z":"4e994db4b13653d5","name":"Reformat data","func":"Object.keys(msg.payload.event).forEach((key, index) => {\n    msg.payload[key] = msg.payload.event[key];\n})\ndelete msg.payload[\"event\"];\nreturn msg;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":440,"y":320,"wires":[["59b5d0e44c355dad"]]},{"id":"59b5d0e44c355dad","type":"switch","z":"4e994db4b13653d5","name":"Knob action","property":"msg","propertyType":"jsonata","rules":[{"t":"jsonata_exp","v":"payload.command = \"step_with_on_off\" and payload.params.step_mode = 0 and payload.params.transition_time = 4","vt":"jsonata"},{"t":"jsonata_exp","v":"payload.command = \"step_with_on_off\" and payload.params.step_mode = 1 and payload.params.transition_time = 4","vt":"jsonata"},{"t":"jsonata_exp","v":"payload.command = \"recall\" and payload.params.scene_id = 1","vt":"jsonata"},{"t":"jsonata_exp","v":"payload.command = \"recall\" and payload.params.scene_id = 0","vt":"jsonata"},{"t":"jsonata_exp","v":"payload.command = \"recall\" and payload.params.scene_id = 5","vt":"jsonata"},{"t":"jsonata_exp","v":"payload.command = \"recall\" and payload.params.scene_id = 4","vt":"jsonata"},{"t":"jsonata_exp","v":"payload.command = \"step_with_on_off\" and payload.params.transition_time = 8","vt":"jsonata"},{"t":"else"}],"checkall":"false","repair":false,"outputs":8,"x":670,"y":320,"wires":[["98993c23f8e6a409"],["5214626b1788a042"],["6091155c8c2f9ed7"],["a0964ec7c08edf2b"],["28ccde4ba1ea1972"],["ac951353ca516ebb"],["42a089976e19329d"],[]],"outputLabels":["up","down","scene1","scene2","scene3","scene4","longpress","else"]},{"id":"98993c23f8e6a409","type":"function","z":"4e994db4b13653d5","name":"Reformat","func":"msg.payload = {\n    \"command\": \"up\",\n    \"step_size\": msg.payload.params.step_size / 8,\n    \"transition_time\": msg.payload.params.transition_time,\n}\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1060,"y":180,"wires":[["7e9d63211b95ace7"]]},{"id":"5214626b1788a042","type":"function","z":"4e994db4b13653d5","name":"Reformat","func":"msg.payload = {\n    \"command\": \"down\",\n    \"step_size\": msg.payload.params.step_size / 8,\n    \"transition_time\": msg.payload.params.transition_time,\n}\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1060,"y":220,"wires":[["7e9d63211b95ace7"]]},{"id":"6091155c8c2f9ed7","type":"function","z":"4e994db4b13653d5","name":"Reformat","func":"msg.payload = {\n    \"command\": \"click\",\n    \"button\": msg.payload.params.scene_id,\n}\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1060,"y":260,"wires":[["7e9d63211b95ace7"]]},{"id":"a0964ec7c08edf2b","type":"function","z":"4e994db4b13653d5","name":"Reformat","func":"msg.payload = {\n    \"command\": \"click\",\n    \"button\": 2,\n}\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1060,"y":300,"wires":[["7e9d63211b95ace7"]]},{"id":"28ccde4ba1ea1972","type":"function","z":"4e994db4b13653d5","name":"Reformat","func":"msg.payload = {\n    \"command\": \"click\",\n    \"button\": 3,\n}\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1060,"y":340,"wires":[["7e9d63211b95ace7"]]},{"id":"ac951353ca516ebb","type":"function","z":"4e994db4b13653d5","name":"Reformat","func":"msg.payload = {\n    \"command\": \"click\",\n    \"button\": msg.payload.params.scene_id,\n}\nreturn msg;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":1060,"y":380,"wires":[["7e9d63211b95ace7"]]},{"id":"252c28a0af3018ca","type":"trigger","z":"4e994db4b13653d5","name":"","op1":"","op2":"","op1type":"nul","op2type":"payl","duration":"250","extend":true,"overrideDelay":true,"units":"ms","reset":"","bytopic":"all","topic":"topic","outputs":1,"x":1400,"y":340,"wires":[["7e9d63211b95ace7"]]},{"id":"7e9d63211b95ace7","type":"function","z":"4e994db4b13653d5","name":"Process delayed taps","func":"var last = context.get(\"last_active\")\nvar current = {\n    time: Date.now(),\n    msg: msg\n};\n\nfunction fmt(msg) {\n    if (msg.payload.command == \"click\" || msg.payload.command == \"doubleclick\" || msg.payload.command == \"tripleclick\") {\n        return \"\" + msg.payload.command + \" of button \" + msg.payload.button + (msg.selection ? \" (mode \" + msg.selection + \")\" : \"\");\n    }\n    else if (msg.payload.command == \"up\" || msg.payload.command == \"down\") {\n        return \"\" + msg.payload.command + \" of step size \" + msg.payload.step_size + (msg.selection ? \" (mode \" + msg.selection + \")\" : \"\");\n    }\n    return JSON.stringify(msg);\n}\n\nfunction status(msg, buffering) {\n    node.status({ text: msg, fill: \"blue\", shape: buffering ? \"ring\" : \"dot\"});\n}\n\nif (current.msg.flush) {\n    if (last !== undefined && last !== null) {\n        context.set(\"last_active\", null);\n        status(\"flushing  \" + fmt(last.msg), false);\n        return [[last.msg], null];\n    }\n}\nelse if (current.msg.payload.command == \"click\") {\n    if (last === undefined || last === null) {\n        context.set(\"last_active\", current);\n        status(\"buffering \" + fmt(current.msg), true);\n        return [null, { flush: true }];\n    } else {\n        if (\n            current.msg.payload.button == last.msg.payload.button\n        ) {\n            if (\n                last.msg.payload.command == \"click\"\n            ) {\n                // Instead of queueing the current message,\n                // update the last message.\n                last.msg.payload.command = \"doubleclick\";\n                status(\"rebuffering \" + fmt(last.msg), true);\n                return [null, { flush: true }];\n            }\n            else if (\n                last.msg.payload.command == \"doubleclick\"\n            ) {\n                last.msg.payload.command = \"tripleclick\";\n                context.set(\"last_active\", null);\n                status(\"flushing \" + fmt(last.msg), false);\n                return [[last.msg], null];\n            }\n            else {\n                // This is an error\n                node.error({msg:\"Cannot be\", current: current, last: last})\n            }\n        } else { /* buttons don't match */\n            context.set(\"last_active\", current);\n            status(\"flushing \" + fmt(last.msg) + \" and buffering \" + fmt(current.msg), true);\n            return [[last.msg], { flush: true }];\n        }\n    }\n}\nelse {\n    if (last === undefined || last === null) {\n        status(\"relaying \" + fmt(current.msg), false);\n        return [current.msg];\n    }\n    else {\n        context.set(\"last_active\", null);\n        status(\"relaying previous \" + fmt(last.msg) + \" and current \" + fmt(current.msg), false);\n        return [[last.msg, current.msg], null];\n    }\n}\n","outputs":2,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":1400,"y":240,"wires":[["705858de13af6112"],["252c28a0af3018ca"]],"outputLabels":["output","flush request"]},{"id":"0bd68f511154ccc1","type":"function","z":"4e994db4b13653d5","name":"switch","func":"if (msg.payload.command == \"click\") {\n    return [msg, null, null, null, null];\n}\nif (msg.payload.command == \"doubleclick\") {\n    return [null, msg, null, null, null];\n}\nif (msg.payload.command == \"tripleclick\") {\n    return [null, null, msg, null, null];\n}\nif (msg.payload.command == \"up\") {\n    return [null, null, null, msg, null];\n}\nif (msg.payload.command == \"down\") {\n    return [null, null, null, null, msg];\n}\nif (msg.payload.command == \"longpress\") {\n    return [null, null, null, null, msg];\n}\n","outputs":6,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":1830,"y":320,"wires":[[],[],[],[],[],[]]},{"id":"66b32dad9f5784b3","type":"status","z":"4e994db4b13653d5","name":"","scope":["7e9d63211b95ace7"],"x":1060,"y":580,"wires":[[]]},{"id":"705858de13af6112","type":"function","z":"4e994db4b13653d5","name":"Selection func","func":"function getSelection() {\n    return context.get(\"selection\");\n}\n\nfunction cloneWithSelection(oldm) {\n    var m = RED.util.cloneMessage(oldm);\n    var g = getSelection();\n    if (g !== null && g !== undefined) {\n        m.payload.last_clicked_button = g;\n    }\n    return m;\n}\n\nfunction setSelection(sel) {\n    context.set(\"selection\", sel);\n}\n\nmsg = cloneWithSelection(msg);\n\nif (msg.payload.command == \"click\") {\n    setSelection(msg.payload.button);\n}\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1660,"y":260,"wires":[["0bd68f511154ccc1"]]},{"id":"42a089976e19329d","type":"function","z":"4e994db4b13653d5","name":"Reformat","func":"msg.payload = {\n    \"command\": \"longpress\"\n    }\nreturn msg;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":1060,"y":460,"wires":[["0bd68f511154ccc1"]]}]

Ah yeah my flow doesn’t handle long presses. It’ll have to be modified.

Contributions welcome!

Hello
I’m also trying to get a bit more out of the Hue Tap Dial.
@Valarie would you mind sharing how you got volume, double & triple click working with ZHA?
Right now I’m using this blueprint but it’s a bit limited in options…

Hi @Thyraz, I’m also interested in the STL file if you’d like to share :blush:

1 Like

Hi,
I would like to know how to " 4. Add an instance of the Hue Tap subflow to your palette, and connect the Events: all node output to the Hue Tap node’s input."
With Google I can’t find any explanation how to connect a node output to a subnote input.

Could you or anybody please give me a hint?