Here is how I populate a dashboard table with hostname and IP address of Linux devices alive on my network. It may be applicable to your list of sensors.
[{"id":"5669fc4cac1d0dfc","type":"group","z":"90fe1578c260fc2b","name":"Save info messages to context","style":{"label":true,"stroke":"#6f2fa0","fill":"#bfdbef","fill-opacity":"0.3"},"nodes":["23155ac8fea5b99d","7d726eaf71e047d0","3daf41fd05c6ea8b","7c174eceb569e8a8","c8519e227fe92b5b"],"x":14,"y":39,"w":772,"h":122},{"id":"23155ac8fea5b99d","type":"mqtt in","z":"90fe1578c260fc2b","g":"5669fc4cac1d0dfc","name":"","topic":"info/pi/#","qos":"2","datatype":"auto-detect","broker":"6acd88c889789c5c","nl":false,"rap":true,"rh":0,"inputs":0,"x":90,"y":100,"wires":[["3daf41fd05c6ea8b"]]},{"id":"7d726eaf71e047d0","type":"function","z":"90fe1578c260fc2b","g":"5669fc4cac1d0dfc","name":"Save to context","func":"var devices = flow.get(\"devices\") ?? {} // default is an empty object\nconst ip = msg.payload.ip // I want my table sorted by ip address so use it as key\nconst ts = new Date() // Timestamp message arrival\ndevices[ip] = msg.payload // Add message to object keyed by ip\ndevices[ip].timestamp = ts // Timesamp so I can identify stale messages\nflow.set (\"devices\", devices) // Save the updated context \n\nmsg.payload = devices[ip] // For debug output\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":680,"y":80,"wires":[[]]},{"id":"3daf41fd05c6ea8b","type":"function","z":"90fe1578c260fc2b","g":"5669fc4cac1d0dfc","name":"Filter bad data","func":"msg.type = typeof(msg.payload)\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":260,"y":100,"wires":[["c8519e227fe92b5b"]]},{"id":"7c174eceb569e8a8","type":"debug","z":"90fe1578c260fc2b","g":"5669fc4cac1d0dfc","name":"Bad data","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":660,"y":120,"wires":[]},{"id":"c8519e227fe92b5b","type":"switch","z":"90fe1578c260fc2b","g":"5669fc4cac1d0dfc","name":"type is Object?","property":"type","propertyType":"msg","rules":[{"t":"eq","v":"object","vt":"str"},{"t":"else"}],"checkall":"true","repair":false,"outputs":2,"x":460,"y":100,"wires":[["7d726eaf71e047d0"],["7c174eceb569e8a8"]]},{"id":"6acd88c889789c5c","type":"mqtt-broker","name":"","broker":"192.168.1.11","port":"1883","clientid":"","autoConnect":true,"usetls":false,"protocolVersion":"4","keepalive":"60","cleansession":true,"birthTopic":"broker/birth","birthQos":"2","birthPayload":"\"Connected\"","birthMsg":{},"closeTopic":"broker/close","closeQos":"2","closePayload":"\"Disconnecting\"","closeMsg":{},"willTopic":"broker/dead","willQos":"2","willPayload":"\"Disconnected\"","willMsg":{},"userProps":"","sessionExpiry":""},{"id":"75db055c564dc258","type":"group","z":"90fe1578c260fc2b","name":"Delete context variable at midnight to reset device list ","style":{"label":true,"stroke":"#6f2fa0","fill":"#dbcbe7","fill-opacity":"0.3"},"nodes":["04899783273dec1c","51c3ae80e4b0caad"],"x":14,"y":319,"w":432,"h":82},{"id":"04899783273dec1c","type":"inject","z":"90fe1578c260fc2b","g":"75db055c564dc258","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"00 00 * * *","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":130,"y":360,"wires":[["51c3ae80e4b0caad"]]},{"id":"51c3ae80e4b0caad","type":"change","z":"90fe1578c260fc2b","g":"75db055c564dc258","name":"","rules":[{"t":"delete","p":"devices","pt":"flow"}],"action":"","property":"","from":"","to":"","reg":false,"x":330,"y":360,"wires":[[]]},{"id":"733fb7b343c1f04d","type":"group","z":"90fe1578c260fc2b","name":"Construct Table","style":{"label":true,"stroke":"#92d04f","fill":"#e3f3d3","fill-opacity":"0.3"},"nodes":["222ba04e6e9a9f73","060287df849594fd","28a4d3149680d07f"],"x":14,"y":199,"w":552,"h":82},{"id":"222ba04e6e9a9f73","type":"inject","z":"90fe1578c260fc2b","g":"733fb7b343c1f04d","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"20","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":130,"y":240,"wires":[["060287df849594fd"]]},{"id":"060287df849594fd","type":"function","z":"90fe1578c260fc2b","g":"733fb7b343c1f04d","name":"Get from context","func":"function sortObj(obj) {\n return Object.keys(obj).sort().reduce(function (result, key) {\n result[key] = obj[key];\n return result;\n }, {});\n}\n\nlet devices = flow.get(\"devices\") ?? {} // get data from context\ndevices = sortObj(devices) // Sort it by key (IP address)\n\nlet arr = [] // Will pass an array of objects to the table\nconst now = new Date().getTime()\n\nfor (let ip in devices) { // Loop through sensors\n const age = (now - devices[ip].timestamp.getTime())/1000 // Age in seconds of the sensor report - can use to id stale records\n let rec = { // Construct object for this sensor\n \"ip\": ip,\n \"hostname\": devices[ip].hostname,\n // etc, etc\n }\n arr.push(rec) // Add it to the array\n}\n\nmsg.payload = arr;\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":320,"y":240,"wires":[["28a4d3149680d07f"]]},{"id":"28a4d3149680d07f","type":"ui_table","z":"90fe1578c260fc2b","g":"733fb7b343c1f04d","group":"a29767e44538ea22","name":"Table ","order":0,"width":"9","height":"6","columns":[],"outputs":0,"cts":false,"x":490,"y":240,"wires":[]},{"id":"a29767e44538ea22","type":"ui_group","name":"Pies","tab":"75458084fdb19d21","order":1,"disp":false,"width":"30","collapse":false,"className":""},{"id":"75458084fdb19d21","type":"ui_tab","name":"Status","icon":"dashboard","disabled":false,"hidden":false}]
I don't use a join node, each incoming mqtt message is added/updated discretely in a context variable.
My context variable is an object keyed by IP address rather than an array, so I can sort the data by IP.
ps. I don't actually know if I need to sort the context variable, maybe it will automatically come out in alphabetical order of the key?