Npm function @network-utils/arp-lookup weird Promise output

hello, i am using the npm function node to use a function called @network-utils/arp-lookup

this function should have a couple of options to get an ip from the corresponding mac address or a mac address from the corresponding ip address
isMAC(working true/false is returned)
isIP(working true/false is returned)
toMAC(not working "[object Promise]" is returned)
toIP(not working "[object Promise]" is returned)

somehow i can not get it to work because the response is "[object Promise]"

this is the code used in the npm function node

var arp = require('@network-utils/arp-lookup');
var mac = arp.toMAC('192.168.1.5');
msg.payload = mac
return msg

i think it is returning an async message but i have no idea how to get this
await does not work in a function node

Not tested but something like the following should work I think:

var arp = require('@network-utils/arp-lookup');
arp.toMAC('192.168.1.5').then(function(mac){
   msg.payload = mac
   return msg
})

Should that be node.send(msg)?

Nice. Thank you @TotallyInformation and @Colin

with the node.send it works great.

Probably best.

Thanks. Is it possible in node red to use a varaible as a node name

eg,
pass msg.payload.name to a function node and that node get's the name from the variables value

No, but if you want to display that value, you could set it as status instead. See node.status here: https://nodered.org/docs/user-guide/writing-functions

1 Like

yes did that. my idea was to use that for the ip address. i think i am going to use t2 function nodes, one for the ip and another for the name

thanks for the help

By the way, for reference, I now do this a different way. Instead of ARP, I use nmap running from cron as root (every 15 minutes) with output to XML. That gets me all of the active devices. I use a flow to suck in the XML file and consolidate to a master network global variable that uses Node-RED's file system persistence. This has the added advantage of building up a more complete picture of your network over time since it picks up devices that are not always active (like phones and tablets).

You can then query that data in Node-RED.

I'm also building a new UI that lets you edit the master network data to add annotations to it. Using uibuilder of course.

1 Like

nice, care to share your flow.

changed my flow to make it more simple. indeed only downfall is that arp is slow in responding so i would like to use nmap as well

Sure, here is the flow. Actually 2 flows.

One that presents an API url that is called by the script that runs the nmap local scan so that it updates things when the scan finishes. It reads the XML file and combines it into the existing network variable. I initially created that from a spreadsheet that I've been using for years to track the devices on my network manually. It hen sends the data to uibuilder (the second part of the flow), splits the variable object and sends each device to MQTT.

The second part takes the data and dumps it to uibuilder which displays it as an editable table, If the user updates the table, it sends back the changed record which is merged into the networks variable again. A cache is included so that when a new page is loaded, the latest device table is automatically sent through.

In addition to the network variable, there is also a schema object variable. schema.network contains the table schema for bootstrap-vue's table component to use along with some additional metadata for formatting and the like.

[{"id":"e97f9cf0.f86dd","type":"inject","z":"d0860be6.7951b8","name":"","topic":"","payload":"","payloadType":"str","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":75,"y":320,"wires":[["e16eeebb.792f8"]],"l":false},{"id":"e16eeebb.792f8","type":"file in","z":"d0860be6.7951b8","name":"","filename":"/tmp/nmap.xml","format":"utf8","chunk":false,"sendError":false,"encoding":"none","x":320,"y":320,"wires":[["75a84cfb.fcccb4"]]},{"id":"75a84cfb.fcccb4","type":"xml","z":"d0860be6.7951b8","name":"","property":"payload","attr":"","chr":"","x":470,"y":320,"wires":[["35baaad9.7a1f06"]]},{"id":"35baaad9.7a1f06","type":"function","z":"d0860be6.7951b8","name":"Update networks global var","func":"const network = global.get('network','file')\n\nconst newnetwork = {}\n\n// arr.forEach(callback(currentValue [, index [, array]])[, thisArg])\nmsg.payload.nmaprun.host.forEach( (host) => {\n    let mac = ''; vendor = ''\n    \n    // Should only be for localhost because nmap doesn't report the mac address for that\n    if (! host.address[1]) {\n        if ( host.address[0].$.addr !== '192.168.1.191' ) {\n            node.warn(host)\n            return\n        } else {\n            mac = '28:D2:44:69:FD:D9'\n            vendor = 'Lenovo'\n        }\n    } else {\n        mac = host.address[1].$.addr.toUpperCase()\n        vendor = host.address[1].$.vendor\n    }\n    \n    // Create the entry if it doesn't exist\n    if ( ! network.hasOwnProperty(mac) ) {\n        // ID = mac for anything coming from nmap\n        network[mac] = { id: mac, 'mac': mac }\n        newnetwork[mac] = host\n    }\n    \n    // Record latest IP address\n    network[mac].ipaddr = host.address[0].$.addr\n    // and vendor\n    network[mac].vendor = vendor\n    // Add a JS timestamp for when this was run \n    network[mac].updated = new Date(Number.parseInt(msg.payload.nmaprun.$.start, 10)*1000).toLocaleString()\n    network[mac].updatedBy = 'nmap'\n    // Add host name if available\n    if ( host.hostnames[0].hostname ) network[mac].host = host.hostnames[0].hostname[0].$.name\n    // Add latency to host if available\n    // srtt is in text in microseconds so convert to milliseconds. /1000 again to get to seconds \n    if ( host.times ) network[mac].srtt = Number.parseInt(host.times[0].$.srtt, 10)/1000\n\n})\n\n// Vuetify Table\n// network.schema = [\n//     {\"value\": \"mac\", text: 'MAC', dataType: 'string', eg: \"D8:6C:63:5E:41:5F\"},\n//     {\"value\": \"ipaddr\", text: 'IP', dataType: 'string', eg: \"192.168.1.1\"},\n//     {\"value\": \"vendor\", text: 'Vendor', dataType: 'string', eg: \"Expressif\"},\n//     {\"value\": \"updated\", text: 'Updated', dataType: 'Date', eg: 1581869007000},\n//     {\"value\": \"updatedBy\", text: 'By', dataType: 'string', eg: 'nmap'},\n//     {\"value\": \"srtt\", text: 'Latency', dataType: 'integer', eg: 97.269},\n//     {\"value\": \"host\", text: 'Host Name', dataType: 'string', eg: 'pi2.knightnet.co.uk'},\n//     {\"value\": \"hostname\", text: 'Host Descr', dataType: 'string', eg: 'D1M04 - Access Point'},\n//     {\"value\": \"dhcp\", text: 'DHCP', dataType: 'string', eg: 'Yes-Fixed'},\n//     {\"value\": \"description\", text: 'Description', dataType: 'string', eg: \"Some Text\"}\n// ]\n\n/** bootstrap-vue table format. https://bootstrap-vue.js.org/docs/components/table#field-definition-reference\n * BV native: key, label, headerTitle\n * uibuilder: primary, dataType, format, editable, eg\n * SEE COMMENT FOR DETAILS\n **/\n//network.schema = [\nglobal.set('schemas.network', {\n    id         : {\"key\": \"id\", primary: true, label: 'ID', dataType: 'string', \n                    thClass: 'd-none', tdClass: 'd-none', // Don't display this field in tables\n                    eg: \"D8:6C:63:5E:41:5F\"},\n    mac        : {\"key\": \"mac\", label: 'MAC', dataType: 'string', \n                    eg: \"D8:6C:63:5E:41:5F\"},\n    ipaddr     : {\"key\": \"ipaddr\", label: 'IP', dataType: 'text', eg: \"192.168.1.1\"},\n    vendor     : {\"key\": \"vendor\", label: 'Vendor', dataType: 'text', eg: \"Expressif\"},\n    updated    : {\"key\": \"updated\", label: 'Updated', dataType: 'date', eg: 1581869007000},\n    updatedBy  : {\"key\": \"updatedBy\", label: 'By', dataType: 'text', eg: 'nmap'},\n    srtt       : {\"key\": \"srtt\", label: 'Latency', dataType: 'number', eg: 97.269},\n    host       : {\"key\": \"host\", label: 'Host Name', dataType: 'text', eg: 'pi2.knightnet.co.uk'},\n    hostname   : {\"key\": \"hostname\", label: 'Host Descr', dataType: 'text', editable: true,\n                    description: 'Device Description', eg: 'D1M04 - Access Point'},\n    dhcp       : {\"key\": \"dhcp\", label: 'DHCP', dataType: 'text', editable: true, \n                    description: 'Is IP address set from DHCP?', eg: 'Yes-Fixed'},\n    description: {\"key\": \"description\", label: 'Description', dataType: 'text', editable: true, \n                    eg: \"Some Text\"},\n}, 'file')\n\nglobal.set('network', network, 'file')\n\nreturn [{topic: 'network', payload: network}, {topic: 'new-network', payload: newnetwork}];\n","outputs":2,"noerr":0,"x":680,"y":320,"wires":[["99ae73de.4727a","965955f1.964818","36544259.7c139e"],["a4b7b52c.4cc688"]],"inputLabels":["nmap XML converted to JSON"],"outputLabels":["Device master","Newly added devices"],"info":"## Data Schema\n\nObject of objects. Each object's property is a row column.\n\nIf passed as an object of object, MUST be turned into an array for sending to bootstrap-vue's b-table component.\n\nThe column/field schema is written to the global variable: `schema.devices`.\n\n## Column Schema\n\nObject of objects, 1 row for each column in the data array\n\n### bootstrap-vue related\n\n* `key` {string}: column ID\n* `label` {string}: Used in table and edit dialog \n* `headerTitle` {string}: Added to each columns title attrib in the col headers\n\n### uibuilder specific\n\n* `id` {string}: Unique identifier for the row\n* `primary` {boolean}: If true, this column is the primary key that uniquely identifies a data table row\n* `dataType` {string}: HTML 5 `<input>` tag type attribute.\n* `editable` {boolean}: Is this column editable?\n* `eg` {string}: Gives an example of the data in this column\n* `format`\n* `description`\n"},{"id":"99ae73de.4727a","type":"debug","z":"d0860be6.7951b8","name":"Network Device Count","active":false,"tosidebar":true,"console":false,"tostatus":true,"complete":"$count($keys(payload))","targetType":"jsonata","x":1111,"y":340,"wires":[]},{"id":"a4b7b52c.4cc688","type":"debug","z":"d0860be6.7951b8","name":"New Network Devices","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","x":960,"y":400,"wires":[]},{"id":"f3e31d57.9735c","type":"uibuilder","z":"d0860be6.7951b8","name":"","topic":"","url":"admin","fwdInMessages":false,"allowScripts":false,"allowStyles":false,"copyIndex":true,"showfolder":false,"x":330,"y":560,"wires":[["176ef11c.d3b67f"],["d6ce7d82.f7ae3"]]},{"id":"54ffbc11.b95ab4","type":"link in","z":"d0860be6.7951b8","name":"admin-in","links":["d6ce7d82.f7ae3","965955f1.964818"],"x":75,"y":560,"wires":[["baaed2ef.366e9","796970ad.c231a"]]},{"id":"d6ce7d82.f7ae3","type":"link out","z":"d0860be6.7951b8","name":"admin-control-out","links":["54ffbc11.b95ab4"],"x":495,"y":600,"wires":[]},{"id":"baaed2ef.366e9","type":"function","z":"d0860be6.7951b8","name":"Cache","func":"\nif ( msg.uibuilderCtrl === 'ready for content' && msg.cacheControl === 'REPLAY' && msg.from === 'client' ) {\n    /** When a client loads or reloads, send the latest data \n     * NOTE that we send data/schema as an array of objects not an object\n     *      because that is what bootstrap-vue's b-table component requires\n     */\n    return {\n        topic: 'network',\n        payload: Object.values(global.get('network','file')),\n        // Include the schema so that tables will rend correctly\n        schema: Object.values(global.get('schemas.network','file')),\n        // Only send to the required client\n        _socketId: msg._socketId,\n    }\n} else {\n    /** Send the input msg to all connected clients */\n    // Add the schema if required so that tables will rend correctly\n    if (msg.topic === 'network') msg.schema = Object.values(global.get('schemas.network','file'))\n    return msg\n}","outputs":1,"noerr":0,"x":170,"y":560,"wires":[["f3e31d57.9735c"]]},{"id":"796970ad.c231a","type":"debug","z":"d0860be6.7951b8","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","x":170,"y":520,"wires":[]},{"id":"965955f1.964818","type":"link out","z":"d0860be6.7951b8","name":"network-out","links":["54ffbc11.b95ab4"],"x":930,"y":340,"wires":[],"inputLabels":["devices object"],"l":true},{"id":"176ef11c.d3b67f","type":"function","z":"d0860be6.7951b8","name":"Upd network global var","func":"const network = global.get('network', 'file')\n\ndelete network[0]\n\nconst mac = msg.payload.mac\n\ndelete msg.payload._idx\n\nnetwork[mac] = msg.payload\n\nglobal.set('network', network, 'file')\n\nreturn msg","outputs":1,"noerr":0,"x":580,"y":540,"wires":[[]]},{"id":"36544259.7c139e","type":"split","z":"d0860be6.7951b8","name":"","splt":"\\n","spltType":"str","arraySplt":1,"arraySpltType":"len","stream":false,"addname":"","x":910,"y":280,"wires":[["23cf5dd4.ee91f2"]],"info":"Split the devices object into a msg for each device for sending to MQTT."},{"id":"bf65f0bf.66113","type":"comment","z":"d0860be6.7951b8","name":"Update Current Network List","info":"Updated by a cron job to run:\n\n    sudo /home/home/nrmain/system/nmap_scan.sh\n    \nSee `sudo crontab -l` for details","x":160,"y":240,"wires":[]},{"id":"b60f6c72.590b6","type":"comment","z":"d0860be6.7951b8","name":"Show Current Network Device List","info":"","x":200,"y":480,"wires":[]},{"id":"185ba1a6.6563de","type":"http in","z":"d0860be6.7951b8","name":"","url":"/localnetscan","method":"get","upload":false,"swaggerDoc":"","x":130,"y":280,"wires":[["e16eeebb.792f8","740868b4.37e748"]],"info":"API to receive data from executions of imapfilter running on the same device"},{"id":"740868b4.37e748","type":"http response","z":"d0860be6.7951b8","name":"","statusCode":"","headers":{},"x":310,"y":280,"wires":[]},{"id":"5d9cdc1.68ae724","type":"mqtt out","z":"d0860be6.7951b8","name":"nrmain-local","topic":"","qos":"","retain":"","broker":"3784c9f0.57bab6","x":1210,"y":280,"wires":[]},{"id":"23cf5dd4.ee91f2","type":"change","z":"d0860be6.7951b8","name":"topic & retain","rules":[{"t":"set","p":"topic","pt":"msg","to":"'network/'&payload.id","tot":"jsonata"},{"t":"set","p":"retain","pt":"msg","to":"true","tot":"bool"}],"action":"","property":"","from":"","to":"","reg":false,"x":1050,"y":280,"wires":[["5d9cdc1.68ae724"]]},{"id":"3784c9f0.57bab6","type":"mqtt-broker","z":"","name":"nrmain-local","broker":"localhost","port":"1883","clientid":"nrmain-local","usetls":false,"compatmode":false,"keepalive":"60","cleansession":true,"birthTopic":"services/nrmain","birthQos":"0","birthRetain":"true","birthPayload":"Online","closeTopic":"services/nrmain","closeQos":"0","closeRetain":"true","closePayload":"Offline","willTopic":"services/nrmain","willQos":"0","willRetain":"true","willPayload":"Offline"}]

uibuilder code. Very much a work in progress ...

Urm, too big to fit in one post so continued in the next ...

....

html

<!doctype html><html lang="en"><head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <title>Home1: Network Devices</title>
    <meta name="description" content="Home1: Network Devices">

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

    <link href="../uibuilder/vendor/bootstrap/dist/css/bootstrap.min.css" type="text/css" rel="stylesheet" />
    <link href="../uibuilder/vendor/bootstrap-vue/dist/bootstrap-vue.css" type="text/css" rel="stylesheet" />

    <link href="./index.css" media="all" type="text/css" rel="stylesheet" />
</head>
<body>
    <!-- Hidden icon data, <use xlink:href="/path/to/icons.svg#play"></use> if IE not needed -->
    <svg aria-hidden="true" focusable="false" style="display:none">
        <symbol id="icon-edit" viewBox="0 0 576 512">
            <path fill="currentColor" 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"/>
        </symbol>
        <symbol id="icon-delete" viewBox="0 0 448 512">
            <path fill="currentColor" 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"></path>
        </symbol>
    </svg>
    <div id="app" v-cloak>
        <b-container id="app_container" fluid>
            <b-table 
                striped hover small bordered caption-top
                :items="devs.items"         :fields="devs.cols" :primary-key="devs.key"
                :busy="devs.busy"        :head-row-variant="devs.headRowVariant"
                :sort-by="devs.key"        :filter="devs.filter"
                :per-page="devs.perPage" :current-page="devs.currentPage"
                @filtered="onFiltered"   @row-dblclicked="rowDblClicked"
                v-model="devs.model"
            >
                <template v-slot:table-caption>
                    <h2>Network Devices Table</h2>
                    <!-- Filter (search) input - searches whole row - TODO: Add field selector dropdown -->
                    <b-form-group 
                        label="Filter" label-cols-sm="3"
                        label-align-sm="right" label-size="sm"
                        label-for="filterInput" class="mb-0"
                    >
                        <b-input-group size="sm">
                            <b-form-input
                                v-model="devs.filter" type="search"
                                id="filterInput" placeholder="Type to search ..."
                            ></b-form-input>
                        </b-input-group>
                    </b-form-group>
                </template>

                <template v-slot:table-busy>
                    <div class="text-center text-danger my-2">
                    <b-spinner class="align-middle"></b-spinner>
                    <strong>Loading...</strong>
                    </div>
                </template>

                <!-- Custom Header scoped slot -->
                <!--<template v-slot:thead-top="data"><b-tr><b-td :colspan="data.columns">
                </b-td></b-tr></template>-->

                <!-- Custom Footer scoped slot -->
                <template v-slot:custom-foot="data"><b-tr><b-td :colspan="data.columns">
                    <b-container fluid class="m-0 p-0"><b-form-row align-h="between" align-v="center">
                        <b-col>
                            <b-button @click="tblItemAdd(data, $event.target)">
                                Add New Entry
                            </b-button>
                        </b-col>
                        <b-col cols="1">
                            <b-form-input id="devs-rowsperpage" 
                                type="number" v-model="devs.perPage" align="right"
                                title="Rows per page. Use 0 to show whole table."
                            ></b-form-input>
                        </b-col>
                        <b-col cols="3">
                            <b-pagination
                                v-model="devs.currentPage"
                                :total-rows="devs.totalRows"
                                :per-page="devs.perPage"
                                first-number last-number
                                align="right" class="m-0 p-0"
                                title="Select page"
                            ></b-pagination>
                        </b-col>
                    </b-row></b-container>
                    <!--
                    <b-button class="float-left"
                        @click="tblItemAdd(data, $event.target)"
                    >
                        Add New Entry
                    </b-button>
                    <div class="float-right">
                        <b-form-input id="devs-rowsperpage" type="number" v-model="devs.perPage"></b-form-input>
                        <b-pagination
                            v-model="devs.currentPage"
                            :total-rows="devs.totalRows"
                            :per-page="devs.perPage"
                            first-number last-number
                        ></b-pagination>
                    </div>
                    -->
                </b-td></b-tr></template>

                <!-- Default data cell scoped slot (only used if allowing inline edits) -->
                <template v-if="devs.inlineEdit" v-slot:cell()="row">
                    <!--<textarea v-if="row.field.editable"
                        @change="cellEdit('change', row, $event.target, devs.model)"
                    >{{row.value}}</textarea>-->
                    <b-form-textarea v-if="row.field.editable"
                        :id="'inpta-'+row.field.key+'-'+row.index"
                        v-model="row.item[row.field.key]" trim lazy
                        @change="cellEdit('change', row, $event, devs.model)"
                    ></b-form-textarea>
                    <!--<input type="textarea" v-if="row.field.editable" :value="row.value"></input>-->
                    <span v-else>{{ row.value }}</span>
                </template>

                <!-- Actions column -->
                <template v-slot:cell(actions)="row">
                    <!--<b-button variant="outline-secondary" class="p-0" v-b-modal.modal1>
                        <svg class="icon m-2" aria-hidden="true" focusable="false">
                            <use xlink:href="#icon-edit"></use>
                        </svg>
                    </b-button>-->
                    <b-button variant="outline-secondary" class="p-0" 
                        @click="tblRowEdit(row, $event, devs.model)"
                        v-if="!devs.inlineEdit"
                    >
                        <svg class="icon m-2" aria-hidden="true" focusable="false">
                            <use xlink:href="#icon-edit"></use>
                        </svg>
                    </b-button>
                    <span class="access-label">Edit</span>
                    <b-button variant="outline-secondary" class="p-0" @click="tblRowDelete(row.item, row.index, $event.target)">
                        <svg class="icon m-2" aria-hidden="true" focusable="false">
                            <use xlink:href="#icon-delete"></use>
                            </svg>
                        <span class="access-label">Delete</span>
                    </b-button>
                </template>

            </b-table>

            <!-- Modal Edit Dialog box-->
            <b-modal v-model="devs.showEdit" id="modal1"
                title="Device Edit"
                header-bg-variant="primary" header-text-variant="light"
                @ok="modalEditOk" modal-ok="Save"
            >
                <b-form>
                    <div v-for="(column, index) in devs.cols" :key="column.key">
                        <b-form-group v-if="column.key !== 'actions'" 
                            label-cols="3"
                            :id="'mefg-'+index"
                            :label="column.label+': '" :label-for="'input-'+index"
                            :description="(column.description||'')+' e.g. '+column.eg"
                        >
                            <b-form-input :id="'input-'+index"
                                v-model="editFrm[column.key]" lazy trim
                                :type="column.dataType || 'text'"
                                :readonly="column.editable === true ? false : true"
                            ></b-form-input>
                            <!--<b-form-input :id="'input-'+index"
                                @input="frmEdit('input', index, column, $event.target)"
                                @change="frmEdit('change', index, column, $event.target)"
                                v-model="frmCol('model', index, column, $event.target)"
                                lazy trim
                                :type="column.dataType || 'text'"
                                :readonly="column.editable === true ? false : true"
                            ></b-form-input>-->
                        </b-form-group>    
                        <!--<p v-else>{{column.key}} :: {{editFrm[column.key]}}</p>-->
                    </div>
                </b-form>
            </b-modal>

        </b-container>
    </div>

    <script src="../uibuilder/vendor/socket.io/socket.io.js"></script>
    <script src="../uibuilder/vendor/vue/dist/vue.js"></script>
    <script src="../uibuilder/vendor/bootstrap-vue/dist/bootstrap-vue.js"></script>
    <script src="./uibuilderfe.min.js"></script>
    <script src="./network.js"></script>

</body></html>

javascript

/* jshint browser: true, esversion: 5, asi: true */
/*globals Vue, uibuilder */
// @ts-nocheck
/*
  Copyright (c) 2020 Julian Knight (Totally Information)

  Licensed under the Apache License, Version 2.0 (the "License");
  you may not use this file except in compliance with the License.
  You may obtain a copy of the License at

  http://www.apache.org/licenses/LICENSE-2.0

  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License.
*/
'use strict'

/** @see https://github.com/TotallyInformation/node-red-contrib-uibuilder/wiki/Front-End-Library---available-properties-and-methods */

// eslint-disable-next-line no-unused-vars
var vueApp = new Vue({
    el: '#app',
    data: {

        // Devices table
        devs: {
            items: [], // table content
            cols: [],  // column definitions
            key: '',   // primary key field name
            headVariant: 'dark',
            headRowVariant: 'primary',
            busy: false, // Table busy?
            perPage: 10,
            currentPage: 1,
            totalRows: 1,
            filter: null,
            showEdit: false,
            inlineEdit: true, // Allow edits of editable fields in-line?
            /** Holds visible items from devs.items (b-table v-model)
             * Indexing on the model matches the index returned by b-table
             */
            model: null,
        },
        editFrm: {},

    }, // --- End of data --- //
    computed: {

        frmCol: function(chg, index, column, event) {
            console.log({chg, index, column, event})
            return devices
        },
        
    }, // --- End of computed --- //
    methods: {

        /** Called from modal form
         * NOTE: If no params were passed, the system would provide the new value
         */
        cellEdit(chg, row, event, devsModel) {
            console.log({chg, row, event, devsModel})
            //devsModel[row.index][row.field.key] = event.target.value
            uibuilder.send({topic: 'admin/change', payload: devsModel[row.index]})
        },

        // Trigger pagination to update the number of buttons/pages due to filtering
        rowDblClicked(item, index, event) {
            console.log({item, index, event}, this.devsModel)
            this.editFrm = item
            this.devs.showEdit = true // !this.devs.showEdit
        }, // --- End of onFiltered --- //

        /** Handle table edit button
         * @param {Object} item The data content of the row being edited
         * @param {number} index The filtered/paginated row index (not the full data index)
         * @param {Object} event The event object
         * @param {Object} devsModel Reference to the table v-model data (FILTERED)
         */
        tblRowEdit(row, event, devsModel) {
            /** editFrm referenced by input's in form
             * maps to table data model.
             */
            this.editFrm = row.item
            this.devs.showEdit = true
            console.log('Table Row Edit Button Pressed', {row, event, devsModel})
        }, // --- End of tblRowEdit --- //
        frmEdit: function(chg, index, column, event) {
            const value = event.target.value
            console.log({chg, value, index, column, event})
            //this.$emit(chg, { ...this.local, [key]: value })
        },
        // Handle OK button on edit modal dialog
        modalEditOk(bvModalEvt) {
            console.log('Edit Modal Dialog: OK Button Pressed', 'DATA: ', this.editFrm)
            // Update main data table
            this.devs.items[this.editFrm._idx] = this.editFrm
            // Send to NR
            uibuilder.send(this.editFrm)
            // If error, prevent modal from closing
            //bvModalEvt.preventDefault()
        }, // --- End of tblItemAdd --- //

        // Handle table edit button
        tblRowDelete(item, index, event) {
            console.log('Table Row Delete Button Pressed', {item, index, event})
        }, // --- End of tblRowDelete --- //

        // Handle table add new button
        tblItemAdd(data, event) {
            this.devs.showEdit = true
            console.log('Table Item Add Button Pressed', {data, event})
        }, // --- End of tblItemAdd --- //

        // Trigger pagination to update the number of buttons/pages due to filtering
        onFiltered(filteredItems) {
            this.devs.totalRows = filteredItems.length
            this.devs.currentPage = 1
        }, // --- End of onFiltered --- //

        // 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 --- //

    // Available hooks: init,mounted,updated,destroyed
    mounted: function(){
        uibuilder.start()

        var vueApp = this

        // Example of retrieving data from uibuilder
        vueApp.feVersion = uibuilder.get('version')

        uibuilder.onChange('msg', function(msg){
            //console.info('[indexjs:uibuilder.onChange] msg received from Node-RED server:', msg)
            vueApp.msgRecvd = msg

            // May need to use Vue.set(vm.items, indexOfItem, newValue)

            if (msg.topic === 'network') {
                vueApp.devs.busy = true
                console.log('NEW NETWORK DATA RECEIVED')

                //#region ------ COLUMNS ---- //
                // Grab the schema to define the columns
                if ( msg.schema ) {
                    // Convert from object to array if needed
                    if ( Array.isArray(msg.schema) ) vueApp.devs.cols = msg.schema
                    else vueApp.devs.cols = Object.values(msg.schema)
                }

                // Enrich the columns definitions as required
                vueApp.devs.cols.forEach(function (column, i, arr) {
                    // If col not specified as object, make it one
                    if ( ! (column != null && column.constructor.name === "Object") ) column = arr[i] = {'key': column}
                    // Add sortable to column spec unless already specified
                    if (! column.sortable) column.sortable = true
                    // Default headerTitle addrib to key if not present
                    if (column.label && !column.headerTitle) column.headerTitle = column.key
                    // Set primary key for table
                    if (column.primary) {
                        vueApp.devs.key = column.key
                        column.headerTitle += ' [Primary Key]'
                    }
                    //TODO Format or dataType specified?
                    if ( column.dataType.toLowerCase() === 'date' && !column.formatter ) {
                        column.dataType = 'text'
                        column.formatter = function(value, key, item) {
                            if (value === '') {
                                var d = new Date('fred')
                                consonsole.log(d)
                                return ''
                            } else {
                                var d = new Date(value)
                                return d.toLocaleString()
                            }
                        }
                    }
                    // if ( column.format || column.dataType ) {

                    // }
                    //TODO Editable?
                    if (column.editable) {
                        column.headerTitle += ' (EDITABLE)'
                    }

                })

                // If col definitions don't specify a primary key, choose the 1st col
                if (vueApp.devs.key === '') {
                    vueApp.devs.key = vueApp.devs.cols[0].key
                    vueApp.devs.cols[0].headerTitle += ' [Primary Key]'
                }

                // Add actions column WARN: Assumes cols table rebuilt so action col not exist
                vueApp.devs.cols.push({
                    key: 'actions',
                    label: '',
                    sortable: false,
                })

                //#endregion ---- COLUMNS ---- //

                //#region ------- DATA ------- //

                // Set the data (convert from object to array if needed)
                if ( Array.isArray(msg.payload) ) vueApp.devs.items = msg.payload
                else vueApp.devs.items = Object.values(msg.payload)
                vueApp.devs.items.forEach(function (row, i) {
                    // Add an index field to make editing easier when using filtered view
                    row._idx = i
                })
                // Set row count for pagination
                vueApp.devs.totalRows = vueApp.devs.items.length

                //#endregion ----- DATA ----- //

                //console.log(vueApp.devCols,vueApp.devices, vueApp.devs.key)
                vueApp.devs.busy = false
            } // --- End of devices --- //

        }) // ----- End of msg received ----- //

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

}) // --- End of vueApp --- //

// EOF

CSS

/* Cloak elements on initial load to hide the possible display of {{ ... }} 
 * Add to the app tag or to specific tags
 * To display "loading...", change to the following:
 *    [v-cloak] > * { display:none }
 *    [v-cloak]::before { content: "loading…" }
 */
[v-cloak] { display: none; }

/**
 * Visually hidden accessible label
 * Using styles that do not hide the text in screen readers
 * We use !important because we should not apply other styles to this hidden alternative text
 */
 .access-label {
    position: absolute !important;
    width: 1px !important;
    height: 1px !important;
    overflow: hidden !important;
    white-space: nowrap !important;
  }
  
/**
 * Default icon style
 */
.icon {
    /* Use the current text color as the icon’s fill color. */
    fill: currentColor;
    /* Inherit the text’s size too. Also allows sizing the icon by changing its font-size. */
    width: 1.2em; height: 1.2em;
    /* 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`. */
    vertical-align: middle;
    /* Paths and strokes that overflow the viewBox can show in IE11. */
    overflow: hidden;
}

/*  Colours for Syntax Highlighted pre's */
.syntax-highlight {color:white;background-color:black;padding:5px 10px;}
.syntax-highlight > .key {color:#ffbf35}
.syntax-highlight > .string {color:#5dff39;}
.syntax-highlight > .number {color:#70aeff;}
.syntax-highlight > .boolean {color:#b993ff;}

And finally, the shell script which is run every 15min starting at 1min past the hour. Has to run as root of course:

#! /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
#
# To run manually:
#   sudo /home/home/nrmain/system/nmap_scan.sh
#
# To run via cron:
#   sudo crontab -e
#       01,16,31,46 * * * * /home/home/nrmain/system/nmap_scan.sh

# 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 'http://localhost:1880/localnetscan'

wow, very complete. will hve to see if i can implement it.

i currently have a script running on my asus router for use as present detection.

i think i only need the scipt and the node-red flow to make this work(do not use ui builder but have info in function nodes in my flow.

It is complex only because I wanted to use the resulting variable as my master list instead of the old spreadsheet. If all you want to do is to merge each nmap run into your existing data then you only need:

image

And the script.

Hopefully you realise that network scans aren't a very good predicter of presence? That's because most mobile devices turn off the WiFi as much as possible to save power. So no recent response from a device doesn't necessarily mean that it is not present.

Yup. Though give it time, you will find a use for uibuilder eventually! :mage: :grinning:

ok, will try it out.

regarding the presence detection. you are right about the power saving but somehow it works great on my router. maybee because i look for a macaddress present. the router behaves different that let's say nmap or arp . as far as i can tell it works even when any iphone goes to sleep

i use this command to scan for macaddresses

wl -i eth1 assoclist;wl -i eth2 assoclist

getting this error on the flow(nmap.xml exists with correct rights)

TypeError: Cannot read property 'hasOwnProperty' of undefined

looking at the functionnode i see something specific to your setup
i changed this by using my ip/mac address from my synology but this does not help

// Should only be for localhost because nmap doesn't report the mac address for that
if (! host.address[1]) {
    if ( host.address[0].$.addr !== '192.168.1.191' ) {
        node.warn(host)
        return
    } else {
        mac = '28:D2:44:69:FD:D9'
        vendor = 'Lenovo'
    }

OK, that's interesting. What's more, you reminded me that there is a Ubiquiti Unifi API node!

That will give me much better data on WiFi attached devices. It will probably give me better presence detection too. I'll try it out.

Oops. Change that to be the IP/MAC of the device from which you are running nmap. :slight_smile:

I probably haven't searched on the right terms in the flows library, but can you tell me which node that would be?

work great

2 Likes