Filter msg.payload.array and show only new object

Hey,
i have a flow that i can use to scan my network.

I would like to find only new devices with a "filter".

I already thought that this could work with a "split" and "join" node, but I only get "ALL" devices displayed as a result.

[{"id":"17dc25c63a1a2603","type":"exec","z":"e6b21fe72834945d","command":"sudo nmap -sn 192.168.178.0/24 | awk '/Nmap scan report for/{printf $5;}/MAC Address:/{print \"|\"substr($0, index($0,$3)) }' | sort","addpay":false,"append":"","useSpawn":"false","timer":"","winHide":false,"oldrc":false,"name":"scan subnet","x":370,"y":60,"wires":[["51e84dfc1c7a2879"],[],[]]},{"id":"51e84dfc1c7a2879","type":"function","z":"e6b21fe72834945d","name":"Subnet Devices Array","func":"let response = msg.payload.split('\\n');\nlet found = []\nlet device\nresponse.forEach ( line => {\n    if ( line.indexOf('|') > -1 ){\n        device = {\n            ip : line.split('|')[0],\n            mac: line.split('|')[1].split(' ')[0],\n            brand: line.split('|')[1].split(' ')[1]\n        }\n        found.push ( device )\n    }\n})\nmsg.payload = found;\nreturn msg;","outputs":1,"noerr":0,"x":600,"y":60,"wires":[["56e60425cad2376d"]]},{"id":"56e60425cad2376d","type":"debug","z":"e6b21fe72834945d","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":850,"y":60,"wires":[]},{"id":"828da343b0880e7b","type":"inject","z":"e6b21fe72834945d","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":170,"y":60,"wires":[["17dc25c63a1a2603"]]}]

Bildschirmfoto 2022-12-03 um 12.08.13


exec-node:

sudo nmap -sn 192.168.178.0/24 | awk '/Nmap scan report for/{printf $5;}/MAC Address:/{print "|"substr($0, index($0,$3)) }' | sort

funnction-node:

let response = msg.payload.split('\n');
let found = []
let device
response.forEach ( line => {
    if ( line.indexOf('|') > -1 ){
        device = {
            ip : line.split('|')[0],
            mac: line.split('|')[1].split(' ')[0],
            brand: line.split('|')[1].split(' ')[1]
        }
        found.push ( device )
    }
})
msg.payload = found;
return msg;

In order to do that, you would need to keep a copy of the last input message so that you can compare it against the new current message.

I do a periodic nmap scan of my network as well but approach it somewhat differently. I run the following shell script on a 15min CRON schedule:

#! /usr/bin/env bash
# Fast scan the local network for live devices and record
# to /tmp/nmap.xml which can be used in Node-RED
#

# Run the scan
nmap -sn --oX /tmp/nmap.xml --privileged -R --system-dns --webxml 192.168.1.0/24
# Make sure ownership & ACLs on the output are secure
chown root:home /tmp/nmap.xml
chmod --silent 640 /tmp/nmap.xml
# Trigger the Node-RED update
#curl  --silent --output /dev/null 'http://localhost:1880/localnetscan' > /dev/null
curl --insecure -I 'https://localhost:1880/localnetscan'

As you can see, that produces a temporary XML file and then calls a Node-RED http-in/-out endpoint to trigger the following flow.

Rather than having to calculate a diff each time, I simply update data with everything found. Currently, I have 107 entries. Each entry is stored by MAC address and maps IP address, hostname (where possible) and more. I have a web page that lets me update hostnames and descriptions so that I have a complete picture of all known devices.

1 Like

That's great!
I found your flow to the upper part in another post =)
Can you please show me how you set up the "show current device list"?

That is a simple uibuilder node using VueJS and bootstrap-vue to display the table and allow editing of some fields. It isn't especially pretty. You could probably do something similar with Dashboard and ui_table.

but how did you set up the function node "cache"?

you have a query here for: payload["host"].. or am I seeing it wrong?

Sorry, I was a bit cryptic in the last reply.

The cache function is this:


if ( msg.uibuilderCtrl === 'ready for content' && msg.cacheControl === 'REPLAY' && msg.from === 'client' ) {
    /** When a client loads or reloads, send the latest data 
     * NOTE that we send data/schema as an array of objects not an object
     *      because that is what bootstrap-vue's b-table component requires
     */
    return {
        topic: 'network',
        payload: Object.values(global.get('network','file')),
        // Include the schema so that tables will rend correctly
        schema: Object.values(global.get('schemas.network','file')),
        // Only send to the required client
        _socketId: msg._socketId,
    }
} else {
    /** Send the input msg to all connected clients */
    // Add the schema if required so that tables will rend correctly
    if (msg.topic === 'network') msg.schema = Object.values(global.get('schemas.network','file'))
    return msg
}

However, you don't need to do that any more since uibuilder now has a proper cache node. As you can see, the cache uses a node-red retained global variable to store everything for convenience. The variable is updated in the big function node in the main bit of flow. The bottom bit of flow is triggered from that main function so whenever there is an update to the data, any clients connected to the uibuilder node get the latest data. All the cache does is to make sure that if you connect a new client, it will get the last updated data in full. As I say, you could now replace that cache function node with a uib-cache node instead.

I've not shared the front-end code because I've just noticed that it isn't working for some reason. I've not used that page for a while so I assume that a library update has broken something.

but how did you define the index.html and *.js?
when i look at the example for the table, i manage to fill the table.
but unfortunately not in your flow. Is there anything special to consider?

As I say, not much point in me sharing the code since it doesn't seem to work at the moment.

However, you will see that the input to uibuilder looks like this:

image

Most table components want an array which you can get by doing Object.values(msg.payload) - in your index.html.

Here is some example front-end code using Vue v2 and Vuetify v2. Not complete but it gives you an idea. This took just a few minutes to throw together & I used the "Simple Vue" template as a base. Note that I used the CDN version of Vuetify but of course, you could also install via uibuilder's library manager and use locally.

index.html

<!doctype html>
<html lang="en"><head>

    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, minimal-ui">

    <title>Network Devices</title>
    <meta name="description" content="Network Devices: uibuilder + Vuetify">

    <link rel="icon" href="./images/node-blue.ico">

    <link href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900" rel="stylesheet">
    <link href="https://cdn.jsdelivr.net/npm/@mdi/font@6.x/css/materialdesignicons.min.css" rel="stylesheet">
    <link href="https://cdn.jsdelivr.net/npm/vuetify@2.x/dist/vuetify.min.css" rel="stylesheet">
    <link type="text/css" rel="stylesheet" href="./index.css" media="all">

</head><body>

    <div id="app" class="uib" v-cloak><!-- All UI code needs to be in here -->
        <v-app>
            <v-main>
                <v-container>
                    <h1>Network Devices (using uibuilder+Vue v2+Vuetify)</h1>

                    <v-data-table :headers="cols" :items="devices" :items-per-page="5" class="elevation-1"></v-data-table>
                    
                    <pre id="msg" v-html="showLastReceivedMsg" class="syntax-highlight">Waiting for a message from Node-RED</pre>
                </v-container>
            </v-main>
        </v-app>


        </b-container>

    <!-- #region Supporting Scripts. These MUST be in the right order. Note no leading / -->
    <script src="../uibuilder/vendor/socket.io/socket.io.js">/* REQUIRED: Socket.IO is loaded only once for all instances. Without this, you don't get a websocket connection */</script>
    <script src="../uibuilder/vendor/vue/dist/vue.min.js">/* prod version with component compiler */</script>
    <script src="https://cdn.jsdelivr.net/npm/vuetify@2.x/dist/vuetify.js"></script>
    <script src="./uibuilder.iife.js">/* REQUIRED: remove 'min.' to use dev version */</script>
    <script src="./index.js">/* OPTIONAL: Put your custom code here */</script>
    <!-- #endregion -->

</body></html>

index.js

/* jshint browser: true, esversion: 6, asi: true */
/*globals Vue, uibuilder */
// @ts-nocheck
'use strict'

// eslint-disable-next-line no-unused-vars
const app = new Vue({
    el: '#app',
    vuetify: new Vuetify(),
    data() { return {

        lastMsg    : '[Nothing]',
        devices: [],
        cols: [
            {
                text: 'MAC',
                align: 'start',
                sortable: true,
                value: 'mac',
            },
            {
                text: 'IP Addr',
                align: 'start',
                sortable: true,
                value: 'ipaddr',
            },
            {
                text: 'Name',
                align: 'start',
                sortable: true,
                value: 'host',
            },
{
                text: 'DHCP?',
                align: 'start',
                sortable: true,
                value: 'dhcp',
            },
            {
                text: 'Descr',
                align: 'start',
                sortable: true,
                value: 'description',
            },

        ],

    }}, // --- End of data --- //

    computed: {

        // Show the last msg from Node-RED nicely formatted
        showLastReceivedMsg: function() {
            var lastMsg = this.lastMsg
            if (typeof lastMsg === 'string') return 'Last Message Received = ' + lastMsg
            return 'Last Message Received = ' + this.syntaxHighlight(lastMsg)
        },

    }, // --- End of computed --- //

    methods: {

        // return formatted HTML version of JSON object
        syntaxHighlight: function(json) {
            json = JSON.stringify(json, undefined, 4)
            json = json.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
            json = json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) {
                var cls = 'number'
                if ((/^"/).test(match)) {
                    if ((/:$/).test(match)) {
                        cls = 'key'
                    } else {
                        cls = 'string'
                    }
                } else if ((/true|false/).test(match)) {
                    cls = 'boolean'
                } else if ((/null/).test(match)) {
                    cls = 'null'
                }
                return '<span class="' + cls + '">' + match + '</span>'
            })
            return json
        }, // --- End of syntaxHighlight --- //

    }, // --- End of methods --- //

    /** Called once all Vue component instances have been loaded and the virtual DOM built */
    mounted: function(){

        const app = this  // Reference to `this` in case we need it for more complex functions

        // If msg changes - msg is updated when a standard msg is received from Node-RED
        uibuilder.onChange('msg', function(msg){

            if ( msg.topic === 'network' ) {
                app.devices = Object.values(msg.payload)
            }

            app.lastMsg = msg
        })

    }, // --- End of mounted hook --- //

}) // --- End of app definition --- //

// EOF
1 Like

thank you! now I have a starting point...
Even though I've spent a lot of time with NodeRED... uibuilder is completely new territory for me

Try out the examples that come with the node so that you get a feel for the simplicity of use. Then more complex scenarios should also make sense.

@totallyInformation has given you a way to get this running yourself but if you want to see his original code (more or less) - and this works as I am using it myself - I have included the three comment nodes with the code in them that I use for information. [note- copyright goes to totallyInformation :rofl:)

[{"id":"1e87c86c97f26515","type":"comment","z":"cd281ebd2c4a291e","name":"index.html","info":"<!doctype html>\n<html lang=\"en\">\n\n<head>\n    <meta charset=\"utf-8\">\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n\n    <title>Home1: Network Devices</title>\n    <meta name=\"description\" content=\"Home1: Network Devices\">\n\n    <link rel=\"icon\" href=\"./images/node-blue.ico\">\n\n    <link href=\"../uibuilder/vendor/bootstrap/dist/css/bootstrap.min.css\" type=\"text/css\" rel=\"stylesheet\" />\n    <link href=\"../uibuilder/vendor/bootstrap-vue/dist/bootstrap-vue.css\" type=\"text/css\" rel=\"stylesheet\" />\n\n    <link href=\"./index.css\" media=\"all\" type=\"text/css\" rel=\"stylesheet\" />\n</head>\n\n<body>\n    <!-- Hidden icon data, <use xlink:href=\"/path/to/icons.svg#play\"></use> if IE not needed -->\n    <svg aria-hidden=\"true\" focusable=\"false\" style=\"display:none\">\n        <symbol id=\"icon-edit\" viewBox=\"0 0 576 512\">\n            <path fill=\"currentColor\"\n                d=\"M402.6 83.2l90.2 90.2c3.8 3.8 3.8 10 0 13.8L274.4 405.6l-92.8 10.3c-12.4 1.4-22.9-9.1-21.5-21.5l10.3-92.8L388.8 83.2c3.8-3.8 10-3.8 13.8 0zm162-22.9l-48.8-48.8c-15.2-15.2-39.9-15.2-55.2 0l-35.4 35.4c-3.8 3.8-3.8 10 0 13.8l90.2 90.2c3.8 3.8 10 3.8 13.8 0l35.4-35.4c15.2-15.3 15.2-40 0-55.2zM384 346.2V448H64V128h229.8c3.2 0 6.2-1.3 8.5-3.5l40-40c7.6-7.6 2.2-20.5-8.5-20.5H48C21.5 64 0 85.5 0 112v352c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48V306.2c0-10.7-12.9-16-20.5-8.5l-40 40c-2.2 2.3-3.5 5.3-3.5 8.5z\" />\n        \n        </symbol>\n\n        <symbol id=\"icon-delete\" viewBox=\"0 0 448 512\">\n            <path fill=\"currentColor\"\n                d=\"M32 464a48 48 0 0 0 48 48h288a48 48 0 0 0 48-48V128H32zm272-256a16 16 0 0 1 32 0v224a16 16 0 0 1-32 0zm-96 0a16 16 0 0 1 32 0v224a16 16 0 0 1-32 0zm-96 0a16 16 0 0 1 32 0v224a16 16 0 0 1-32 0zM432 32H312l-9.4-18.7A24 24 0 0 0 281.1 0H166.8a23.72 23.72 0 0 0-21.4 13.3L136 32H16A16 16 0 0 0 0 48v32a16 16 0 0 0 16 16h416a16 16 0 0 0 16-16V48a16 16 0 0 0-16-16z\">\n            \n            </path>\n\n        </symbol>\n\n    </svg>\n    <div id=\"app\" v-cloak>\n        <b-container id=\"app_container\" fluid>\n            <b-table striped hover small bordered caption-top :items=\"devs.items\" :fields=\"devs.cols\"\n                :primary-key=\"devs.key\" :busy=\"devs.busy\" :head-row-variant=\"devs.headRowVariant\" :sort-by=\"devs.key\"\n                :filter=\"devs.filter\" :per-page=\"devs.perPage\" :current-page=\"devs.currentPage\" @filtered=\"onFiltered\"\n                @row-dblclicked=\"rowDblClicked\" v-model=\"devs.model\">\n                <template v-slot:table-caption>\n                    <h2>Network Devices Table</h2>\n\n                    <!-- Filter (search) input - searches whole row - TODO: Add field selector dropdown -->\n                    <b-form-group label=\"Filter\" label-cols-sm=\"3\" label-align-sm=\"right\" label-size=\"sm\"\n                        label-for=\"filterInput\" class=\"mb-0\">\n                        <b-input-group size=\"sm\">\n                            <b-form-input v-model=\"devs.filter\" type=\"search\" id=\"filterInput\"\n                                placeholder=\"Type to search ...\"></b-form-input>\n\n                        </b-input-group>\n\n                    </b-form-group>\n\n                </template>\n\n                <template v-slot:table-busy>\n                    <div class=\"text-center text-danger my-2\">\n                        <b-spinner class=\"align-middle\"></b-spinner>\n\n                        <strong>Loading...</strong>\n\n                    </div>\n\n                </template>\n\n                <!-- Custom Header scoped slot -->\n                <!--<template v-slot:thead-top=\"data\"><b-tr><b-td :colspan=\"data.columns\">\n                </b-td></b-tr></template>-->\n\n                <!-- Custom Footer scoped slot -->\n                <template v-slot:custom-foot=\"data\">\n                    <b-tr>\n                        <b-td :colspan=\"data.columns\">\n                            <b-container fluid class=\"m-0 p-0\">\n                                <b-form-row align-h=\"between\" align-v=\"center\">\n                                    <b-col>\n                                        <b-button @click=\"tblItemAdd(data, $event.target)\">\n                                            Add New Entry\n                                        </b-button>\n\n                                    </b-col>\n                                    <b-col cols=\"1\">\n                                        <b-form-input id=\"devs-rowsperpage\" type=\"number\" v-model=\"devs.perPage\"\n                                            align=\"right\" title=\"Rows per page. Use 0 to show whole table.\">\n                                        </b-form-input>\n\n                                    </b-col>\n                                    <b-col cols=\"3\">\n                                        <b-pagination v-model=\"devs.currentPage\" :total-rows=\"devs.totalRows\"\n                                            :per-page=\"devs.perPage\" first-number last-number align=\"right\"\n                                            class=\"m-0 p-0\" title=\"Select page\"></b-pagination>\n\n                                    </b-col>\n\n                                    </b-row>\n\n                            </b-container>\n                            <!--\n                    <b-button class=\"float-left\"\n                        @click=\"tblItemAdd(data, $event.target)\"\n                    >\n                        Add New Entry\n                    </b-button>\n                    <div class=\"float-right\">\n                        <b-form-input id=\"devs-rowsperpage\" type=\"number\" v-model=\"devs.perPage\"></b-form-input>\n                        <b-pagination\n                            v-model=\"devs.currentPage\"\n                            :total-rows=\"devs.totalRows\"\n                            :per-page=\"devs.perPage\"\n                            first-number last-number\n                        ></b-pagination>\n                    </div>\n                    -->\n                        </b-td>\n\n                    </b-tr>\n\n                </template>\n\n                <!-- Default data cell scoped slot (only used if allowing inline edits) -->\n                <template v-if = \"devs.inlineEdit\" v-slot:cell() = \"row\">\n                    <!--<textarea v-if=\"row.field.editable\"\n                        @change=\"cellEdit('change', row, $event.target, devs.model)\"\n                    >{{row.value}}</textarea>-->\n\n                    <b-form-textarea v-if=\"row.field.editable\" :id=\"'inpta-'+row.field.key+'-'+row.index\"\n                        v-model=\"row.item[row.field.key]\" trim lazy\n                        @change=\"cellEdit('change', row, $event, devs.model)\"></b-form-textarea>\n\n                    <!--<input type=\"textarea\" v-if=\"row.field.editable\" :value=\"row.value\"></input>-->\n    \n                    <span v-else>{{ row.value }}</span>\n\n                </template>\n\n                <!-- Actions column -->\n                <template v-slot:cell(actions)=\"row\">\n                    <!--<b-button variant=\"outline-secondary\" class=\"p-0\" v-b-modal.modal1>\n                        <svg class=\"icon m-2\" aria-hidden=\"true\" focusable=\"false\">\n                            <use xlink:href=\"#icon-edit\"></use>\n                        </svg>\n                    </b-button>-->\n                    <b-button variant=\"outline-secondary\" class=\"p-0\" @click=\"tblRowEdit(row, $event, devs.model)\"\n                        v-if=\"!devs.inlineEdit\">\n                        <svg class=\"icon m-2\" aria-hidden=\"true\" focusable=\"false\">\n                            <use xlink:href=\"#icon-edit\"></use>\n\n                        </svg>\n\n                    </b-button>\n\n                    <span class=\"access-label\">Edit</span>\n                    <b-button variant=\"outline-secondary\" class=\"p-0\"\n                        @click=\"tblRowDelete(row.item, row.index, $event.target)\">\n                        <svg class=\"icon m-2\" aria-hidden=\"true\" focusable=\"false\">\n                            <use xlink:href=\"#icon-delete\"></use>\n\n                        </svg>\n\n                        <span class=\"access-label\">Delete</span>\n\n                    </b-button>\n\n                </template>\n\n            </b-table>\n\n            <!-- Modal Edit Dialog box-->\n            <b-modal v-model=\"devs.showEdit\" id=\"modal1\" title=\"Device Edit\" header-bg-variant=\"primary\"\n                header-text-variant=\"light\" @ok=\"modalEditOk\" modal-ok=\"Save\">\n                <b-form>\n                    <div v-for=\"(column, index) in devs.cols\" :key=\"column.key\">\n                        <b-form-group v-if=\"column.key !== 'actions'\" label-cols=\"3\" :id=\"'mefg-'+index\"\n                            :label=\"column.label+': '\" :label-for=\"'input-'+index\"\n                            :description=\"(column.description||'')+' e.g. '+column.eg\">\n                            <b-form-input :id=\"'input-'+index\" v-model=\"editFrm[column.key]\" lazy trim\n                                :type=\"column.dataType || 'text'\" :readonly=\"column.editable === true ? false : true\">\n\n                            </b-form-input>\n                            <!--<b-form-input :id=\"'input-'+index\"\n                                @input=\"frmEdit('input', index, column, $event.target)\"\n                                @change=\"frmEdit('change', index, column, $event.target)\"\n                                v-model=\"frmCol('model', index, column, $event.target)\"\n                                lazy trim\n                                :type=\"column.dataType || 'text'\"\n                                :readonly=\"column.editable === true ? false : true\"\n                            >-->\n                            </b-form-input>\n\n                        </b-form-group>\n\n                        <!--<p v-else>{{column.key}} :: {{editFrm[column.key]}}</p>-->\n                    </div>\n\n                </b-form>\n\n            </b-modal>\n\n        </b-container>\n\n    </div>\n\n    <script src=\"../uibuilder/vendor/socket.io/socket.io.js\"></script>\n    <script src=\"../uibuilder/vendor/vue/dist/vue.js\"></script>\n    <script src=\"../uibuilder/vendor/bootstrap-vue/dist/bootstrap-vue.js\"></script>\n    <script src=\"./uibuilderfe.min.js\"></script>\n    <script src=\"./index.js\"></script>\n\n</body>\n\n</html>","x":180,"y":840,"wires":[]},{"id":"f4b92b1dd6d1c780","type":"comment","z":"cd281ebd2c4a291e","name":"index.js","info":"/* jshint browser: true, esversion: 5, asi: true */\n/*globals Vue, uibuilder */\n// @ts-nocheck\n/*\nCopyright (c) 2020 Julian Knight (Totally Information)\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\nhttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n'use strict'\n\n/** @see\nhttps://github.com/TotallyInformation/node-red-contrib-uibuilder/wiki/Front-End-Library---available-properties-and-methods\n*/\n\n// eslint-disable-next-line no-unused-vars\nvar vueApp = new Vue({\n    el: '#app',\n    data: {\n\n        // Devices table\n        devs: {\n            items: [], // table content\n            cols: [], // column definitions\n            key: '', // primary key field name\n            headVariant: 'dark',\n            headRowVariant: 'primary',\n            busy: false, // Table busy?\n            perPage: 10,\n            currentPage: 1,\n            totalRows: 1,\n            filter: null,\n            showEdit: false,\n            inlineEdit: true, // Allow edits of editable fields in-line?\n            /** Holds visible items from devs.items (b-table v-model)\n            * Indexing on the model matches the index returned by b-table\n            */\n            model: null,\n        },\n\n        editFrm: {},\n\n    }, // --- End of data --- //\n\n    computed: {\n\n        frmCol: function (chg, index, column, event) {\n            console.log({ chg, index, column, event })\n            return devices\n        },\n\n    }, // --- End of computed --- //\n    methods: {\n\n        /** Called from modal form\n        * NOTE: If no params were passed, the system would provide the new value\n        */\n        cellEdit(chg, row, event, devsModel) {\n            console.log({ chg, row, event, devsModel })\n            //devsModel[row.index][row.field.key] = event.target.value\n            uibuilder.send({ topic: 'admin/change', payload: devsModel[row.index] })\n        }, // --- End cellEdit --- //\n\n        // Trigger pagination to update the number of buttons/pages due to filtering\n        rowDblClicked(item, index, event) {\n            console.log({ item, index, event }, this.devsModel)\n            this.editFrm = item\n            this.devs.showEdit = true // !this.devs.showEdit\n        }, // --- End of rowDblClicked --- //\n\n        /** Handle table edit button\n        * @param {Object} item The data content of the row being edited\n        * @param {number} index The filtered/paginated row index (not the full data index)\n        * @param {Object} event The event object\n        * @param {Object} devsModel Reference to the table v-model data (FILTERED)\n        */\n        tblRowEdit(row, event, devsModel) {\n            /** editFrm referenced by input's in form\n            * maps to table data model.\n            */\n            this.editFrm = row.item\n            this.devs.showEdit = true\n            console.log('Table Row Edit Button Pressed', { row, event, devsModel })\n        }, // --- End of tblRowEdit --- //\n\n        frmEdit: function (chg, index, column, event) {\n            const value = event.target.value\n            console.log({ chg, value, index, column, event })\n            //this.$emit(chg, { ...this.local, [key]: value })\n        }, // --- End frmEdit --- //\n        \n        // Handle OK button on edit modal dialog\n        modalEditOk(bvModalEvt) {\n            console.log('Edit Modal Dialog: OK Button Pressed', 'DATA: ', this.editFrm)\n            // Update main data table\n            this.devs.items[this.editFrm._idx] = this.editFrm\n            // Send to NR\n            uibuilder.send(this.editFrm)\n            // If error, prevent modal from closing\n            //bvModalEvt.preventDefault()\n        }, // --- End of modalEditOk --- //\n\n        // Handle table delete button\n        tblRowDelete(item, index, event) {\n            console.log('Table Row Delete Button Pressed', { item, index, event })\n            this.devs.items = this.devs.items.filter(\n                device => device.id !== item.id\n\n            )\n            uibuilder.send({ topic: 'admin/change', payload: this.devs.items })\n\n        }, // --- End of tblRowDelete --- //\n\n        // Handle table add new button\n        tblItemAdd(data, event) {\n            this.devs.showEdit = true\n            console.log('Table Item Add Button Pressed', { data, event })\n        }, // --- End of tblItemAdd --- //\n\n        // Trigger pagination to update the number of buttons/pages due to filtering\n        onFiltered(filteredItems) {\n            this.devs.totalRows = filteredItems.length\n            this.devs.currentPage = 1\n        }, // --- End of onFiltered --- //\n\n        // Return formatted HTML version of JSON object\n        syntaxHighlight: function (json) {\n            json = JSON.stringify(json, undefined, 4)\n            json = json.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')\n            json =\n                json.replace(/(\"(\\\\u[a-zA-Z0-9]{4}|\\\\[^u]|[^\\\\\"])*\"(\\s*:)?|\\b(true|false|null)\\b|-?\\d+(?:\\.\\d*)?(?:[eE][+\\-]?\\d+)?)/g,\n                    function (match) {\n                        var cls = 'number'\n                        if (/^\"/.test(match)) {\n                            if (/:$/.test(match)) {\n                                cls = 'key'\n                            } else {\n                                cls = 'string'\n                            }\n                        } else if (/true|false/.test(match)) {\n                            cls = 'boolean'\n                        } else if (/null/.test(match)) {\n                            cls = 'null'\n                        }\n                        return '<span class=\"' + cls + '\">' + match + '</span>'\n                    })\n            return json\n        }, // --- End of syntaxHighlight --- //\n\n    }, // --- End of methods --- //\n\n    // Available hooks: init,mounted,updated,destroyed\n    mounted: function () {\n        uibuilder.start()\n\n        var vueApp = this\n\n        // Example of retrieving data from uibuilder\n        vueApp.feVersion = uibuilder.get('version')\n\n        uibuilder.onChange('msg', function (msg) {\n            //console.info('[indexjs:uibuilder.onChange] msg received from Node-RED server:', msg)\n            vueApp.msgRecvd = msg\n\n            // May need to use Vue.set(vm.items, indexOfItem, newValue)\n\n            if (msg.topic === 'network/status') {\n                vueApp.devs.busy = true\n                console.log('NEW NETWORK DATA RECEIVED')\n\n                //#region ------ COLUMNS ---- //\n                // Grab the schema to define the columns\n                if (msg.schema) {\n                    // Convert from object to array if needed\n                    if (Array.isArray(msg.schema)) vueApp.devs.cols = msg.schema\n                    else vueApp.devs.cols = Object.values(msg.schema)\n                }\n\n                // Enrich the columns definitions as required\n                vueApp.devs.cols.forEach(function (column, i, arr) {\n                    // If col not specified as object, make it one\n                    if (!(column != null && column.constructor.name === \"Object\")) column = arr[i] = { 'key': column }\n                    \n                    // Add sortable to column spec unless already specified\n                    if (!column.sortable) column.sortable = true\n                    \n                    // Default headerTitle addrib to key if not present\n                    if (column.label && !column.headerTitle) column.headerTitle = column.key\n                    \n                    // Set primary key for table\n                    if (column.primary) {\n                        vueApp.devs.key = column.key\n                        column.headerTitle += ' [Primary Key]'\n\n                    }\n                    \n                    //TODO Format or dataType specified?\n                    if (column.dataType.toLowerCase() === 'date' && !column.formatter) {\n                        column.dataType = 'text'\n                        column.formatter = function (value, key, item) {\n                            if (value === '') {\n                                var d = new Date('fred')\n                                consonsole.log(d)\n\n                                return ''\n\n                            } else {\n                                var d = new Date(value)\n                                return d.toLocaleString()\n\n                            }\n                        }\n                    }\n\n                    /* \n                    if ( column.format || column.dataType ) {\n\n                    }\n                    */\n\n                    // TODO Editable?\n                    if (column.editable) {\n                        column.headerTitle += ' (EDITABLE)'\n\n                    }\n\n                })\n\n                // If col definitions don't specify a primary key, choose the 1st col\n                if (vueApp.devs.key === '') {\n                    vueApp.devs.key = vueApp.devs.cols[0]\n                    vueApp.devs.cols[0].headerTitle += ' [Primary Key]'\n                }\n\n                // Add actions column WARN: Assumes cols table rebuilt so action col not exist\n                vueApp.devs.cols.push({\n                    key: 'actions',\n                    label: '',\n                    sortable: false,\n                })\n\n                //#endregion ---- COLUMNS ---- //\n\n                //#region ------- DATA ------- //\n\n                // Set the data (convert from object to array if needed)\n                if (Array.isArray(msg.payload)) vueApp.devs.items = msg.payload\n                else vueApp.devs.items = Object.values(msg.payload)\n\n                vueApp.devs.items.forEach(function (row, i) {\n                    // Add an index field to make editing easier when using filtered view\n                    row._idx = i\n\n                })\n\n                // Set row count for pagination\n                vueApp.devs.totalRows = vueApp.devs.items.length\n\n                //#endregion ----- DATA ----- //\n\n                //console.log(vueApp.devCols,vueApp.devices, vueApp.devs.key)\n                vueApp.devs.busy = false\n\n            } // --- End of devices --- //\n\n        }) // ----- End of msg received ----- //\n\n    } // --- End of mounted hook --- //\n\n}) // --- End of vueApp --- //\n\n// EOF","x":350,"y":840,"wires":[]},{"id":"0b492baa66cf45ba","type":"comment","z":"cd281ebd2c4a291e","name":"index.css","info":"/* Cloak elements on initial load to hide the possible display of {{ ... }} \n * Add to the app tag or to specific tags\n * To display \"loading...\", change to the following:\n *    [v-cloak] > * { display:none }\n *    [v-cloak]::before { content: \"loading…\" }\n */\n[v-cloak] { display: none; }\n\n/**\n * Visually hidden accessible label\n * Using styles that do not hide the text in screen readers\n * We use !important because we should not apply other styles to this hidden alternative text\n */\n .access-label {\n    position: absolute !important;\n    width: 1px !important;\n    height: 1px !important;\n    overflow: hidden !important;\n    white-space: nowrap !important;\n  }\n  \n/**\n * Default icon style\n */\n.icon {\n    /* Use the current text color as the icon’s fill color. */\n    fill: currentColor;\n    /* Inherit the text’s size too. Also allows sizing the icon by changing its font-size. */\n    width: 1.2em; height: 1.2em;\n    /* The default vertical-align is `baseline`, which leaves a few pixels of space below the icon. Using `center` prevents this. For icons shown alongside text, you may want to use a more precise value, e.g. `vertical-align: -4px` or `vertical-align: -0.15em`. */\n    vertical-align: middle;\n    /* Paths and strokes that overflow the viewBox can show in IE11. */\n    overflow: hidden;\n}\n\n/*  Colours for Syntax Highlighted pre's */\n.syntax-highlight {color:white;background-color:black;padding:5px 10px;}\n.syntax-highlight > .key {color:#ffbf35}\n.syntax-highlight > .string {color:#5dff39;}\n.syntax-highlight > .number {color:#70aeff;}\n.syntax-highlight > .boolean {color:#b993ff;}","x":520,"y":840,"wires":[]}]

PS I am now off to try the uiBuilder cache node

1 Like

Sorry about that! Just delete it :slight_smile: I was a bit over-enthusiastic with those notices. They were only meant for the original templates not for your own versions of them.

Not at all, you put in the hard work to get the ideas out there. :+1:

Re the cache node, a bit of alternate thinking is required as it works, but sends 2 messages instead of the cached Object in one go (unless I am missing something).

PS the reason the uiBuilder does not update on page load with the original cache function node I think is because the messages OUT of uiBuilder have changed, at least that fixed it for me. (client connect)

It will only do that if you have 2 msg's in the cache. When asked to send the cache (e.g. on a new client connect), it does just that. But you can control the output.

image

If you are using the defaults, only the last msg for each different topic will be sent. Notice as well the extra flag to double-check if the client is really a new one rather than simply a reconnection after Socket.IO lost a connection. (Though that might not currently fully work for the old uibuilderfe client library).

with your flow i should be able to find "new devices" right away, right?
so if i add a device to my network now, for example, it should make itself felt somewhere, right?

Not quite understanding what you are asking.

If you open a web browser tab to a uibuilder managed URL, the uibuilder node will pop out a control msg saying it has connected and is a new client. If you reload an existing browser tab (e.g. press F5), you also get a similar control msg but it should have a property that tells you it is a reconnection (it is a number that counts up on each reconnection).

This is not quite the same as "finding" a new "device". Since you get the new connection for a new browser tab and you could open dozens of those to the same page from the same device and each is treated as a new connection. What should stay the same is that any connection from the same browser profile should get the same user identifier. Even that doesn't uniquely identify a device though since the id is shared by the same browser profile until you close that profile and reopen.

Identifying devices from a browser connection is rather a poor way to treat your users and may even be illegal in some countries. It requires you to collect all sorts of data from the browser/device and quite possibly to abuse a marker of some sort that you leave behind (a cookie or some such).

I think (hope) that he is asking if any devices added to his network will show up in the system, i.e. in New Device Log and in Network Database Management. The answer (if that WAS the question) is yes.

that's exactly what I mean =)
Unfortunately I haven't found a way to display it properly.

Ah, my brain has finally caught up with this thread now!

If you run the nmap command I shared, a new device will show up in the XML file on the next run certainly. As you are immediately passing that XML file to a node-red flow, it will show up there as well.

I'm not entirely certain whether the nmap scan would pick up something that appeared and went away between scans though. Easily tested though. And just set the CRON schedule for a sensible period. I think mine runs every 15minutes.

that doesn't matter :wink: ^^

yes, that actually works quite well.
i just sometimes have the feeling that devices that were already in the network are displayed as new devices... but somehow not always. so it's hard to reproduce.

I have an inject for this. because i don't really need a permanent cron job here.
or what are the advantages for you?
If necessary, I could also trigger the inject node in time...

My main concern is that when a new device is added to the network... WLED, ESP (or whatever) that I don't always have to look for it on the router first.