Map device id to name and vice versa

I use an RFXCom to receive input and send output to KlikAanKlikUit devices. The rfx-lights-in/out nodes use KaKu device id's like AC/0x01010101/1, but for various flows I need to translate these device id's to or from friendly names, like 'light-kitchen'. For example:

  • Given any KaKu input from the rfx-lights-in node, I want to send a Telegram message showing the device friendly name
  • Given a Telegram message or HTTP request that specifies the device friendly name and on/off action, I want to send a message to the rfx-lights-out node with the correct device id for the given device friendly name

What would be the best approach to accomplish this? I was thinking of the following approach:

  • Have a node that is configured with settings 'device id property' and 'friendly name property'; if the given device id property is set, the node will set the friendly name property accordingly, and vice versa if the friendly name property is set, the node will set the device id property accordingly.
  • Mapping between device id's and friendly names is configured through a configuration node.

For example, if the configuration node is configured with mappings AC/0x01010101/1<->light-kitchen and AC/0x01010101/2<->light-bedroom:

  • If the workflow node is called with a message that contains AC/0x01010101/1 as the device id property, it will set the friendly name property to light-kitchen
  • If the workflow node is called with a message that contains light-bedroom as the device friendly name property, it will set the device id property to AC/0x01010101/2

Does something like this exist already, or are there better ways for implementing this functionality?

Hi, I really wanted to write up and explain (one of many) ways you could achieve this but TBH, I am fairly hungover and found it easier just to knock up a demo (picture/flow worth a thousand words etc)...

[{"id":"911d97ae.52d498","type":"tab","label":"Flow 5","disabled":false,"info":""},{"id":"8a23298a.31ec48","type":"inject","z":"911d97ae.52d498","name":"Init pulse","topic":"","payload":"","payloadType":"bool","repeat":"","crontab":"","once":true,"onceDelay":0.1,"x":120,"y":60,"wires":[["e7219648.327ce8"]]},{"id":"e7219648.327ce8","type":"function","z":"911d97ae.52d498","name":"Initialise lookups","func":"var lookups = {};\nlookups.name2id = {}\nlookups.id2name = {}\nfunction addLookup(id, name){\n    lookups.name2id[name] = id;\n    lookups.id2name[id] = name;\n}\naddLookup(\"AC/0x01010101/1\",\"light-kitchen\");\naddLookup(\"AC/0x01010101/2\",\"light-bedroom\");\nflow.set(\"lookups\",lookups);\nreturn msg;","outputs":1,"noerr":0,"x":300,"y":60,"wires":[[]]},{"id":"f7ae7749.be0018","type":"function","z":"911d97ae.52d498","name":"Lookup msg.id or msg.name","func":"var lookups = flow.get(\"lookups\");\nif(msg.id){\n    msg.payload = lookups.id2name[msg.id]; \n    return msg;\n}\nif(msg.name){\n    msg.payload = lookups.name2id[msg.name]; \n    return msg;\n}\nif(msg.topic == \"id\"){\n    msg.payload = lookups.id2name[msg.payload];\n    return msg;\n}\nif(msg.topic == \"name\"){\n    msg.payload = lookups.name2id[msg.payload]; \n    return msg;\n}\nnode.warn(`msg doesnt contain id or name - or - topic is not set to id or msg`);","outputs":1,"noerr":0,"x":540,"y":200,"wires":[["c2586736.09aee8"]]},{"id":"44b36164.832bb","type":"comment","z":"911d97ae.52d498","name":"Send msg.id & get name in msg.payload output","info":"","x":600,"y":140,"wires":[]},{"id":"5e148952.f5db18","type":"comment","z":"911d97ae.52d498","name":"OR Send msg.name & get id in msg.payload output","info":"","x":610,"y":170,"wires":[]},{"id":"42368843.ae3358","type":"inject","z":"911d97ae.52d498","name":"AC/0x01010101/1 (topic + payload)","topic":"id","payload":"AC/0x01010101/1","payloadType":"str","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":200,"y":120,"wires":[["f7ae7749.be0018"]]},{"id":"3ddcbdfc.723f22","type":"inject","z":"911d97ae.52d498","name":"inject","topic":"id","payload":"AC/0x01010101/1","payloadType":"bool","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":110,"y":200,"wires":[["d132b532.5fa188"]]},{"id":"d132b532.5fa188","type":"change","z":"911d97ae.52d498","name":"msg.id = AC/0x01010101/2","rules":[{"t":"set","p":"id","pt":"msg","to":"AC/0x01010101/2","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":200,"y":240,"wires":[["f7ae7749.be0018"]]},{"id":"e365d7ca.71d098","type":"inject","z":"911d97ae.52d498","name":"light-kitchen (topic + payload)","topic":"name","payload":"light-kitchen","payloadType":"str","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":180,"y":160,"wires":[["f7ae7749.be0018"]]},{"id":"dc9efbb5.8f0fe8","type":"inject","z":"911d97ae.52d498","name":"inject","topic":"id","payload":"AC/0x01010101/1","payloadType":"bool","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":110,"y":280,"wires":[["5f332c43.24d824"]]},{"id":"5f332c43.24d824","type":"change","z":"911d97ae.52d498","name":"msg.name = light-bedroom","rules":[{"t":"set","p":"name","pt":"msg","to":"light-bedroom","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":200,"y":320,"wires":[["f7ae7749.be0018"]]},{"id":"c2586736.09aee8","type":"debug","z":"911d97ae.52d498","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":540,"y":260,"wires":[]},{"id":"f8ad16a1.e11ce8","type":"comment","z":"911d97ae.52d498","name":"** Edit lookups here **","info":"","x":320,"y":20,"wires":[]}]

please take a look inside the nodes & understand the key parts (making a lookup object, storing it in flow context, retrieving it from flow context & retrieving values by name or id)

Here is my standard solution - advantage to the previous solution:
No mixture of configuration and code - config is pure json.
Lookup code is generic and based on the topic field.

Also, i would use only the id and map it to a user friendly name just for the ui.

[{"id":"68b278fd.f60838","type":"inject","z":"c421dfeb.3e752","name":"config","topic":"","payload":"[{\"id\":\"AC/0x01010101/1\",\"name\":\"t1\",\"system\":\"xiaomi\",\"val\":5},{\"id\":\"AC/0x01010101/2\",\"name\":\"t2\",\"system\":\"xiaomi\",\"val\":15}]","payloadType":"json","repeat":"","crontab":"","once":true,"onceDelay":"","x":391,"y":3687,"wires":[["19ae1f9a.88e2a"]]},{"id":"19ae1f9a.88e2a","type":"split","z":"c421dfeb.3e752","name":"","splt":"\\n","spltType":"str","arraySplt":1,"arraySpltType":"len","stream":false,"addname":"","x":529,"y":3702,"wires":[["772ef20c.ee2ccc"]]},{"id":"772ef20c.ee2ccc","type":"change","z":"c421dfeb.3e752","name":"prep","rules":[{"t":"set","p":"topic","pt":"msg","to":"payload.id","tot":"msg"},{"t":"set","p":"priority","pt":"msg","to":"0","tot":"num"}],"action":"","property":"","from":"","to":"","reg":false,"x":677,"y":3717,"wires":[["65e2317c.17f1a"]]},{"id":"e54fd317.b8c3e","type":"inject","z":"c421dfeb.3e752","name":"","topic":"AC/0x01010101/1","payload":"1","payloadType":"num","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":535,"y":3794,"wires":[["65e2317c.17f1a"]]},{"id":"2e4dc67a.f88e9a","type":"debug","z":"c421dfeb.3e752","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","x":1004,"y":3768,"wires":[]},{"id":"65e2317c.17f1a","type":"function","z":"c421dfeb.3e752","name":"AJV8","func":"var countmsg = 2;          //how many items to join?\n\nvar nodublicates = false;   //only join if payload unequal existing payload\nvar limit;                 //only join if payloads are on both sides of the limit\nvar hysteresis;      //hysteresis \nvar clearaftersend = false;//clear queue after sending\nvar sendinstantly = false;\nvar reverse = false;         //reverse output array\nvar priomode = false;\nvar storagemode = true;     \n//------------ different topic mode ------------------\nvar diftopics = false;      //true: join only different topics; false: join only same topics\n\nvar inmsg = msg;\nvar outmsg = { \"topic\": inmsg.topic , \"payload\": \"\"};\nvar msgs = context.get(\"aggmsgs\")||{};\nvar topicmsgs = msgs[inmsg.topic]||[];\nvar lastval = context.get(\"lastval\");\nvar inprio = typeof inmsg.priority != \"undefined\" ? inmsg.priority: countmsg-1 ;     //default prio is 1\nvar revArr;\nvar tmsgs;\n\nif (inmsg.topic === \"\")\n    return null;\n\nif (typeof inmsg.reset != \"undefined\") {\n    msgs = {};\n    topicmsgs = [];\n    lastval = undefined;\n    context.set(\"aggmsgs\",msgs);\n    context.set(\"lastval\",lastval);\n    return null;\n}\nif (typeof inmsg.resettopic != \"undefined\") {\n    topicmsgs = [];\n    lastval = undefined;\n}\nif (typeof hysteresis !== 'undefined' && typeof lastval !== 'undefined') {\n    if (Math.abs(inmsg.payload - lastval) <= hysteresis) {\n        return null;\n    }\n}\nif (typeof lastval !== 'undefined') {\n    if (nodublicates && lastval == inmsg.payload)\n        return null;\n    if (typeof limit !== 'undefined' && countmsg == 2) {\n        if (!((lastval <= limit && inmsg.payload >= limit) || (lastval >= limit && inmsg.payload <= limit)))   \n            return null;\n    }\n} \n\nif (diftopics) {\n    if (topicmsgs.length > 0) {     //msg with same topic -> replace old msg\n        topicmsgs.shift();\n    }\n    topicmsgs.push(inmsg);\n    msgs[inmsg.topic] = topicmsgs;    \n    if (Object.keys(msgs).length > countmsg) {\n        for (tmsgs in msgs) {\n            if (msgs.hasOwnProperty(tmsgs)) {   \n                delete msgs[tmsgs];\n                break;\n            }\n        }\n    }\n    context.set(\"aggmsgs\",msgs);\n    context.set(\"lastval\",inmsg.payload);\n    if (Object.keys(msgs).length == countmsg) {\n        var aout = {};\n        outmsg.topic = \"\";\n        for (tmsgs in msgs) {\n            if (msgs.hasOwnProperty(tmsgs)) {\n                aout[tmsgs] = msgs[tmsgs][0];\n                outmsg.topic += tmsgs + \"_\";\n            }\n        }\n        if (clearaftersend) {\n            context.set(\"aggmsgs\",undefined);\n            context.set(\"lastval\",undefined);\n        }\n        outmsg.topic = outmsg.topic.substr(0,String(outmsg.topic).length-1);\n        outmsg.payload = aout;\n        if (reverse) {\n            revArr = outmsg.payload.map(a => Object.assign({}, a));\n            revArr.reverse();\n            outmsg.payload = revArr;\n        }\n        return outmsg;\n    } else\n        return null;\n} else {\n    if (topicmsgs.length == countmsg && !storagemode){\n        topicmsgs.shift();\n    }\n    if (storagemode) {\n        topicmsgs[inprio] = inmsg;\n    } else {\n        topicmsgs.push(inmsg);\n    }\n\n    msgs[inmsg.topic] = topicmsgs;\n    context.set(\"aggmsgs\",msgs);\n    context.set(\"lastval\",inmsg.payload);\n    \n    if (storagemode) {                          //check send msg\n        if (inprio < topicmsgs.length - 1)      //only send on trigger msg => no priority\n            return null;\n        for (var i = 0; i < topicmsgs.length; i++) { //only send if storage is full\n            if (topicmsgs[i] === undefined)\n                return null;\n        }    \n    }\n\n    if (topicmsgs.length == countmsg || sendinstantly) {\n        outmsg.payload = topicmsgs;\n        if (clearaftersend) {\n            msgs[inmsg.topic] = undefined;\n            context.set(\"aggmsgs\",msgs);\n            context.set(\"lastval\",undefined);\n        }\n        if (reverse) {\n            revArr = outmsg.payload.map(a => Object.assign({}, a));\n            revArr.reverse();\n            outmsg.payload = revArr;\n        }\n        return outmsg;\n    }\n    else\n        return null;\n}\n\n","outputs":1,"noerr":0,"x":819,"y":3768,"wires":[["2e4dc67a.f88e9a"]]},{"id":"e69b3e73.25ada","type":"inject","z":"c421dfeb.3e752","name":"","topic":"AC/0x01010101/2","payload":"1","payloadType":"num","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":487,"y":3851,"wires":[["65e2317c.17f1a"]]}]

I think my approach is similar to the others.

I have a flow that is triggered on startup that initialises some global variables that contain the mapping table as an object. I really should replace this with a file read but it works and I've not really had sufficient incentive to actually change it.

My RFX and other input nodes take incoming data and normalise the output including translating the device ID to something more friendly and adding location. That way, I can track by location even if the sensor/switch/etc is moved elsewhere.

For outputs (mainly switches). I use a standard naming convention: SWITCH01 .... SWITCH16
Again, these can be mapped to actual locations and the numbers correspond to some LightwaveRF remote controls that are grouped into 4 on/off buttons with a 4-way switch giving 16 switch options. It also has a master on/off which I use to group a set of light switches to go off at bedtime (which varies so I turn them off manually). I have a few other logical outputs such as BELL01 which would trigger the sounder of our front-door bell - never used that but you never know :slight_smile:

There is a flow that maps the logical control name to a physical device ID and splits the output depending on device type which I also have in the map variable. That is because some outputs are on the RFX but others are WiFi connected.

The same map variable is sent to my custom dashboard so that I can have a simple web page to control lighting as well. Eventually, if I ever find the time, I will include a "scene" map as well so that the web app will show and control groups of lights.

Oops, looks like the builders have just dislodged the smart heating controller!

Thanks for all the suggestions.

Instead of defining the mappings in some custom JavaScript/JSON code, I have now developed a small Node-RED plugin that allows for defining re-usable mappings through configuration nodes. These mappings can then be used by the map , set and switch nodes provided by the plugin under the mappings category.

Its main use is to define mappings between device id's and device names:

  • The map node allows you to map an incoming device id to a device name, and vice versa.
  • The set node allows for selecting a device from the configured mappings, and then add the device id or name to an incoming message.
  • The switch node allows for defining node outputs by matching incoming messages against the configured mappings.

This plugin has now been published and announced here: https://discourse.nodered.org/t/announce-node-red-contrib-map