UIBuilder - How to know if somebody is connected & how many people?

Hi all,

I'm looking to know how to retrieve if somebody is connected, and how many people ?
The idea is to do jobs only when somebody is connected.

Does anyone have an idea ? Thanks for your help.

.Christian

I assume from the tag that you mean via uibuilder?

uibuilder signals each client connection and each disconnection using a control message. The control msg contains information that helps you identify the client. e.g. the IP address, a "stable" client id (that identifies the users browser and is only reset if that browser is closed), the socket.io socketId (which changes every time the client reconnects) and so on. There are more identifiers in the upcoming v6.1 release as well.

To do proper session management, you need to keep a record of connections in node-red. The socketId changes rather too rapidly (a socket.io limitation not a uibuilder one), the IP address may have several clients connected since each browser tab is a new client connection. The stable client id identifies the browser more than the user.

How you put those things together is very much dependent on what you want to achieve from managing sessions. For example, would multiple client connections from a single IP address (which might be from multiple browsers, multiple windows and multiple tabs) constitute a single "person"? While that might work in most cases (since I do take into account intermediate proxy servers), a badly configured proxy would throw it out.

Lots of choice.

This is what I use / created a while ago. works ok for me:

[{"id":"1f0fd6d3ca2ac5ec","type":"tab","label":"Clients Connection state","disabled":false,"info":"","env":[]},{"id":"cf1062a1b4f34f1b","type":"ui_ui_control","z":"1f0fd6d3ca2ac5ec","name":"","events":"all","x":540,"y":360,"wires":[["58829326795af4b4","2be7704bb4bf45bf"]]},{"id":"58829326795af4b4","type":"debug","z":"1f0fd6d3ca2ac5ec","name":"debug 86","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":700,"y":400,"wires":[]},{"id":"2be7704bb4bf45bf","type":"function","z":"1f0fd6d3ca2ac5ec","name":"setSlobalConnectedClients","func":"let clientId = msg.socketid;\nlet action = msg.payload;\nlet originalPayload = JSON.parse(JSON.stringify(msg));\n\n//let value = \"\";\n//let connectedClients = JSON.parse(JSON.stringify(global.get('globalConnectedClients') || {}));\nlet connectedClients = global.get('globalConnectedClients') || {};\n//let myJSON2 = JSON.stringify(connectedClients);\n\nlet found = false;\nfor (let x in connectedClients) \n{\n    // client found in list;\n    if (x == clientId) \n    {\n        found = true;\n        // on connection (refresh client): \n        if (action === \"connect\") connectedClients[clientId] = originalPayload;\n\n        // on connection lost (remove client):\n        if (action === \"lost\") delete connectedClients[clientId];\n        \n        // on group event (refresh client)\n        if (action === \"change\") connectedClients[clientId] = originalPayload;\n\n\n        // // clientId gibts, also gucken wie der clientId gibt:\n        // let nestedObj = connectedClients[clientId];\n        // for (var y in nestedObj) \n        // {\n        //     if (y == key) {\n        //         // value updaten wenn vorhanden:\n        //         value = config[clientIp][key];\n        //         break;\n        //     }\n        // }\n    }\n}\n\n// client not found in list:\nif(!found)\n{\n    // on connection asn not found (add client): \n    connectedClients[clientId] = originalPayload;\n}\n\nglobal.set('globalConnectedClients', connectedClients);\n\n\nmsg.orgMsg = originalPayload;\nmsg.payload = connectedClients;\nreturn msg;\n\n\n\n// let flow = msg.payload.flow;\n// let key = msg.payload.key;\n// let value = msg.payload.value;\n\n// //var myJSON2 = JSON.stringify(myObj2);\n\n// var config = global.get('globalConfig') || \"empty\";\n// //var myJSON2 = JSON.stringify(config);\n\n// //  console.log(myJSON2);\n// let ValueSetted = false;\n// for (var x in config) {\n//     if (x == flow) {\n//         // flow gibts, also gucken obs key gibt:\n//         let nestedObj = config[flow];\n//         for (var y in nestedObj) {\n//             if (y == key) {\n//                 // value updaten wenn vorhanden:\n//                 config[flow][key] = value;\n//                 ValueSetted = true;\n//                 break;\n//             }\n//         }\n//         if (ValueSetted === false) {\n//             // key value einfügen, wenn nicht vorhanden:\n//             config[flow][key] = value;\n//             ValueSetted = true;\n//             break;\n//         }\n//     }\n// }\n// // Value wurde nicht gesetzt / Flow nicht gefunden, also Flow neu einfügen:\n// if (ValueSetted === false) {\n//     let newNest = { [key]: value };\n//     config[flow] = newNest;\n// }\n\n// //myJSON2 = JSON.stringify(config);\n// global.set('globalConfig', config);\n// config = global.get('globalConfig') || \"empty\";\n// msg.payload = config;\n// return msg;\n","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":760,"y":360,"wires":[["da0406d3c3dbb7f9"]]},{"id":"da0406d3c3dbb7f9","type":"debug","z":"1f0fd6d3ca2ac5ec","name":"debug 87","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1020,"y":360,"wires":[]},{"id":"1d923386a52d0e14","type":"inject","z":"1f0fd6d3ca2ac5ec","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":550,"y":180,"wires":[["48ec37b79da7989f"]]},{"id":"48ec37b79da7989f","type":"function","z":"1f0fd6d3ca2ac5ec","name":"clearSlobalConnectedClients","func":"global.set('globalConnectedClients', {});\nreturn msg;\n\n\n\n// let flow = msg.payload.flow;\n// let key = msg.payload.key;\n// let value = msg.payload.value;\n\n// //var myJSON2 = JSON.stringify(myObj2);\n\n// var config = global.get('globalConfig') || \"empty\";\n// //var myJSON2 = JSON.stringify(config);\n\n// //  console.log(myJSON2);\n// let ValueSetted = false;\n// for (var x in config) {\n//     if (x == flow) {\n//         // flow gibts, also gucken obs key gibt:\n//         let nestedObj = config[flow];\n//         for (var y in nestedObj) {\n//             if (y == key) {\n//                 // value updaten wenn vorhanden:\n//                 config[flow][key] = value;\n//                 ValueSetted = true;\n//                 break;\n//             }\n//         }\n//         if (ValueSetted === false) {\n//             // key value einfügen, wenn nicht vorhanden:\n//             config[flow][key] = value;\n//             ValueSetted = true;\n//             break;\n//         }\n//     }\n// }\n// // Value wurde nicht gesetzt / Flow nicht gefunden, also Flow neu einfügen:\n// if (ValueSetted === false) {\n//     let newNest = { [key]: value };\n//     config[flow] = newNest;\n// }\n\n// //myJSON2 = JSON.stringify(config);\n// global.set('globalConfig', config);\n// config = global.get('globalConfig') || \"empty\";\n// msg.payload = config;\n// return msg;\n","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":780,"y":180,"wires":[["1c8b47c007631512"]]},{"id":"1c8b47c007631512","type":"debug","z":"1f0fd6d3ca2ac5ec","name":"debug 88","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1040,"y":180,"wires":[]},{"id":"dcf44afb32e9f7b9","type":"comment","z":"1f0fd6d3ca2ac5ec","name":"cleanConnections","info":"","x":570,"y":120,"wires":[]},{"id":"abc2f4de752ae7bc","type":"comment","z":"1f0fd6d3ca2ac5ec","name":"setConnections","info":"","x":560,"y":300,"wires":[]},{"id":"04ef72fe57742e41","type":"comment","z":"1f0fd6d3ca2ac5ec","name":"getConnections","info":"","x":560,"y":500,"wires":[]},{"id":"9c230043d796bf13","type":"ui_ui_control","z":"1f0fd6d3ca2ac5ec","d":true,"name":"","events":"all","x":540,"y":580,"wires":[["0e8073a22a5ce50e"]]},{"id":"24305008ea9855b1","type":"function","z":"1f0fd6d3ca2ac5ec","d":true,"name":"getGlobalConnectedClients","func":"let clients = global.get('globalConnectedClients') || {};\n\n//msg.clients = clients;\nmsg.clientsInWebcamsTab = false;\nmsg.clientsConnected = Object.keys(clients).length; \nif (msg.clientsConnected > 0)\n{\n    for (let x in clients) \n    {\n        x = clients[x];\n        //msg.x = clients[x];\n        // client is connected to webcams tab:\n        if (x.name !== undefined && x.name === \"Webcams\" && x.payload === \"change\") \n        {\n            msg.clientsInWebcamsTab = true;\n            break;\n        }\n    }\n \n}\n// only return if no client on webcams or connected:\nif (!msg.clientsInWebcamsTab || msg.clientsConnected > 0) return msg;\nreturn null;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":920,"y":580,"wires":[["8444364af3373fa9","f1ff61832c87d4fe"]]},{"id":"0e8073a22a5ce50e","type":"delay","z":"1f0fd6d3ca2ac5ec","d":true,"name":"","pauseType":"delay","timeout":"200","timeoutUnits":"milliseconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"allowrate":false,"outputs":1,"x":690,"y":580,"wires":[["24305008ea9855b1"]]},{"id":"8444364af3373fa9","type":"debug","z":"1f0fd6d3ca2ac5ec","d":true,"name":"debug 89","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1140,"y":540,"wires":[]},{"id":"f1ff61832c87d4fe","type":"link out","z":"1f0fd6d3ca2ac5ec","d":true,"name":"IsConnectedToWebcamsTab","mode":"link","links":[],"x":1095,"y":580,"wires":[]},{"id":"fbf924f1cf3d3435","type":"function","z":"1f0fd6d3ca2ac5ec","name":"setSlobalConnectedClients","func":"let connectedClients = global.get('globalConnectedClients') || {};\n\nfor (let i in connectedClients)\n{\n    if (connectedClients[i].payload === \"lost\") delete connectedClients[i];\n}\n\nglobal.set('globalConnectedClients', connectedClients);\n\nmsg.payload = connectedClients;\nreturn msg;\n\n\n\n// let flow = msg.payload.flow;\n// let key = msg.payload.key;\n// let value = msg.payload.value;\n\n// //var myJSON2 = JSON.stringify(myObj2);\n\n// var config = global.get('globalConfig') || \"empty\";\n// //var myJSON2 = JSON.stringify(config);\n\n// //  console.log(myJSON2);\n// let ValueSetted = false;\n// for (var x in config) {\n//     if (x == flow) {\n//         // flow gibts, also gucken obs key gibt:\n//         let nestedObj = config[flow];\n//         for (var y in nestedObj) {\n//             if (y == key) {\n//                 // value updaten wenn vorhanden:\n//                 config[flow][key] = value;\n//                 ValueSetted = true;\n//                 break;\n//             }\n//         }\n//         if (ValueSetted === false) {\n//             // key value einfügen, wenn nicht vorhanden:\n//             config[flow][key] = value;\n//             ValueSetted = true;\n//             break;\n//         }\n//     }\n// }\n// // Value wurde nicht gesetzt / Flow nicht gefunden, also Flow neu einfügen:\n// if (ValueSetted === false) {\n//     let newNest = { [key]: value };\n//     config[flow] = newNest;\n// }\n\n// //myJSON2 = JSON.stringify(config);\n// global.set('globalConfig', config);\n// config = global.get('globalConfig') || \"empty\";\n// msg.payload = config;\n// return msg;\n","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":760,"y":720,"wires":[["ccebc7946008c93a"]]},{"id":"ccebc7946008c93a","type":"debug","z":"1f0fd6d3ca2ac5ec","name":"debug 97","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1020,"y":720,"wires":[]},{"id":"10e36a2c4397ce2e","type":"inject","z":"1f0fd6d3ca2ac5ec","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"60","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":550,"y":720,"wires":[["fbf924f1cf3d3435"]]},{"id":"10f18bcaa6cd9490","type":"comment","z":"1f0fd6d3ca2ac5ec","name":"cleanLostConnections","info":"","x":580,"y":680,"wires":[]},{"id":"552bb95dfd936ee3","type":"function","z":"1f0fd6d3ca2ac5ec","name":"setSlobalConnectedClients","func":"global.set('globalConnectedClients', msg.payload);\nreturn null;\n\n\n\n// let flow = msg.payload.flow;\n// let key = msg.payload.key;\n// let value = msg.payload.value;\n\n// //var myJSON2 = JSON.stringify(myObj2);\n\n// var config = global.get('globalConfig') || \"empty\";\n// //var myJSON2 = JSON.stringify(config);\n\n// //  console.log(myJSON2);\n// let ValueSetted = false;\n// for (var x in config) {\n//     if (x == flow) {\n//         // flow gibts, also gucken obs key gibt:\n//         let nestedObj = config[flow];\n//         for (var y in nestedObj) {\n//             if (y == key) {\n//                 // value updaten wenn vorhanden:\n//                 config[flow][key] = value;\n//                 ValueSetted = true;\n//                 break;\n//             }\n//         }\n//         if (ValueSetted === false) {\n//             // key value einfügen, wenn nicht vorhanden:\n//             config[flow][key] = value;\n//             ValueSetted = true;\n//             break;\n//         }\n//     }\n// }\n// // Value wurde nicht gesetzt / Flow nicht gefunden, also Flow neu einfügen:\n// if (ValueSetted === false) {\n//     let newNest = { [key]: value };\n//     config[flow] = newNest;\n// }\n\n// //myJSON2 = JSON.stringify(config);\n// global.set('globalConfig', config);\n// config = global.get('globalConfig') || \"empty\";\n// msg.payload = config;\n// return msg;\n","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":760,"y":860,"wires":[["5e230928e0e62dd7"]]},{"id":"5e230928e0e62dd7","type":"debug","z":"1f0fd6d3ca2ac5ec","name":"debug 98","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1020,"y":860,"wires":[]},{"id":"f631bd543950e508","type":"inject","z":"1f0fd6d3ca2ac5ec","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{\"2r8V8VJFZDwHYzxHAAAB\":{\"payload\":\"lost\",\"socketid\":\"2r8V8VJFZDwHYzxHAAAB\",\"socketip\":\"192.168.0.115\",\"_msgid\":\"c77e4c70549fdef0\"},\"SlyObDGQj0QEU29zAAAf\":{\"payload\":\"change\",\"tab\":15,\"name\":\"Administration\",\"socketid\":\"SlyObDGQj0QEU29zAAAf\",\"socketip\":\"192.168.0.115\",\"params\":{},\"_msgid\":\"e0d4643071ca494e\"},\"0QEvFG9sq0XU3wCaAAAh\":{\"payload\":\"change\",\"tab\":0,\"name\":\"Schalter - Licht\",\"socketid\":\"0QEvFG9sq0XU3wCaAAAh\",\"socketip\":\"192.168.0.115\",\"params\":{},\"_msgid\":\"cdb7e76f5314643b\"}}","payloadType":"json","x":530,"y":860,"wires":[["552bb95dfd936ee3"]]},{"id":"83e335a682ebd6d4","type":"comment","z":"1f0fd6d3ca2ac5ec","name":"test setConnections","info":"","x":570,"y":820,"wires":[]}]

little typo copy and paste reproducer included :wink:

Unfortunately Dashboard, which you are using in your example, only has the socket id and source ip address (and I've not tested how complete the Dashboard IP address processing is). Also note that the socket id changes if the websocket connection is lost (e.g. if the PC goes to sleep or the user reloads the page).

As explained above - this might be enough or it might not depending on the requirements.

Dashboard Connect msg:
image

uibuilder Connect msg (v6.1 not yet released. v6.0 has slightly less):

In addition, uibuilder has the option to include that data in ALL messages from the client so that you can make ongoing security decisions for each incoming msg.

The uibuilder clientId is the "stable" unique identifier for a specific browser. It is only reset if the browser is completely shut down and restarted.

The tabId identifies the browser tab that the message came from (not the SPA tab as in Dashboards case). Useful if you need to allow different connected tabs to have different views.

The pageName is there because uibuilder allows for multiple actual pages (rather than Dashboard's "tabs").

1 Like

well, you are right. It may not be 100 % by reliable. If a client leaves by hibernate or crash it will not get that. In my case its not such a big thing if a client stays connected since there are not many users. I ve installed ui-builder but never used it. Its properly not combinable with the Dashboard I ve already build up my system?

Well, you can include uibuilder pages into Dashboard or Dashboard into uibuilder pages using iframes. So there is certainly flexibility there even though the processing is different and you may need to do some tweaking of styles.

However, we are getting away from the original question. But feel free to start another thread about integration between Dashboard and uibuilder. :slight_smile:

Well, I thought it was easier because when you look at the node in node-red you get the information directly. I think I'll implement @WhiteLion's solution while waiting for uibuilder version 6.1.

Thanks a lot for your help !!!