Access Control framework for an RFID/PIN reader

I am working on access control solution that uses a Wiegand RFID reader Keypad I purchased from Aliexpress. The reader communicates with a ESP8266 and MQTT with Node-Red.

So far I have implemented the following:

  • Storing user and user accesses: RFIDs and PINs. Users can identify themselves with either RFID is PIN code.
  • Access control check is the user is blocked or not, restricted in day of the week or time of the date against the current date and time: user can be restricted to certain days, or time of the day
  • Users can have 2 factor identification (actually two identification steps) where the user need RFID and PIN code as well
  • Solution can also identify events, which is similar to users: multiple IDs, days of week and time restriction, but the event can also request a user identification when the user after entering the PIN/RFID for the event also needs to enter his PIN/RFID and the user access level is checked against system's access level.
  • Access logging with success identifications and also errors/failed identification
  • Access log reports

The Arduino sketch is available here: https://github.com/nygma2004/RFID_Wiegand_MQTT There is also a video on how this logic is working: https://youtu.be/YPGb-sdbfdw There is also a follow-up video to this project with PCB and some extra featured in the Node-Red flow: https://youtu.be/ft3YiPXy4ck (flow also updated here).

[{"id":"11ddb64.fa7864a","type":"tab","label":"Access Control","disabled":false,"info":""},{"id":"d8fe437f.279ad","type":"mqtt in","z":"11ddb64.fa7864a","name":"","topic":"rfid/status","qos":"0","datatype":"json","broker":"cea5258a.b34038","x":120,"y":80,"wires":[["f8a5edf2.79438","61cf7214.38a4cc"]]},{"id":"f8a5edf2.79438","type":"debug","z":"11ddb64.fa7864a","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":350,"y":80,"wires":[]},{"id":"d329f426.061f58","type":"mqtt in","z":"11ddb64.fa7864a","name":"","topic":"rfid/event","qos":"0","datatype":"json","broker":"cea5258a.b34038","x":120,"y":140,"wires":[["5a9ff0c6.1f18b"]]},{"id":"5a9ff0c6.1f18b","type":"debug","z":"11ddb64.fa7864a","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":350,"y":140,"wires":[]},{"id":"d1164d3e.5314b","type":"mqtt out","z":"11ddb64.fa7864a","name":"","topic":"rfid/light","qos":"0","retain":"false","broker":"cea5258a.b34038","x":980,"y":240,"wires":[]},{"id":"4cf4ac8e.54aaa4","type":"inject","z":"11ddb64.fa7864a","name":"Sample user database","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"[{\"name\":\"Dad\",\"accesslevel\":5,\"mode\":\"active\",\"daysofweek\":\"0123456\",\"twofactor\":false,\"id\":[{\"rfid\":1962964},{\"rfid\":12373949},{\"pin\":\"2233\"},{\"ibutton\":\"01e32e621600001b\"}]},{\"name\":\"Mom\",\"accesslevel\":3,\"mode\":\"active\",\"daysofweek\":\"0123456\",\"id\":[{\"rfid\":9497996},{\"rfid\":12477083},{\"pin\":\"5555\"},{\"ibutton\":\"01ab337a4e000025\"}]},{\"name\":\"Cleaner\",\"accesslevel\":1,\"mode\":\"active\",\"daysofweek\":\"12345\",\"timefrom\":\"08:00\",\"timeto\":\"18:00\",\"id\":[{\"pin\":\"7623\"}]}]","payloadType":"json","x":160,"y":380,"wires":[["afc0c393.304f"]]},{"id":"afc0c393.304f","type":"change","z":"11ddb64.fa7864a","name":"","rules":[{"t":"set","p":"#:(file)::accesscontrol","pt":"global","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":450,"y":380,"wires":[[]]},{"id":"f3a18c75.020d7","type":"mqtt in","z":"11ddb64.fa7864a","name":"","topic":"rfid/event","qos":"0","datatype":"json","broker":"cea5258a.b34038","x":140,"y":540,"wires":[["97a8523.16d29b","42acab1e.3f0164"]]},{"id":"97a8523.16d29b","type":"function","z":"11ddb64.fa7864a","name":"User ID validation","func":"let now = new Date();\nlet dow = now.getDay().toString();\nlet yyyy = now.getFullYear();\nlet mm = now.getMonth() < 9 ? \"0\" + (now.getMonth() + 1) : (now.getMonth() + 1); // getMonth() is zero-based\nlet dd  = now.getDate() < 10 ? \"0\" + now.getDate() : now.getDate();\nlet hh = now.getHours() < 10 ? \"0\" + now.getHours() : now.getHours();\nlet mmm  = now.getMinutes() < 10 ? \"0\" + now.getMinutes() : now.getMinutes();\nlet ss  = now.getSeconds() < 10 ? \"0\" + now.getSeconds() : now.getSeconds();\nlet millis = now.getTime();\n\nlet accesscontrol = global.get(\"accesscontrol\",\"file\");\nif (accesscontrol===undefined) {\n    node.status({fill:\"red\",shape:\"dot\",text:\"No access data\"});\n    return;\n}\n\n// filter out the ratelimit message and pass them along as errors\nif (msg.payload.type.indexOf(\"rfidratelimit\")>-1) {\n    node.status({fill:\"red\",shape:\"ring\",text:\"Rate limited attempts\"});\n    return [null,{\"topic\": msg.topic, \"payload\": {\"reason\":msg.payload.type}}];\n}\n\n// check which user the code is related to\nfor (let i=0;i<accesscontrol.length;i++) {  // loop through all the users\n    for (let j=0;j<accesscontrol[i].id.length;j++) {  // loop through all the IDs\n        if (accesscontrol[i].id[j][msg.payload.type]!==undefined) {  // check if the type is the same type what was received\n            if (accesscontrol[i].id[j][msg.payload.type]===msg.payload.code) {  // check the ID value\n                // This point the user has been identified\n                if (accesscontrol[i].mode===\"disabled\") {\n                    // User is blocked, deny access\n                    node.status({fill:\"yellow\",shape:\"ring\",text:accesscontrol[i].name+\" blocked\"});\n                    return [null,{\"topic\": msg.topic, \"payload\": {\"reason\":\"blocked\", \"name\":accesscontrol[i].name, \"type\":msg.payload.type, \"code\":msg.payload.code}}];\n                }\n                if (accesscontrol[i].daysofweek.indexOf(dow)===-1) {\n                    // User is blocked for the current day\n                    node.status({fill:\"yellow\",shape:\"ring\",text:accesscontrol[i].name+\" not allowed today\"});\n                    return [null,{\"topic\": msg.topic, \"payload\": {\"reason\":\"dayofweek\", \"name\":accesscontrol[i].name, \"type\":msg.payload.type, \"code\":msg.payload.code}}];\n                }\n                if ((accesscontrol[i].timefrom!==undefined)&&(accesscontrol[i].timeto!==undefined)) {\n                    if ((new Date(yyyy,mm,dd,hh,mmm,ss) < new Date(yyyy,mm,dd,parseInt(accesscontrol[i].timefrom.split(\":\")[0]),parseInt(accesscontrol[i].timefrom.split(\":\")[1]),0)) || (new Date(yyyy,mm,dd,hh,mmm,ss) > new Date(yyyy,mm,dd,parseInt(accesscontrol[i].timeto.split(\":\")[0]),parseInt(accesscontrol[i].timeto.split(\":\")[1]),0))) {\n                    // User is blocked for the current time within the day\n                    node.status({fill:\"yellow\",shape:\"ring\",text:accesscontrol[i].name+\" not allowed at this time\"});\n                    return [null,{\"topic\": msg.topic, \"payload\": {\"reason\":\"timelimit\", \"name\":accesscontrol[i].name, \"type\":msg.payload.type, \"code\":msg.payload.code}}];\n                    }\n                }\n                if (accesscontrol[i].twofactor) {\n                    if (context.get(\"lastcode\")===undefined) {\n                        // this is the first code, save the last code and wait for the next code\n                        context.set(\"lastcode\",millis);\n                        context.set(\"code\",msg.payload.code);\n                        context.set(\"type\",msg.payload.type);\n                        node.status({fill:\"blue\",shape:\"ring\",text:accesscontrol[i].name+\" waiting for second code\"});\n                        return [null,{\"topic\": msg.topic, \"payload\": {\"reason\":\"twofactor\", \"name\":accesscontrol[i].name, \"type\":msg.payload.type, \"code\":msg.payload.code}}];\n                    } else {\n                        // check if the older code did not time out\n                        if (millis-context.get(\"lastcode\")>30000) {\n                            // Earlier code timed out\n                            context.set(\"lastcode\",millis);\n                            context.set(\"code\",msg.payload.code);\n                            context.set(\"type\",msg.payload.type);\n                            node.status({fill:\"blue\",shape:\"ring\",text:accesscontrol[i].name+\" waiting for second code, last timed out\"});\n                            return [null,{\"topic\": msg.topic, \"payload\": {\"reason\":\"twofactor\", \"name\":accesscontrol[i].name, \"type\":msg.payload.type, \"code\":msg.payload.code}}];\n                        } else {\n                            if ((context.get(\"code\")===msg.payload.code) && (context.get(\"type\")===msg.payload.type)) {\n                                // Do not allow the same codes to be used twice\n                                context.set(\"lastcode\",undefined);\n                                context.set(\"code\",undefined);\n                                context.set(\"type\",undefined);\n                                node.status({fill:\"red\",shape:\"ring\",text:accesscontrol[i].name+\" cannot use same code\"});\n                                return [null,{\"topic\": msg.topic, \"payload\": {\"reason\":\"samecode\", \"name\":accesscontrol[i].name, \"type\":msg.payload.type, \"code\":msg.payload.code}}];\n                            } else {\n                                // finally the second code accepted\n                                context.set(\"lastcode\",undefined);\n                                context.set(\"code\",undefined);\n                                context.set(\"type\",undefined);\n                                node.status({fill:\"green\",shape:\"ring\",text:accesscontrol[i].name+\" | \"+msg.payload.type});\n                                return [{\"topic\": msg.topic, \"payload\": {\"name\":accesscontrol[i].name, \"accesslevel\":accesscontrol[i].accesslevel, \"type\":msg.payload.type, \"code\":msg.payload.code}},null];\n                            }\n                        }\n                    }\n                }\n                // Finally everything was correct and access is given\n                // disable the account if it was a one-time access\n                if (accesscontrol[i].mode===\"onetime\") {\n                    accesscontrol[i].mode = \"disabled\";\n                    global.set(\"accesscontrol\",accesscontrol,\"file\");\n                }\n                node.status({fill:\"green\",shape:\"ring\",text:accesscontrol[i].name+\" | \"+msg.payload.type});\n                return [{\"topic\": msg.topic, \"payload\": {\"name\":accesscontrol[i].name, \"accesslevel\":accesscontrol[i].accesslevel, \"type\":msg.payload.type, \"code\":msg.payload.code}},null];\n            }\n        }\n    }\n}\nnode.status({fill:\"red\",shape:\"ring\",text:\"Unknown ID\"});\nreturn [null,{\"topic\": msg.topic, \"payload\": {\"reason\":\"unknown\", \"type\":msg.payload.type, \"code\":msg.payload.code}}];\n","outputs":2,"noerr":0,"initialize":"","finalize":"","x":450,"y":540,"wires":[["e58acf9e.fd5a","58561079.da08e"],["a5e3d3e3.24cab","c1464e2a.143ad"]],"outputLabels":["Valid ID","Invalid ID"]},{"id":"98ff8ef3.200f","type":"inject","z":"11ddb64.fa7864a","name":"Sample event database","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"[{\"name\":\"Garage Door\",\"mode\":\"active\",\"daysofweek\":\"0123456\",\"id\":[{\"pin\":\"12*55\"}]},{\"name\":\"Pump\",\"mode\":\"active\",\"daysofweek\":\"0123456\",\"userconfirmation\":false,\"accesslevel\":1,\"id\":[{\"pin\":\"45*88\"}]}]","payloadType":"json","x":160,"y":420,"wires":[["49ad0a43.963df4"]]},{"id":"49ad0a43.963df4","type":"change","z":"11ddb64.fa7864a","name":"","rules":[{"t":"set","p":"#:(file)::eventcontrol","pt":"global","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":440,"y":420,"wires":[[]]},{"id":"42acab1e.3f0164","type":"function","z":"11ddb64.fa7864a","name":"Event ID validation","func":"let now = new Date();\nlet dow = now.getDay().toString();\nlet yyyy = now.getFullYear();\nlet mm = now.getMonth() < 9 ? \"0\" + (now.getMonth() + 1) : (now.getMonth() + 1); // getMonth() is zero-based\nlet dd  = now.getDate() < 10 ? \"0\" + now.getDate() : now.getDate();\nlet hh = now.getHours() < 10 ? \"0\" + now.getHours() : now.getHours();\nlet mmm  = now.getMinutes() < 10 ? \"0\" + now.getMinutes() : now.getMinutes();\nlet ss  = now.getSeconds() < 10 ? \"0\" + now.getSeconds() : now.getSeconds();\nlet millis = now.getTime();\n\nlet accesscontrol = global.get(\"accesscontrol\",\"file\");\nlet eventcontrol = global.get(\"eventcontrol\",\"file\");\nif (eventcontrol===undefined) {\n    node.status({fill:\"red\",shape:\"dot\",text:\"No event data\"});\n    return;\n}\n\n// filter out the ratelimit message and pass them along as errors\nif (msg.payload.type.indexOf(\"rfidratelimit\")>-1) {\n    node.status({fill:\"red\",shape:\"ring\",text:\"Rate limited attempts\"});\n    return [null,{\"topic\": msg.topic, \"payload\": {\"reason\":msg.payload.type}}];\n}\n\n// check which event the code is related to\nfor (let i=0;i<eventcontrol.length;i++) {  // loop through all the events\n    for (let j=0;j<eventcontrol[i].id.length;j++) {  // loop through all the IDs\n        if (eventcontrol[i].id[j][msg.payload.type]!==undefined) {  // check if the type is the same type what was received\n            if (eventcontrol[i].id[j][msg.payload.type]===msg.payload.code) {  // check the ID value\n                // This point the event has been identified\n                if (eventcontrol[i].mode===\"blocked\") {\n                    // Event is blocked, deny access\n                    node.status({fill:\"yellow\",shape:\"ring\",text:eventcontrol[i].name+\" blocked\"});\n                    return [null,{\"topic\": msg.topic, \"payload\": {\"reason\":\"blocked\", \"name\":eventcontrol[i].name, \"type\":msg.payload.type, \"code\":msg.payload.code}}];\n                }\n                if (eventcontrol[i].daysofweek.indexOf(dow)===-1) {\n                    // Event is blocked for the current day\n                    node.status({fill:\"yellow\",shape:\"ring\",text:eventcontrol[i].name+\" not allowed today\"});\n                    return [null,{\"topic\": msg.topic, \"payload\": {\"reason\":\"dayofweek\", \"name\":eventcontrol[i].name, \"type\":msg.payload.type, \"code\":msg.payload.code}}];\n                }\n                if ((eventcontrol[i].timefrom!==undefined) && (eventcontrol[i].timeto!==undefined)) {\n                    if ((new Date(yyyy,mm,dd,hh,mmm,ss) < new Date(yyyy,mm,dd,parseInt(eventcontrol[i].timefrom.split(\":\")[0]),parseInt(eventcontrol[i].timefrom.split(\":\")[1]),0)) || (new Date(yyyy,mm,dd,hh,mmm,ss) > new Date(yyyy,mm,dd,parseInt(eventcontrol[i].timeto.split(\":\")[0]),parseInt(eventcontrol[i].timeto.split(\":\")[1]),0))) {\n                    // Event is blocked for the current time within the day\n                    node.status({fill:\"yellow\",shape:\"ring\",text:eventcontrol[i].name+\" not allowed at this time\"});\n                    return [null,{\"topic\": msg.topic, \"payload\": {\"reason\":\"timelimit\", \"name\":eventcontrol[i].name, \"type\":msg.payload.type, \"code\":msg.payload.code}}];\n                    }\n                }\n                if (eventcontrol[i].userconfirmation) {\n                    // this even requires a user confirmation, save the current data\n                    context.set(\"lastcode\",millis);\n                    context.set(\"code\",msg.payload.code);\n                    context.set(\"type\",msg.payload.type);\n                    context.set(\"name\",eventcontrol[i].name);\n                    context.set(\"accesslevel\",eventcontrol[i].accesslevel);\n                    node.status({fill:\"blue\",shape:\"ring\",text:eventcontrol[i].name+\" waiting for user confirmation\"});\n                    return [null,{\"topic\": msg.topic, \"payload\": {\"reason\":\"userconfirmation\", \"name\":eventcontrol[i].name, \"type\":msg.payload.type, \"code\":msg.payload.code}}];\n                }\n                node.status({fill:\"green\",shape:\"ring\",text:eventcontrol[i].name+\" | \"+msg.payload.type});\n                return [{\"topic\": msg.topic, \"payload\": {\"name\":eventcontrol[i].name, \"accesslevel\":eventcontrol[i].accesslevel, \"type\":msg.payload.type, \"code\":msg.payload.code}},null];\n            }\n        }\n    }\n}\n// Before bailing out, check if it is a valid user access code we have been waiting for\nif (context.get(\"lastcode\")!==undefined) {\n    if (accesscontrol===undefined) {\n        node.status({fill:\"red\",shape:\"dot\",text:\"No access data\"});\n        return;\n    }\n    // check if the older code did not time out\n    if (millis-context.get(\"lastcode\")>30000) {\n        // Earlier code timed out\n        node.status({fill:\"blue\",shape:\"ring\",text:context.get(\"name\")+\" waiting for second code, last timed out\"});\n        let resp = {\"topic\": msg.topic, \"payload\": {\"reason\":\"twofactor\", \"name\":context.get(\"name\"), \"type\":msg.payload.type, \"code\":msg.payload.code}};\n        context.set(\"lastcode\",undefined);\n        context.set(\"code\",undefined);\n        context.set(\"type\",undefined);\n        context.set(\"name\",undefined);\n        context.set(\"accesslevel\",undefined);\n        return [resp,null];\n    }\n\n    \n    // Check the user access. This code is the copy from the User ID Validation\n    for (let i=0;i<accesscontrol.length;i++) {  // loop through all the users\n        for (let j=0;j<accesscontrol[i].id.length;j++) {  // loop through all the IDs\n            if (accesscontrol[i].id[j][msg.payload.type]!==undefined) {  // check if the type is the same type what was received\n                if (accesscontrol[i].id[j][msg.payload.type]===msg.payload.code) {  // check the ID value\n                    // This point the user has been identified\n                    if (accesscontrol[i].blocked) {\n                        // User is blocked, deny access\n                        node.status({fill:\"yellow\",shape:\"ring\",text:accesscontrol[i].name+\" blocked\"});\n                        return [null,{\"topic\": msg.topic, \"payload\": {\"reason\":\"blocked\", \"name\":accesscontrol[i].name, \"type\":msg.payload.type, \"code\":msg.payload.code}}];\n                    }\n                    if (accesscontrol[i].daysofweek.indexOf(dow)===-1) {\n                        // User is blocked for the current day\n                        node.status({fill:\"yellow\",shape:\"ring\",text:accesscontrol[i].name+\" not allowed today\"});\n                        return [null,{\"topic\": msg.topic, \"payload\": {\"reason\":\"dayofweek\", \"name\":accesscontrol[i].name, \"type\":msg.payload.type, \"code\":msg.payload.code}}];\n                    }\n                    if ((accesscontrol[i].timefrom!==undefined)&&(accesscontrol[i].timeto!==undefined)) {\n                        if ((new Date(yyyy,mm,dd,hh,mmm,ss) < new Date(yyyy,mm,dd,parseInt(accesscontrol[i].timefrom.split(\":\")[0]),parseInt(accesscontrol[i].timefrom.split(\":\")[1]),0)) || (new Date(yyyy,mm,dd,hh,mmm,ss) > new Date(yyyy,mm,dd,parseInt(accesscontrol[i].timeto.split(\":\")[0]),parseInt(accesscontrol[i].timeto.split(\":\")[1]),0))) {\n                        // User is blocked for the current time within the day\n                        node.status({fill:\"yellow\",shape:\"ring\",text:accesscontrol[i].name+\" not allowed at this time\"});\n                        return [null,{\"topic\": msg.topic, \"payload\": {\"reason\":\"timelimit\", \"name\":accesscontrol[i].name, \"type\":msg.payload.type, \"code\":msg.payload.code}}];\n                        }\n                    }\n                    // Check that the user has access to this event\n                    if (accesscontrol[i].accesslevel<context.get(\"accesslevel\")) {\n                        node.status({fill:\"red\",shape:\"ring\",text:accesscontrol[i].name+\" has no access to event\"});\n                        let resp = {\"topic\": msg.topic, \"payload\": {\"reason\":\"noaccess\", \"name\":context.get(\"name\"), \"type\":msg.payload.type, \"code\":msg.payload.code}};\n                        context.set(\"lastcode\",undefined);\n                        context.set(\"code\",undefined);\n                        context.set(\"type\",undefined);\n                        context.set(\"name\",undefined);\n                        context.set(\"accesslevel\",undefined);\n                        return [resp,null];\n                    }\n                    \n                    \n                    // finally use confirmation accepted\n                    node.status({fill:\"green\",shape:\"ring\",text:context.get(\"name\")+\" | \"+context.get(\"type\")});\n                    let resp = {\"topic\": msg.topic, \"payload\": {\"name\":context.get(\"name\"), \"accesslevel\":context.get(\"accesslevel\"), \"type\":context.get(\"type\"), \"code\":context.get(\"code\")}};\n                    context.set(\"lastcode\",undefined);\n                    context.set(\"code\",undefined);\n                    context.set(\"type\",undefined);\n                    context.set(\"name\",undefined);\n                    context.set(\"accesslevel\",undefined);\n                    return [resp,null];\n                }\n            }\n        }\n    }\n}\nnode.status({fill:\"red\",shape:\"ring\",text:\"Unknown Event\"});\nreturn [null,{\"topic\": msg.topic, \"payload\": {\"reason\":\"unknown\", \"type\":msg.payload.type, \"code\":msg.payload.code}}];\n","outputs":2,"noerr":0,"initialize":"","finalize":"","x":450,"y":660,"wires":[["4f40da82.986144"],["c813db56.f1bfe8"]],"outputLabels":["Valid ID","Invalid ID"]},{"id":"c78058a1.914ee8","type":"sqlite","z":"11ddb64.fa7864a","mydb":"1c25415d.b8427f","sqlquery":"msg.topic","sql":"","name":"Access Log","x":2490,"y":580,"wires":[[]]},{"id":"e58acf9e.fd5a","type":"function","z":"11ddb64.fa7864a","name":"SQL Success","func":"var now = new Date();\nvar epoch = now.getTime();\n\nmsg.topic = \"INSERT INTO accesslog (name,result,type,code,epoch) \" +\n        \"VALUES ('\"+msg.payload.name+\"','Success','\"+msg.payload.type+\"','\"+msg.payload.code+\"',\"+epoch+\")\";\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":2020,"y":440,"wires":[["b9d3cd87.b0089"]]},{"id":"4f40da82.986144","type":"function","z":"11ddb64.fa7864a","name":"SQL Success","func":"var now = new Date();\nvar epoch = now.getTime();\n\nmsg.topic = \"INSERT INTO accesslog (name,result,type,code,epoch) \" +\n        \"VALUES ('\"+msg.payload.name+\"','Success','\"+msg.payload.type+\"','\"+msg.payload.code+\"',\"+epoch+\")\";\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":2020,"y":720,"wires":[["b9d3cd87.b0089"]]},{"id":"a5e3d3e3.24cab","type":"switch","z":"11ddb64.fa7864a","name":"Error reason","property":"payload.reason","propertyType":"msg","rules":[{"t":"eq","v":"blocked","vt":"str"},{"t":"eq","v":"dayofweek","vt":"str"},{"t":"eq","v":"timelimit","vt":"str"},{"t":"eq","v":"twofactor","vt":"str"},{"t":"eq","v":"samecode","vt":"str"},{"t":"eq","v":"unknown","vt":"str"},{"t":"eq","v":"rfidratelimit","vt":"str"},{"t":"eq","v":"pinratelimit","vt":"str"}],"checkall":"true","repair":false,"outputs":8,"x":1770,"y":540,"wires":[["65bc7cda.1c9f54"],["27d1c1d8.851e7e"],["555f9287.57480c"],["916420e5.cd59e"],["c3b56b70.ddaff8"],["7003d1ea.a2a49"],["26231be5.032454"],["e490e0.88881f2"]]},{"id":"c813db56.f1bfe8","type":"switch","z":"11ddb64.fa7864a","name":"Error reason","property":"payload.reason","propertyType":"msg","rules":[{"t":"eq","v":"blocked","vt":"str"},{"t":"eq","v":"dayofweek","vt":"str"},{"t":"eq","v":"timelimit","vt":"str"},{"t":"eq","v":"userconfirmation","vt":"str"},{"t":"eq","v":"timeout","vt":"str"},{"t":"eq","v":"noaccess","vt":"str"},{"t":"eq","v":"unknown","vt":"str"},{"t":"eq","v":"rfidratelimit","vt":"str"},{"t":"eq","v":"pinratelimit","vt":"str"}],"checkall":"true","repair":false,"outputs":9,"x":1770,"y":780,"wires":[["65bc7cda.1c9f54"],["27d1c1d8.851e7e"],["555f9287.57480c"],["dfb3874c.b72658"],["55dac3e3.4e05ac"],["431c01e2.960be"],["821b7eab.32579"],["26231be5.032454"],["e490e0.88881f2"]]},{"id":"65bc7cda.1c9f54","type":"function","z":"11ddb64.fa7864a","name":"SQL Blocked","func":"var now = new Date();\nvar epoch = now.getTime();\n\nmsg.topic = \"INSERT INTO accesslog (name,result,type,code,epoch) \" +\n        \"VALUES ('\"+msg.payload.name+\"','Disabled','\"+msg.payload.type+\"','\"+msg.payload.code+\"',\"+epoch+\")\";\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":2010,"y":480,"wires":[["b9d3cd87.b0089"]]},{"id":"27d1c1d8.851e7e","type":"function","z":"11ddb64.fa7864a","name":"SQL Dayofweek","func":"var now = new Date();\nvar epoch = now.getTime();\n\nmsg.topic = \"INSERT INTO accesslog (name,result,type,code,epoch) \" +\n        \"VALUES ('\"+msg.payload.name+\"','Not allowed today','\"+msg.payload.type+\"','\"+msg.payload.code+\"',\"+epoch+\")\";\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":2020,"y":520,"wires":[["b9d3cd87.b0089"]]},{"id":"555f9287.57480c","type":"function","z":"11ddb64.fa7864a","name":"SQL Timelimit","func":"var now = new Date();\nvar epoch = now.getTime();\n\nmsg.topic = \"INSERT INTO accesslog (name,result,type,code,epoch) \" +\n        \"VALUES ('\"+msg.payload.name+\"','Not allowed this time of the day','\"+msg.payload.type+\"','\"+msg.payload.code+\"',\"+epoch+\")\";\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":2020,"y":560,"wires":[["b9d3cd87.b0089"]]},{"id":"916420e5.cd59e","type":"function","z":"11ddb64.fa7864a","name":"SQL Twofactor","func":"var now = new Date();\nvar epoch = now.getTime();\n\nmsg.topic = \"INSERT INTO accesslog (name,result,type,code,epoch) \" +\n        \"VALUES ('\"+msg.payload.name+\"','Waiting for second code','\"+msg.payload.type+\"','\"+msg.payload.code+\"',\"+epoch+\")\";\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":2020,"y":600,"wires":[["b9d3cd87.b0089"]]},{"id":"c3b56b70.ddaff8","type":"function","z":"11ddb64.fa7864a","name":"SQL Samecode","func":"var now = new Date();\nvar epoch = now.getTime();\n\nmsg.topic = \"INSERT INTO accesslog (name,result,type,code,epoch) \" +\n        \"VALUES ('\"+msg.payload.name+\"','Same code used twice','\"+msg.payload.type+\"','\"+msg.payload.code+\"',\"+epoch+\")\";\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":2020,"y":640,"wires":[["b9d3cd87.b0089"]]},{"id":"7003d1ea.a2a49","type":"function","z":"11ddb64.fa7864a","name":"SQL Unknown","func":"var now = new Date();\nvar epoch = now.getTime();\n\nmsg.topic = \"INSERT INTO accesslog (name,result,type,code,epoch) \" +\n        \"VALUES ('','Unknown User','\"+msg.payload.type+\"','\"+msg.payload.code+\"',\"+epoch+\")\";\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":2020,"y":680,"wires":[["b9d3cd87.b0089"]]},{"id":"821b7eab.32579","type":"function","z":"11ddb64.fa7864a","name":"SQL Unknown","func":"var now = new Date();\nvar epoch = now.getTime();\n\nmsg.topic = \"INSERT INTO accesslog (name,result,type,code,epoch) \" +\n        \"VALUES ('','Unknown Event','\"+msg.payload.type+\"','\"+msg.payload.code+\"',\"+epoch+\")\";\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":2020,"y":880,"wires":[["b9d3cd87.b0089"]]},{"id":"dfb3874c.b72658","type":"function","z":"11ddb64.fa7864a","name":"SQL Userconfirmation","func":"var now = new Date();\nvar epoch = now.getTime();\n\nmsg.topic = \"INSERT INTO accesslog (name,result,type,code,epoch) \" +\n        \"VALUES ('\"+msg.payload.name+\"','Waiting for user code','\"+msg.payload.type+\"','\"+msg.payload.code+\"',\"+epoch+\")\";\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":2040,"y":760,"wires":[["b9d3cd87.b0089"]]},{"id":"55dac3e3.4e05ac","type":"function","z":"11ddb64.fa7864a","name":"SQL Timeout","func":"var now = new Date();\nvar epoch = now.getTime();\n\nmsg.topic = \"INSERT INTO accesslog (name,result,type,code,epoch) \" +\n        \"VALUES ('\"+msg.payload.name+\"','User code times out','\"+msg.payload.type+\"','\"+msg.payload.code+\"',\"+epoch+\")\";\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":2010,"y":800,"wires":[["b9d3cd87.b0089"]]},{"id":"431c01e2.960be","type":"function","z":"11ddb64.fa7864a","name":"SQL Noaccess","func":"var now = new Date();\nvar epoch = now.getTime();\n\nmsg.topic = \"INSERT INTO accesslog (name,result,type,code,epoch) \" +\n        \"VALUES ('\"+msg.payload.name+\"','User has no access','\"+msg.payload.type+\"','\"+msg.payload.code+\"',\"+epoch+\")\";\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":2020,"y":840,"wires":[["b9d3cd87.b0089"]]},{"id":"bc1a27c9.a0f028","type":"comment","z":"11ddb64.fa7864a","name":"Table structure","info":"This is the database table statement under SQLite:\n\nCREATE TABLE 'accesslog' ('id' INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, 'name' TEXT, 'result' TEXT, 'type' TEXT, 'code' TEXT, 'epoch' INTEGER, 'timestamp' INTEGER DEFAULT CURRENT_TIMESTAMP)","x":2500,"y":520,"wires":[]},{"id":"7f6207ba.148c38","type":"sqlite","z":"11ddb64.fa7864a","mydb":"1c25415d.b8427f","sqlquery":"msg.topic","sql":"","name":"Access Log","x":2870,"y":680,"wires":[["ed000bd.88b41f8","59b1911c.04151"]]},{"id":"ed000bd.88b41f8","type":"template","z":"11ddb64.fa7864a","name":"Format","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"<table width=\"100%\">\n    <tr><th>Event/User Name</th><th>Result</th><th>Type</th><th>Code</th><th>Event Timestamp</th></tr>\n    {{#payload}}\n        <tr class=\"\">\n            <td>{{name}}</td>\n            <td>{{result}}</td>\n            <td>{{type}}</td>\n            <td>{{code}}</td>\n            <td>{{timestamp}}</td>\n        </tr>\n    {{/payload}}\n</table>\n","output":"str","x":3060,"y":680,"wires":[["a0173cac.3ccdd"]]},{"id":"a0173cac.3ccdd","type":"ui_template","z":"11ddb64.fa7864a","group":"dcf77e46.abcf4","name":"Last 10 Log Events","order":0,"width":0,"height":0,"format":"<div ng-bind-html=\"msg.payload\" height=\"350\" style=\"height: 350px;\"></div>","storeOutMessages":true,"fwdInMessages":true,"resendOnRefresh":false,"templateScope":"local","x":3250,"y":680,"wires":[[]]},{"id":"b9d3cd87.b0089","type":"function","z":"11ddb64.fa7864a","name":"Dummy","func":"\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":2290,"y":620,"wires":[["c78058a1.914ee8","bb6af06e.d2aff"]]},{"id":"bc72bf79.29b76","type":"change","z":"11ddb64.fa7864a","name":"Last 10 events","rules":[{"t":"set","p":"topic","pt":"msg","to":"SELECT * FROM accesslog ORDER BY epoch DESC LIMIT 10","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":2680,"y":680,"wires":[["7f6207ba.148c38"]]},{"id":"bb6af06e.d2aff","type":"delay","z":"11ddb64.fa7864a","name":"","pauseType":"delay","timeout":"500","timeoutUnits":"milliseconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":2490,"y":680,"wires":[["bc72bf79.29b76"]]},{"id":"af5a8976.6884f8","type":"comment","z":"11ddb64.fa7864a","name":"Access Log Report","info":"","x":150,"y":1180,"wires":[]},{"id":"82d196b0.f1f9b8","type":"function","z":"11ddb64.fa7864a","name":"SQL","func":"context.set(msg.topic,msg.payload);\n\nlet d = new Date();\nlet epoch = d.getTime();\nlet fromdate = 0;\nlet enddate = 0;\n\nmsg.topic = \"SELECT * FROM accesslog WHERE 1 \";\n\nif (context.get(\"event\")!==undefined) {\n    if (context.get(\"event\")!==\"\") {\n        msg.topic += \" AND name = '\"+context.get(\"event\")+\"'\";\n    }\n}\n\nif (context.get(\"result\")!==undefined) {\n    if (context.get(\"result\")!==\"\") {\n        msg.topic += \" AND result = '\"+context.get(\"result\")+\"'\";\n    }\n}\n\nif (context.get(\"type\")!==undefined) {\n    if (context.get(\"type\")!==\"\") {\n        msg.topic += \" AND type = '\"+context.get(\"type\")+\"'\";\n    }\n}\n\nif (context.get(\"time\")!==undefined) {\n    switch (context.get(\"time\")) {\n        case 0:\n            fromdate = epoch - 1000*60*60*24;\n            msg.topic = msg.topic + \" AND epoch > \"+fromdate;\n            break;\n        case 1:\n            fromdate = epoch - 1000*60*60*24*7;\n            msg.topic = msg.topic + \" AND epoch > \"+fromdate;\n            break;\n        case 2:\n            fromdate = epoch - 1000*60*60*24*30;\n            msg.topic = msg.topic + \" AND epoch > \"+fromdate;\n            break;\n    }\n}\n\nmsg.topic = msg.topic+ \" ORDER BY id DESC\";\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":1010,"y":1360,"wires":[["893dbea4.9a74","3fd73168.c889fe"]]},{"id":"d9db8a5d.9d0568","type":"inject","z":"11ddb64.fa7864a","name":"","props":[{"p":"payload","v":"","vt":"date"},{"p":"topic","v":"","vt":"string"}],"repeat":"","crontab":"","once":false,"topic":"","payload":"","payloadType":"date","x":820,"y":1520,"wires":[["82d196b0.f1f9b8"]]},{"id":"d5903f58.27417","type":"function","z":"11ddb64.fa7864a","name":"Convert Data","func":"var output = [];\noutput.push({\"All users/events\":\"\"});\nfor (var i = 0; i < msg.payload.length; i++) {\n    if (msg.payload[i].name!==\"\") {\n        obj = {};\n        obj [msg.payload[i].name]=msg.payload[i].name;\n        output.push(obj);\n    }\n}\nmsg.options = output;\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":610,"y":1240,"wires":[["2c9e3595.7fe3ca"]]},{"id":"1c228f3.e237a71","type":"ui_button","z":"11ddb64.fa7864a","name":"","group":"75146709.571928","order":1,"width":"3","height":"1","passthru":false,"label":"Refresh","tooltip":"","color":"","bgcolor":"","icon":"","payload":"","payloadType":"str","topic":"refresh","x":800,"y":1300,"wires":[["82d196b0.f1f9b8"]]},{"id":"258175e.7125a8a","type":"ui_dropdown","z":"11ddb64.fa7864a","name":"Result","label":"","tooltip":"Filter on result","place":"Filter on result","group":"75146709.571928","order":2,"width":"4","height":"1","passthru":false,"multiple":false,"options":[{"label":"All results","value":"*","type":"str"},{"label":"Information only","value":0,"type":"num"},{"label":"Warnings only","value":1,"type":"num"},{"label":"Errors only","value":2,"type":"num"}],"payload":"","topic":"result","x":790,"y":1340,"wires":[["82d196b0.f1f9b8"]]},{"id":"336608.527d59f8","type":"ui_dropdown","z":"11ddb64.fa7864a","name":"Time filter","label":"","tooltip":"Filter on time","place":"Filter on time","group":"75146709.571928","order":3,"width":"4","height":"1","passthru":false,"multiple":false,"options":[{"label":"Last 24 hrs","value":0,"type":"num"},{"label":"Last 7 days","value":1,"type":"num"},{"label":"Last 30 days","value":2,"type":"num"},{"label":"All","value":3,"type":"num"}],"payload":"","topic":"time","x":800,"y":1460,"wires":[["82d196b0.f1f9b8"]]},{"id":"2c9e3595.7fe3ca","type":"ui_dropdown","z":"11ddb64.fa7864a","name":"Event","label":"","tooltip":"Filter on event/user","place":"Filter on event/user","group":"75146709.571928","order":5,"width":"5","height":"1","passthru":false,"multiple":false,"options":[{"label":"","value":"","type":"str"}],"payload":"","topic":"event","x":790,"y":1240,"wires":[["82d196b0.f1f9b8"]]},{"id":"893dbea4.9a74","type":"sqlite","z":"11ddb64.fa7864a","mydb":"1c25415d.b8427f","sql":"","name":"Access Log","x":1170,"y":1360,"wires":[["a8504d6b.fc97f","58370d5c.ae2d34"]]},{"id":"a8504d6b.fc97f","type":"template","z":"11ddb64.fa7864a","name":"Format","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"<table width=\"100%\">\n    <tr><th>Event/User Name</th><th>Result</th><th>Type</th><th>Code</th><th>Event Timestamp</th></tr>\n    {{#payload}}\n        <tr class=\"\">\n            <td>{{name}}</td>\n            <td>{{result}}</td>\n            <td>{{type}}</td>\n            <td>{{code}}</td>\n            <td>{{timestamp}}</td>\n        </tr>\n    {{/payload}}\n</table>\n","output":"str","x":1340,"y":1360,"wires":[["bcf7a25e.9a46a"]]},{"id":"bcf7a25e.9a46a","type":"ui_template","z":"11ddb64.fa7864a","group":"75146709.571928","name":"Access Log","order":0,"width":0,"height":0,"format":"<div ng-bind-html=\"msg.payload\" height=\"600\" style=\"height: 600px;\"></div>","storeOutMessages":true,"fwdInMessages":true,"resendOnRefresh":false,"templateScope":"local","x":1510,"y":1360,"wires":[[]]},{"id":"90ddabd6.a676e8","type":"sqlite","z":"11ddb64.fa7864a","mydb":"1c25415d.b8427f","sql":"","name":"Access Log","x":410,"y":1240,"wires":[["d5903f58.27417"]]},{"id":"5ebb10f9.1cc1f","type":"change","z":"11ddb64.fa7864a","name":"SQL","rules":[{"t":"set","p":"topic","pt":"msg","to":"SELECT DISTINCT name FROM accesslog ORDER BY name","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":250,"y":1240,"wires":[["90ddabd6.a676e8"]]},{"id":"d3e346e7.9f2ac8","type":"function","z":"11ddb64.fa7864a","name":"Convert Data","func":"var output = [];\noutput.push({\"All results\":\"\"});\nfor (var i = 0; i < msg.payload.length; i++) {\n    if (msg.payload[i].result!==\"\") {\n        obj = {};\n        obj [msg.payload[i].result]=msg.payload[i].result;\n        output.push(obj);\n    }\n}\nmsg.options = output;\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":610,"y":1340,"wires":[["258175e.7125a8a"]]},{"id":"b2f0b8bc.373228","type":"sqlite","z":"11ddb64.fa7864a","mydb":"1c25415d.b8427f","sql":"","name":"Access Log","x":410,"y":1320,"wires":[["d3e346e7.9f2ac8"]]},{"id":"92b431e7.bd4a","type":"change","z":"11ddb64.fa7864a","name":"SQL","rules":[{"t":"set","p":"topic","pt":"msg","to":"SELECT DISTINCT result FROM accesslog ORDER BY result","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":250,"y":1320,"wires":[["b2f0b8bc.373228"]]},{"id":"d129f3ce.a641c","type":"ui_dropdown","z":"11ddb64.fa7864a","name":"Type","label":"","tooltip":"Filter on type","place":"Filter on type","group":"75146709.571928","order":2,"width":"4","height":"1","passthru":false,"multiple":false,"options":[],"payload":"","topic":"type","x":790,"y":1400,"wires":[["82d196b0.f1f9b8"]]},{"id":"eb24ee5d.8df5e","type":"function","z":"11ddb64.fa7864a","name":"Convert Data","func":"var output = [];\noutput.push({\"All types\":\"\"});\nfor (var i = 0; i < msg.payload.length; i++) {\n    if (msg.payload[i].type!==\"\") {\n        obj = {};\n        obj [msg.payload[i].type]=msg.payload[i].type;\n        output.push(obj);\n    }\n}\nmsg.options = output;\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":610,"y":1400,"wires":[["d129f3ce.a641c"]]},{"id":"ae8516bc.7c0028","type":"sqlite","z":"11ddb64.fa7864a","mydb":"1c25415d.b8427f","sql":"","name":"Access Log","x":410,"y":1400,"wires":[["eb24ee5d.8df5e"]]},{"id":"87477186.460fc","type":"change","z":"11ddb64.fa7864a","name":"SQL","rules":[{"t":"set","p":"topic","pt":"msg","to":"SELECT DISTINCT type FROM accesslog ORDER BY type","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":250,"y":1400,"wires":[["ae8516bc.7c0028"]]},{"id":"3affb1a.ca8e74e","type":"inject","z":"11ddb64.fa7864a","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":true,"onceDelay":"1","topic":"","payload":"","payloadType":"date","x":100,"y":1320,"wires":[["5ebb10f9.1cc1f","92b431e7.bd4a","87477186.460fc"]]},{"id":"3fd73168.c889fe","type":"debug","z":"11ddb64.fa7864a","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1160,"y":1440,"wires":[]},{"id":"26231be5.032454","type":"function","z":"11ddb64.fa7864a","name":"SQL RFID Ratelimit","func":"var now = new Date();\nvar epoch = now.getTime();\n\nmsg.topic = \"INSERT INTO accesslog (name,result,type,epoch) \" +\n        \"VALUES ('','Too many RFID attempts','rfid',\"+epoch+\")\";\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":2030,"y":920,"wires":[["b9d3cd87.b0089"]]},{"id":"e490e0.88881f2","type":"function","z":"11ddb64.fa7864a","name":"SQL PIN Ratelimit","func":"var now = new Date();\nvar epoch = now.getTime();\n\nmsg.topic = \"INSERT INTO accesslog (name,result,type,epoch) \" +\n        \"VALUES ('','Too many PIN attempts','pin',\"+epoch+\")\";\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":2030,"y":960,"wires":[["b9d3cd87.b0089"]]},{"id":"58370d5c.ae2d34","type":"function","z":"11ddb64.fa7864a","name":"Store first entry","func":"if (Array.isArray(msg.payload)) {\n    if (msg.payload.length>0) {\n        flow.set(\"lasttype\", msg.payload[0].type);\n        flow.set(\"lastcode\", msg.payload[0].code);\n    }\n}\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":1380,"y":1300,"wires":[[]]},{"id":"7aaa46f3.e33198","type":"comment","z":"11ddb64.fa7864a","name":"User Management","info":"","x":130,"y":1560,"wires":[]},{"id":"a7d27a21.3addb8","type":"function","z":"11ddb64.fa7864a","name":"Convert Data","func":"let accesscontrol = global.get(\"accesscontrol\",\"file\");\n\nvar output = [];\nfor (var i = 0; i < accesscontrol.length; i++) {\n    obj = {};\n    obj [accesscontrol[i].name]=accesscontrol[i].name;\n    output.push(obj);\n}\nmsg.options = output;\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":330,"y":1640,"wires":[["9b286e89.847da","22907fb9.0c665"]]},{"id":"9b286e89.847da","type":"ui_dropdown","z":"11ddb64.fa7864a","name":"User List","label":"","tooltip":"Select user","place":"Select user","group":"be6fecf1.19027","order":1,"width":"5","height":"1","passthru":false,"multiple":false,"options":[{"label":"","value":"","type":"str"}],"payload":"","topic":"user","x":520,"y":1640,"wires":[["b3285263.e3af6"]]},{"id":"63feb0be.a575e","type":"inject","z":"11ddb64.fa7864a","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":true,"onceDelay":"1","topic":"","payload":"","payloadType":"date","x":120,"y":1640,"wires":[["a7d27a21.3addb8"]]},{"id":"b3285263.e3af6","type":"function","z":"11ddb64.fa7864a","name":"Get user data","func":"let accesscontrol = global.get(\"accesscontrol\",\"file\");\n\nvar output = [];\nfor (var i = 0; i < accesscontrol.length; i++) {\n    if (accesscontrol[i].name === msg.payload) {\n        msg.payload = accesscontrol[i];\n        return msg;\n    }\n}","outputs":1,"noerr":0,"initialize":"","finalize":"","x":140,"y":1760,"wires":[["2178aa22.b12836","9b4405f4.d838c8","7c5ed848.5fe898","d9afcaf7.6ece38","4b55fe8a.496c2","72418ae2.587d94","bc81886e.fd46a8","6c030332.9e385c"]]},{"id":"6b14934c.4fe92c","type":"ui_text_input","z":"11ddb64.fa7864a","name":"Name","label":"Name","tooltip":"Name of the user","group":"be6fecf1.19027","order":2,"width":0,"height":0,"passthru":true,"mode":"text","delay":300,"topic":"name","x":550,"y":1760,"wires":[["19320f79.9e15d1"]]},{"id":"63408769.089188","type":"ui_dropdown","z":"11ddb64.fa7864a","name":"Mode","label":"Mode","tooltip":"Disable all access for the user if Blocked","place":"Select option","group":"be6fecf1.19027","order":4,"width":0,"height":0,"passthru":true,"multiple":false,"options":[{"label":"Active","value":"active","type":"str"},{"label":"One-Time","value":"onetime","type":"str"},{"label":"Disabled","value":"disabled","type":"str"}],"payload":"","topic":"mode","x":550,"y":1800,"wires":[["19320f79.9e15d1"]]},{"id":"dff32d83.41d27","type":"ui_text_input","z":"11ddb64.fa7864a","name":"DaysOfWeek","label":"Days Of Week","tooltip":"Days when the user allowed to access: 0 Sunday, 1 Monday, 2 Tuesday...","group":"be6fecf1.19027","order":6,"width":0,"height":0,"passthru":true,"mode":"text","delay":300,"topic":"daysofweek","x":570,"y":1840,"wires":[["19320f79.9e15d1"]]},{"id":"1b93fa41.da48f6","type":"ui_dropdown","z":"11ddb64.fa7864a","name":"2Factor","label":"Two factor authentication","tooltip":"User needs to scan RFID and enter PIN as well","place":"Select option","group":"be6fecf1.19027","order":5,"width":0,"height":0,"passthru":true,"multiple":false,"options":[{"label":"Enabled","value":true,"type":"bool"},{"label":"Disabled","value":false,"type":"bool"}],"payload":"","topic":"twofactor","x":560,"y":1880,"wires":[["19320f79.9e15d1"]]},{"id":"f4e215ba.bae1d8","type":"ui_text_input","z":"11ddb64.fa7864a","name":"Access Level","label":"Access Level","tooltip":"Access level of the user","group":"be6fecf1.19027","order":3,"width":0,"height":0,"passthru":true,"mode":"number","delay":300,"topic":"accesslevel","x":570,"y":1920,"wires":[["19320f79.9e15d1"]]},{"id":"c1d297ed.89f1e8","type":"ui_text_input","z":"11ddb64.fa7864a","name":"Time From","label":"Time From","tooltip":"Start of access within a day","group":"be6fecf1.19027","order":7,"width":0,"height":0,"passthru":true,"mode":"time","delay":300,"topic":"timefrom","x":570,"y":1960,"wires":[["19320f79.9e15d1"]]},{"id":"5d0c0c31.498bf4","type":"ui_text_input","z":"11ddb64.fa7864a","name":"Time To","label":"Time To","tooltip":"End of access within a day","group":"be6fecf1.19027","order":8,"width":0,"height":0,"passthru":true,"mode":"time","delay":300,"topic":"timeto","x":560,"y":2000,"wires":[["19320f79.9e15d1"]]},{"id":"19320f79.9e15d1","type":"function","z":"11ddb64.fa7864a","name":"Process changes","func":"context.set(msg.topic,msg.payload);\nlet accesscontrol = global.get(\"accesscontrol\",\"file\");\n\nfunction deleteall() {\n    context.set(\"selectedname\",undefined);\n    context.set(\"name\",undefined);\n    context.set(\"mode\",undefined);\n    context.set(\"twofactor\",undefined);\n    context.set(\"daysofweek\",undefined);\n    context.set(\"timefrom\",undefined);\n    context.set(\"timeto\",undefined);\n}\n\n// create a new user\nif (msg.topic===\"create\") {\n    accesscontrol.push({\"name\": \"New user\", \"accesslevel\": 0, \"mode\": \"active\", \"daysofweek\": \"0123456\", \"twofactor\": false, \"id\": []});\n    global.set(\"accesscontrol\",accesscontrol,\"file\");\n    deleteall();\n    return [{\"topic\": \"create\", \"payload\": accesscontrol},{\"topic\": \"New user\", \"payload\": \"New user has been created\"}];\n}\n\n// update user\nif (msg.topic===\"update\") {\n    if (context.get(\"selectedname\")===undefined) {\n        return [null,{\"topic\": \"Update user\", \"payload\": \"No user selected\"}];\n    }\n    \n    // find the user\n    let i = 0;\n    for (i=0;i<accesscontrol.length;i++) {\n        if (accesscontrol[i].name===context.get(\"selectedname\")) {\n            break;\n        }\n    }\n    \n    if (context.get(\"name\")===undefined) {\n        return [null,{\"topic\": \"Update user\", \"payload\": \"User name cannot be empty\"}];\n    }\n    accesscontrol[i].name = context.get(\"name\");\n    accesscontrol[i].mode = context.get(\"mode\");\n    accesscontrol[i].accesslevel = context.get(\"accesslevel\");\n    accesscontrol[i].twofactor = context.get(\"twofactor\");\n    accesscontrol[i].daysofweek = context.get(\"daysofweek\");\n    if (!isNaN(context.get(\"timefrom\"))) {\n        let mytime = new Date();\n        mytime.setTime(context.get(\"timefrom\"));\n        let hh = mytime.getHours() < 10 ? \"0\" + mytime.getHours() : mytime.getHours();\n        let mmm  = mytime.getMinutes() < 10 ? \"0\" + mytime.getMinutes() : mytime.getMinutes();\n\n        accesscontrol[i].timefrom = hh+\":\"+mmm;\n    } else {\n        accesscontrol[i].timefrom = undefined;\n    }\n    if (!isNaN(context.get(\"timeto\"))) {\n        let mytime = new Date();\n        mytime.setTime(context.get(\"timeto\"));\n        let hh = mytime.getHours() < 10 ? \"0\" + mytime.getHours() : mytime.getHours();\n        let mmm  = mytime.getMinutes() < 10 ? \"0\" + mytime.getMinutes() : mytime.getMinutes();\n\n        accesscontrol[i].timeto = hh+\":\"+mmm;\n    } else {\n        accesscontrol[i].timeto = undefined;\n    }\n    global.set(\"accesscontrol\",accesscontrol,\"file\");\n    deleteall();\n    return [{\"topic\": \"create\", \"payload\": accesscontrol},{\"topic\": \"Update user\", \"payload\": \"User details have been updated\"}];\n    \n}\n\n// delete user\nif (msg.topic===\"delete\") {\n    if (context.get(\"selectedname\")===undefined) {\n        return [null,{\"topic\": \"Delete user\", \"payload\": \"No user selected\"}];\n    }\n    \n    // find the user\n    let i = 0;\n    for (i=0;i<accesscontrol.length;i++) {\n        if (accesscontrol[i].name===context.get(\"selectedname\")) {\n            break;\n        }\n    }\n    accesscontrol.splice(i,1);\n    global.set(\"accesscontrol\",accesscontrol,\"file\");\n    deleteall();\n    return [{\"topic\": \"delete\", \"payload\": accesscontrol},{\"topic\": \"Delete user\", \"payload\": \"Selected user has been deleted\"}];\n    \n}\n\n// add the last scanned/entered ID\nif (msg.topic===\"addlast\") {\n    if (context.get(\"selectedname\")===undefined) {\n        return [null,{\"topic\": \"Add code\", \"payload\": \"No user selected\"}];\n    }\n    \n    // find the user\n    let i = 0;\n    for (i=0;i<accesscontrol.length;i++) {\n        if (accesscontrol[i].name===context.get(\"selectedname\")) {\n            break;\n        }\n    }\n    \n    if (flow.get(\"lasttype\")===undefined || flow.get(\"lasttype\")===\"\") {\n        return [null,{\"topic\": \"Add code\", \"payload\": \"Last code not found\"}];\n    }\n \n    accesscontrol[i].id.push({[flow.get(\"lasttype\")] : flow.get(\"lastcode\")});\n    global.set(\"accesscontrol\",accesscontrol,\"file\");\n    deleteall();\n    return [{\"topic\": \"addlast\", \"payload\": accesscontrol},{\"topic\": \"Add code\", \"payload\": \"Last code added to the selected user\"}];\n}","outputs":2,"noerr":0,"initialize":"","finalize":"","x":990,"y":1760,"wires":[["a7d27a21.3addb8"],["dd5f40c5.b10a3"]]},{"id":"9b4405f4.d838c8","type":"function","z":"11ddb64.fa7864a","name":"Get Data","func":"let data = msg.payload.name;\nif (data!==undefined) {\n    msg.payload = data;\n} else {\n    msg.payload = \"\";\n}\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":380,"y":1760,"wires":[["6b14934c.4fe92c","3176634a.cb8f1c"]]},{"id":"2178aa22.b12836","type":"function","z":"11ddb64.fa7864a","name":"Get Data","func":"let data = msg.payload.timefrom;\nif (data!==undefined) {\n    msg.payload = data;\n} else {\n    msg.payload = \"\";\n}\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":380,"y":1960,"wires":[["c1d297ed.89f1e8"]]},{"id":"7c5ed848.5fe898","type":"function","z":"11ddb64.fa7864a","name":"Get Data","func":"let data = msg.payload.mode;\nif (data!==undefined) {\n    msg.payload = data;\n} else {\n    msg.payload = \"active\";\n}\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":380,"y":1800,"wires":[["63408769.089188"]]},{"id":"d9afcaf7.6ece38","type":"function","z":"11ddb64.fa7864a","name":"Get Data","func":"let data = msg.payload.daysofweek;\nif (data!==undefined) {\n    msg.payload = data;\n} else {\n    msg.payload = false;\n}\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":380,"y":1840,"wires":[["dff32d83.41d27"]]},{"id":"4b55fe8a.496c2","type":"function","z":"11ddb64.fa7864a","name":"Get Data","func":"let data = msg.payload.twofactor;\nif (data!==undefined) {\n    msg.payload = data;\n} else {\n    msg.payload = false;\n}\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":380,"y":1880,"wires":[["1b93fa41.da48f6"]]},{"id":"72418ae2.587d94","type":"function","z":"11ddb64.fa7864a","name":"Get Data","func":"let data = msg.payload.accesslevel;\nif (data!==undefined) {\n    msg.payload = data;\n} else {\n    msg.payload = 0;\n}\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":380,"y":1920,"wires":[["f4e215ba.bae1d8"]]},{"id":"bc81886e.fd46a8","type":"function","z":"11ddb64.fa7864a","name":"Get Data","func":"let data = msg.payload.timeto;\nif (data!==undefined) {\n    msg.payload = data;\n} else {\n    msg.payload = \"\";\n}\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":380,"y":2000,"wires":[["5d0c0c31.498bf4"]]},{"id":"bc081a1c.76d958","type":"ui_button","z":"11ddb64.fa7864a","name":"","group":"be6fecf1.19027","order":0,"width":0,"height":0,"passthru":false,"label":"Create New","tooltip":"","color":"","bgcolor":"","icon":"add","payload":"","payloadType":"str","topic":"create","x":810,"y":2000,"wires":[["19320f79.9e15d1"]]},{"id":"a4ae5cdf.e09cf","type":"ui_button","z":"11ddb64.fa7864a","name":"","group":"be6fecf1.19027","order":0,"width":0,"height":0,"passthru":false,"label":"Update selected","tooltip":"","color":"","bgcolor":"#FF9000","icon":"mode_edit","payload":"","payloadType":"str","topic":"update","x":818,"y":1956,"wires":[["19320f79.9e15d1"]]},{"id":"82854f51.94319","type":"ui_button","z":"11ddb64.fa7864a","name":"","group":"be6fecf1.19027","order":0,"width":0,"height":0,"passthru":false,"label":"Delete selected","tooltip":"","color":"","bgcolor":"#ff5555","icon":"delete","payload":"","payloadType":"str","topic":"delete","x":819,"y":1908,"wires":[["19320f79.9e15d1"]]},{"id":"dd5f40c5.b10a3","type":"ui_toast","z":"11ddb64.fa7864a","position":"top right","displayTime":"3","highlight":"","sendall":false,"outputs":0,"ok":"OK","cancel":"","raw":false,"topic":"","name":"","x":1180,"y":1840,"wires":[]},{"id":"3176634a.cb8f1c","type":"function","z":"11ddb64.fa7864a","name":"Save selection","func":"msg.topic = \"selectedname\";\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":580,"y":1720,"wires":[["19320f79.9e15d1"]]},{"id":"22907fb9.0c665","type":"function","z":"11ddb64.fa7864a","name":"Null screen","func":"\nreturn [{\"topic\": \"clear\", \"payload\": \"\"}];","outputs":1,"noerr":0,"initialize":"","finalize":"","x":370,"y":1720,"wires":[["6b14934c.4fe92c","63408769.089188","dff32d83.41d27","1b93fa41.da48f6","f4e215ba.bae1d8","c1d297ed.89f1e8","5d0c0c31.498bf4"]]},{"id":"8881e4ae.0b8688","type":"comment","z":"11ddb64.fa7864a","name":"Delete Old Access Logs","info":"","x":140,"y":1040,"wires":[]},{"id":"58acab1e.432594","type":"function","z":"11ddb64.fa7864a","name":"Delete older than 60 days","func":"var d = new Date();\nvar epoch = d.getTime();\n\n// today - 60 days\nvar fromdate = epoch - 1000*60*60*24*60;\n\nmsg.topic = \"DELETE * FROM accesslog WHERE epoch < \"+fromdate;\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":350,"y":1100,"wires":[["a896741a.d61798"]]},{"id":"21f673e4.94b22c","type":"inject","z":"11ddb64.fa7864a","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"30 03 * * *","once":false,"onceDelay":"","topic":"","payload":"","payloadType":"date","x":130,"y":1100,"wires":[["58acab1e.432594"]]},{"id":"a896741a.d61798","type":"sqlite","z":"11ddb64.fa7864a","mydb":"1c25415d.b8427f","sql":"","name":"Access Log","x":570,"y":1100,"wires":[["ddd7836d.b4b65"]]},{"id":"49cdb3e8.a3e43c","type":"change","z":"11ddb64.fa7864a","name":"1s red","rules":[{"t":"set","p":"payload","pt":"msg","to":"{ \"color1\": \"red\", \"color2\": \"black\", \"blink1\": 1000, \"blink2\": 0, \"duration\": 1000 }","tot":"json"}],"action":"","property":"","from":"","to":"","reg":false,"x":370,"y":240,"wires":[["d1164d3e.5314b"]]},{"id":"cae1f1c8.26d9f","type":"change","z":"11ddb64.fa7864a","name":"5s blinking green","rules":[{"t":"set","p":"payload","pt":"msg","to":"{ \"color1\": \"green\", \"color2\": \"black\", \"blink1\": 500, \"blink2\": 500, \"duration\": 5000 }","tot":"json"}],"action":"","property":"","from":"","to":"","reg":false,"x":410,"y":280,"wires":[["d1164d3e.5314b"]]},{"id":"b73bc38.5774b4","type":"change","z":"11ddb64.fa7864a","name":"1.5s quick yellow","rules":[{"t":"set","p":"payload","pt":"msg","to":"{\"color1\":\"yellow\",\"color2\":\"black\",\"blink1\":250,\"blink2\":250,\"duration\":1500}","tot":"json"}],"action":"","property":"","from":"","to":"","reg":false,"x":410,"y":320,"wires":[["d1164d3e.5314b"]]},{"id":"8add569b.1f8e78","type":"inject","z":"11ddb64.fa7864a","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":140,"y":240,"wires":[["49cdb3e8.a3e43c"]]},{"id":"77484a7.ab0f8b4","type":"inject","z":"11ddb64.fa7864a","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":140,"y":280,"wires":[["cae1f1c8.26d9f"]]},{"id":"63bb89dc.c07ed8","type":"inject","z":"11ddb64.fa7864a","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":140,"y":320,"wires":[["b73bc38.5774b4"]]},{"id":"58561079.da08e","type":"change","z":"11ddb64.fa7864a","name":"Long green","rules":[{"t":"set","p":"payload","pt":"msg","to":"{ \"color1\": \"green\", \"color2\": \"black\", \"blink1\": 2000, \"blink2\": 0, \"duration\": 2000 }","tot":"json"}],"action":"","property":"","from":"","to":"","reg":false,"x":710,"y":460,"wires":[["d1164d3e.5314b"]]},{"id":"c1464e2a.143ad","type":"switch","z":"11ddb64.fa7864a","name":"Error reason","property":"payload.reason","propertyType":"msg","rules":[{"t":"eq","v":"twofactor","vt":"str"},{"t":"else"}],"checkall":"true","repair":false,"outputs":2,"x":690,"y":580,"wires":[["39b5bb8e.42abf4"],["326b1d52.208552"]]},{"id":"39b5bb8e.42abf4","type":"change","z":"11ddb64.fa7864a","name":"5s quick yellow","rules":[{"t":"set","p":"payload","pt":"msg","to":"{\"color1\":\"yellow\",\"color2\":\"black\",\"blink1\":250,\"blink2\":250,\"duration\":5000}","tot":"json"}],"action":"","property":"","from":"","to":"","reg":false,"x":900,"y":580,"wires":[["d1164d3e.5314b"]]},{"id":"326b1d52.208552","type":"change","z":"11ddb64.fa7864a","name":"Red error","rules":[{"t":"set","p":"payload","pt":"msg","to":"{\"color1\":\"red\",\"color2\":\"black\",\"blink1\":500,\"blink2\":500,\"duration\":2000}","tot":"json"}],"action":"","property":"","from":"","to":"","reg":false,"x":880,"y":620,"wires":[["d1164d3e.5314b"]]},{"id":"6c030332.9e385c","type":"function","z":"11ddb64.fa7864a","name":"Get Data","func":"let data = msg.payload.id;\nif (data===undefined) {\n    msg.payload = \"No IDs\";\n    return msg;\n}\nif (data.length===0) {\n    msg.payload = \"No IDs\";\n    return msg;\n}\nmsg.payload = \"<table><tr><th>Type</th><th>ID</th></tr>\";\nfor (let i=0;i<data.length;i++) {\n    for (const key in data[i]) {\n        msg.payload = msg.payload + \"<tr><td>\" + key + \"</td><td>\" + data[i][key] + \"</td></tr>\";\n    }    \n}\nmsg.payload = msg.payload + \"</table>\";\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":380,"y":2040,"wires":[["b3979914.349348"]]},{"id":"b3979914.349348","type":"ui_template","z":"11ddb64.fa7864a","group":"be6fecf1.19027","name":"","order":11,"width":"6","height":"4","format":"<div ng-bind-html=\"msg.payload\"></div>","storeOutMessages":true,"fwdInMessages":true,"resendOnRefresh":true,"templateScope":"local","x":560,"y":2040,"wires":[[]]},{"id":"59b1911c.04151","type":"change","z":"11ddb64.fa7864a","name":"Store last ID","rules":[{"t":"set","p":"lasttype","pt":"flow","to":"payload[0].type","tot":"msg"},{"t":"set","p":"lastcode","pt":"flow","to":"payload[0].code","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":3070,"y":620,"wires":[[]]},{"id":"4cecb490.1d40cc","type":"ui_button","z":"11ddb64.fa7864a","name":"Add Last Key","group":"be6fecf1.19027","order":0,"width":0,"height":0,"passthru":false,"label":"Add last ID","tooltip":"Add the last scanned ID to this user","color":"","bgcolor":"","icon":"vpn_key","payload":"","payloadType":"str","topic":"addlast","x":810,"y":2040,"wires":[["19320f79.9e15d1"]]},{"id":"d2a82d9a.70d81","type":"inject","z":"11ddb64.fa7864a","name":"Off","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"0","payloadType":"str","x":150,"y":2160,"wires":[["777e783f.d548f8"]]},{"id":"777e783f.d548f8","type":"mqtt out","z":"11ddb64.fa7864a","name":"","topic":"rfid/relay1","qos":"","retain":"true","broker":"cea5258a.b34038","x":470,"y":2160,"wires":[]},{"id":"caa34d1c.bcb8d","type":"inject","z":"11ddb64.fa7864a","name":"On","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"1","payloadType":"str","x":150,"y":2200,"wires":[["777e783f.d548f8"]]},{"id":"cc65ea2e.d0abe8","type":"inject","z":"11ddb64.fa7864a","name":"Off","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"0","payloadType":"str","x":150,"y":2260,"wires":[["fbd62280.fad55"]]},{"id":"fbd62280.fad55","type":"mqtt out","z":"11ddb64.fa7864a","name":"","topic":"rfid/relay2","qos":"","retain":"true","broker":"cea5258a.b34038","x":470,"y":2260,"wires":[]},{"id":"3ca85816.fa6e58","type":"inject","z":"11ddb64.fa7864a","name":"On","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"1","payloadType":"str","x":150,"y":2300,"wires":[["fbd62280.fad55"]]},{"id":"54ad1891.4adb68","type":"inject","z":"11ddb64.fa7864a","name":"Off","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"0","payloadType":"str","x":150,"y":2360,"wires":[["dad07774.3a3ff8"]]},{"id":"dad07774.3a3ff8","type":"mqtt out","z":"11ddb64.fa7864a","name":"","topic":"rfid/relay3","qos":"","retain":"true","broker":"cea5258a.b34038","x":470,"y":2360,"wires":[]},{"id":"26521151.38215e","type":"inject","z":"11ddb64.fa7864a","name":"On","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"1","payloadType":"str","x":150,"y":2400,"wires":[["dad07774.3a3ff8"]]},{"id":"846930f2.51e8","type":"inject","z":"11ddb64.fa7864a","name":"Off","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"0","payloadType":"str","x":150,"y":2460,"wires":[["d60fa0e8.93914"]]},{"id":"d60fa0e8.93914","type":"mqtt out","z":"11ddb64.fa7864a","name":"","topic":"rfid/relay4","qos":"","retain":"true","broker":"cea5258a.b34038","x":470,"y":2460,"wires":[]},{"id":"4b61b232.c2b49c","type":"inject","z":"11ddb64.fa7864a","name":"On","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"1","payloadType":"str","x":150,"y":2500,"wires":[["d60fa0e8.93914"]]},{"id":"a796aa6f.d62888","type":"mqtt in","z":"11ddb64.fa7864a","name":"","topic":"rfid/input","qos":"0","datatype":"auto","broker":"cea5258a.b34038","x":200,"y":2580,"wires":[["56833959.b9e798"]]},{"id":"56833959.b9e798","type":"debug","z":"11ddb64.fa7864a","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":420,"y":2580,"wires":[]},{"id":"1bf1902.4cecb7","type":"inject","z":"11ddb64.fa7864a","name":"Update","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"1","crontab":"","once":false,"onceDelay":0.1,"topic":"update","payload":"","payloadType":"date","x":620,"y":180,"wires":[["61cf7214.38a4cc"]]},{"id":"61cf7214.38a4cc","type":"function","z":"11ddb64.fa7864a","name":"Time since last update","func":"let temp = context.get(\"store\");\nlet current = new Date();\nmsg.payload = \"No data\";\n\nif (msg.topic===\"update\") {\n    if (temp!==undefined) {\n        current = current - temp;\n        current = Math.floor(current/1000);\n        msg.sinceupdate = current;\n        var minute = Math.floor(current/60);\n        var hour = Math.floor(minute/60);\n        var day = Math.floor(hour/24);\n        if (current>24*60*60) {\n            msg.payload = \"Last update \" + day + \" days, \" + hour%24 + \" hours, \" + minute%60 + \" minutes, \" + current%60 + \" seconds ago\";\n        } else if (current>60*60) {\n            msg.payload = \"Last update \" + hour%24 + \" hours, \" + minute%60 + \" minutes, \" + current%60 + \" seconds ago\";\n        } else if (current>60) {\n            msg.payload = \"Last update \" + minute%60 + \" minutes, \" + current%60 + \" seconds ago\";\n        } else {\n            msg.payload = \"Last update \" + current%60 + \" seconds ago\";\n        }\n        \n        node.status({fill:\"blue\",shape:\"ring\",text:msg.payload});\n        return msg;\n    } else {\n        node.status({fill:\"grey\",shape:\"dot\",text:\"no data\"});\n    }\n} else {\n    context.set(\"store\",current);\n    return msg;\n}\n\n\n\n\n","outputs":1,"noerr":0,"initialize":"","finalize":"","x":780,"y":100,"wires":[[]]},{"id":"77534a6f.10a4d4","type":"mqtt out","z":"11ddb64.fa7864a","name":"","topic":"rfid/pulse1","qos":"","retain":"false","broker":"cea5258a.b34038","x":890,"y":2160,"wires":[]},{"id":"ad978a12.2d9b98","type":"inject","z":"11ddb64.fa7864a","name":"On","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"1","payloadType":"str","x":690,"y":2160,"wires":[["77534a6f.10a4d4"]]},{"id":"3f79fe12.7c47c2","type":"mqtt out","z":"11ddb64.fa7864a","name":"","topic":"rfid/pulse2","qos":"","retain":"false","broker":"cea5258a.b34038","x":890,"y":2220,"wires":[]},{"id":"70e7d04e.742f8","type":"inject","z":"11ddb64.fa7864a","name":"On","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"1","payloadType":"str","x":690,"y":2220,"wires":[["3f79fe12.7c47c2"]]},{"id":"ddd7836d.b4b65","type":"change","z":"11ddb64.fa7864a","name":"SQL","rules":[{"t":"set","p":"topic","pt":"msg","to":"SELECT COUNT(*) FROM accesslog","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":730,"y":1100,"wires":[["7f1e9b4d.01d4f4"]]},{"id":"7f1e9b4d.01d4f4","type":"sqlite","z":"11ddb64.fa7864a","mydb":"1c25415d.b8427f","sql":"","name":"Access Log","x":890,"y":1100,"wires":[["19f4dd24.b86bc3"]]},{"id":"19f4dd24.b86bc3","type":"function","z":"11ddb64.fa7864a","name":"Record Count","func":"node.status({fill:\"blue\",shape:\"ring\",text:msg.payload[0][\"COUNT(*)\"]+\" records\"});\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":1080,"y":1100,"wires":[[]]},{"id":"35df4ca0.fb5384","type":"comment","z":"11ddb64.fa7864a","name":"Event Management","info":"","x":1490,"y":1560,"wires":[]},{"id":"49cc6e08.e7362","type":"function","z":"11ddb64.fa7864a","name":"Convert Data","func":"let eventcontrol = global.get(\"eventcontrol\",\"file\");\n\nvar output = [];\nfor (var i = 0; i < eventcontrol.length; i++) {\n    obj = {};\n    obj [eventcontrol[i].name]=eventcontrol[i].name;\n    output.push(obj);\n}\nmsg.options = output;\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":1690,"y":1640,"wires":[["aa4b65ad.06a6c8","6310c4c1.e7da6c"]]},{"id":"aa4b65ad.06a6c8","type":"ui_dropdown","z":"11ddb64.fa7864a","name":"Event List","label":"","tooltip":"Select event","place":"Select event","group":"4d5b23c0.57e1ac","order":1,"width":"5","height":"1","passthru":false,"multiple":false,"options":[{"label":"","value":"","type":"str"}],"payload":"","topic":"event","x":1880,"y":1640,"wires":[["e5f0a09a.9ce8b"]]},{"id":"2fafdc3.f814124","type":"inject","z":"11ddb64.fa7864a","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":true,"onceDelay":"1","topic":"","payload":"","payloadType":"date","x":1480,"y":1640,"wires":[["49cc6e08.e7362"]]},{"id":"e5f0a09a.9ce8b","type":"function","z":"11ddb64.fa7864a","name":"Get user data","func":"let eventcontrol = global.get(\"eventcontrol\",\"file\");\n\nvar output = [];\nfor (var i = 0; i < eventcontrol.length; i++) {\n    if (eventcontrol[i].name === msg.payload) {\n        msg.payload = eventcontrol[i];\n        return msg;\n    }\n}","outputs":1,"noerr":0,"initialize":"","finalize":"","x":1500,"y":1760,"wires":[["bc4b3fa1.4b0b9","8ffa6da4.99d4a","12b4dc42.f87a84","454cb5d8.20b5dc","4aa1dc3b.689aa4","773248f2.471ba8","fbbc7b.cf710388","6efad0a3.243ec"]]},{"id":"8f18a8d0.95f3b8","type":"ui_text_input","z":"11ddb64.fa7864a","name":"Name","label":"Name","tooltip":"Name of the user","group":"4d5b23c0.57e1ac","order":2,"width":0,"height":0,"passthru":true,"mode":"text","delay":300,"topic":"name","x":1910,"y":1760,"wires":[["e332cf2b.b133d"]]},{"id":"8ae8c5c0.7708e8","type":"ui_dropdown","z":"11ddb64.fa7864a","name":"Mode","label":"Mode","tooltip":"Disable all access for the event if Blocked","place":"Select option","group":"4d5b23c0.57e1ac","order":4,"width":0,"height":0,"passthru":true,"multiple":false,"options":[{"label":"Active","value":"active","type":"str"},{"label":"Blocked","value":"blocked","type":"str"}],"payload":"","topic":"mode","x":1910,"y":1800,"wires":[["e332cf2b.b133d"]]},{"id":"9d257b32.0d0478","type":"ui_text_input","z":"11ddb64.fa7864a","name":"DaysOfWeek","label":"Days Of Week","tooltip":"Days when the event allowed to access: 0 Sunday, 1 Monday, 2 Tuesday...","group":"4d5b23c0.57e1ac","order":6,"width":0,"height":0,"passthru":true,"mode":"text","delay":300,"topic":"daysofweek","x":1930,"y":1840,"wires":[["e332cf2b.b133d"]]},{"id":"28f63a7f.4e2856","type":"ui_dropdown","z":"11ddb64.fa7864a","name":"User Confirmation","label":"User Confirmation","tooltip":"User needs to autheticate themselves with his/her own id","place":"Select option","group":"4d5b23c0.57e1ac","order":5,"width":0,"height":0,"passthru":true,"multiple":false,"options":[{"label":"Enabled","value":true,"type":"bool"},{"label":"Disabled","value":false,"type":"bool"}],"payload":"","topic":"userconfirmation","x":1950,"y":1880,"wires":[["e332cf2b.b133d"]]},{"id":"93092c4e.21669","type":"ui_text_input","z":"11ddb64.fa7864a","name":"Access Level","label":"Access Level","tooltip":"Access level of the user","group":"4d5b23c0.57e1ac","order":3,"width":0,"height":0,"passthru":true,"mode":"number","delay":300,"topic":"accesslevel","x":1930,"y":1920,"wires":[["e332cf2b.b133d"]]},{"id":"c824ace7.efa96","type":"ui_text_input","z":"11ddb64.fa7864a","name":"Time From","label":"Time From","tooltip":"Start of access within a day","group":"4d5b23c0.57e1ac","order":7,"width":0,"height":0,"passthru":true,"mode":"time","delay":300,"topic":"timefrom","x":1930,"y":1960,"wires":[["e332cf2b.b133d"]]},{"id":"bedc6bcc.24b118","type":"ui_text_input","z":"11ddb64.fa7864a","name":"Time To","label":"Time To","tooltip":"End of access within a day","group":"4d5b23c0.57e1ac","order":8,"width":0,"height":0,"passthru":true,"mode":"time","delay":300,"topic":"timeto","x":1920,"y":2000,"wires":[["e332cf2b.b133d"]]},{"id":"e332cf2b.b133d","type":"function","z":"11ddb64.fa7864a","name":"Process changes","func":"context.set(msg.topic,msg.payload);\nlet eventcontrol = global.get(\"eventcontrol\",\"file\");\n\nfunction deleteall() {\n    context.set(\"selectedname\",undefined);\n    context.set(\"name\",undefined);\n    context.set(\"mode\",undefined);\n    context.set(\"userconfirmation\",undefined);\n    context.set(\"daysofweek\",undefined);\n    context.set(\"timefrom\",undefined);\n    context.set(\"timeto\",undefined);\n}\n\n// create a new user\nif (msg.topic===\"create\") {\n    eventcontrol.push({\"name\": \"New event\", \"accesslevel\": 0, \"mode\": \"active\", \"daysofweek\": \"0123456\", \"userconfirmation\": false, \"id\": []});\n    global.set(\"eventcontrol\",eventcontrol,\"file\");\n    deleteall();\n    return [{\"topic\": \"create\", \"payload\": eventcontrol},{\"topic\": \"New event\", \"payload\": \"New event has been created\"}];\n}\n\n// update event\nif (msg.topic===\"update\") {\n    if (context.get(\"selectedname\")===undefined) {\n        return [null,{\"topic\": \"Update event\", \"payload\": \"No event selected\"}];\n    }\n    \n    // find the event\n    let i = 0;\n    for (i=0;i<eventcontrol.length;i++) {\n        if (eventcontrol[i].name===context.get(\"selectedname\")) {\n            break;\n        }\n    }\n    \n    if (context.get(\"name\")===undefined) {\n        return [null,{\"topic\": \"Update event\", \"payload\": \"User event cannot be empty\"}];\n    }\n    eventcontrol[i].name = context.get(\"name\");\n    eventcontrol[i].mode = context.get(\"mode\");\n    eventcontrol[i].accesslevel = context.get(\"accesslevel\");\n    eventcontrol[i].userconfirmation = context.get(\"userconfirmation\");\n    eventcontrol[i].daysofweek = context.get(\"daysofweek\");\n    if (!isNaN(context.get(\"timefrom\"))) {\n        let mytime = new Date();\n        mytime.setTime(context.get(\"timefrom\"));\n        let hh = mytime.getHours() < 10 ? \"0\" + mytime.getHours() : mytime.getHours();\n        let mmm  = mytime.getMinutes() < 10 ? \"0\" + mytime.getMinutes() : mytime.getMinutes();\n\n        eventcontrol[i].timefrom = hh+\":\"+mmm;\n    } else {\n        eventcontrol[i].timefrom = undefined;\n    }\n    if (!isNaN(context.get(\"timeto\"))) {\n        let mytime = new Date();\n        mytime.setTime(context.get(\"timeto\"));\n        let hh = mytime.getHours() < 10 ? \"0\" + mytime.getHours() : mytime.getHours();\n        let mmm  = mytime.getMinutes() < 10 ? \"0\" + mytime.getMinutes() : mytime.getMinutes();\n\n        eventcontrol[i].timeto = hh+\":\"+mmm;\n    } else {\n        eventcontrol[i].timeto = undefined;\n    }\n    global.set(\"eventcontrol\",eventcontrol,\"file\");\n    deleteall();\n    return [{\"topic\": \"create\", \"payload\": eventcontrol},{\"topic\": \"Update event\", \"payload\": \"Event details have been updated\"}];\n    \n}\n\n// delete event\nif (msg.topic===\"delete\") {\n    if (context.get(\"selectedname\")===undefined) {\n        return [null,{\"topic\": \"Delete event\", \"payload\": \"No event selected\"}];\n    }\n    \n    // find the event\n    let i = 0;\n    for (i=0;i<eventcontrol.length;i++) {\n        if (eventcontrol[i].name===context.get(\"selectedname\")) {\n            break;\n        }\n    }\n    eventcontrol.splice(i,1);\n    global.set(\"eventcontrol\",eventcontrol,\"file\");\n    deleteall();\n    return [{\"topic\": \"delete\", \"payload\": eventcontrol},{\"topic\": \"Delete event\", \"payload\": \"Selected event has been deleted\"}];\n    \n}\n\n// add the last scanned/entered ID\nif (msg.topic===\"addlast\") {\n    if (context.get(\"selectedname\")===undefined) {\n        return [null,{\"topic\": \"Add code\", \"payload\": \"No event selected\"}];\n    }\n    \n    // find the event\n    let i = 0;\n    for (i=0;i<eventcontrol.length;i++) {\n        if (eventcontrol[i].name===context.get(\"selectedname\")) {\n            break;\n        }\n    }\n    \n    if (flow.get(\"lasttype\")===undefined || flow.get(\"lasttype\")===\"\") {\n        return [null,{\"topic\": \"Add code\", \"payload\": \"Last code not found\"}];\n    }\n \n    eventcontrol[i].id.push({[flow.get(\"lasttype\")] : flow.get(\"lastcode\")});\n    global.set(\"eventcontrol\",eventcontrol,\"file\");\n    deleteall();\n    return [{\"topic\": \"addlast\", \"payload\": eventcontrol},{\"topic\": \"Add code\", \"payload\": \"Last code added to the selected event\"}];\n}","outputs":2,"noerr":0,"initialize":"","finalize":"","x":2350,"y":1760,"wires":[["49cc6e08.e7362"],["5f1627fb.ad5fe8"]]},{"id":"8ffa6da4.99d4a","type":"function","z":"11ddb64.fa7864a","name":"Get Data","func":"let data = msg.payload.name;\nif (data!==undefined) {\n    msg.payload = data;\n} else {\n    msg.payload = \"\";\n}\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":1740,"y":1760,"wires":[["8f18a8d0.95f3b8","5ca3976e.3f31a8"]]},{"id":"bc4b3fa1.4b0b9","type":"function","z":"11ddb64.fa7864a","name":"Get Data","func":"let data = msg.payload.timefrom;\nif (data!==undefined) {\n    msg.payload = data;\n} else {\n    msg.payload = \"\";\n}\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":1740,"y":1960,"wires":[["c824ace7.efa96"]]},{"id":"12b4dc42.f87a84","type":"function","z":"11ddb64.fa7864a","name":"Get Data","func":"let data = msg.payload.mode;\nif (data!==undefined) {\n    msg.payload = data;\n} else {\n    msg.payload = \"active\";\n}\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":1740,"y":1800,"wires":[["8ae8c5c0.7708e8"]]},{"id":"454cb5d8.20b5dc","type":"function","z":"11ddb64.fa7864a","name":"Get Data","func":"let data = msg.payload.daysofweek;\nif (data!==undefined) {\n    msg.payload = data;\n} else {\n    msg.payload = false;\n}\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":1740,"y":1840,"wires":[["9d257b32.0d0478"]]},{"id":"4aa1dc3b.689aa4","type":"function","z":"11ddb64.fa7864a","name":"Get Data","func":"let data = msg.payload.userconfirmation;\nif (data!==undefined) {\n    msg.payload = data;\n} else {\n    msg.payload = false;\n}\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":1740,"y":1880,"wires":[["28f63a7f.4e2856"]]},{"id":"773248f2.471ba8","type":"function","z":"11ddb64.fa7864a","name":"Get Data","func":"let data = msg.payload.accesslevel;\nif (data!==undefined) {\n    msg.payload = data;\n} else {\n    msg.payload = 0;\n}\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":1740,"y":1920,"wires":[["93092c4e.21669"]]},{"id":"fbbc7b.cf710388","type":"function","z":"11ddb64.fa7864a","name":"Get Data","func":"let data = msg.payload.timeto;\nif (data!==undefined) {\n    msg.payload = data;\n} else {\n    msg.payload = \"\";\n}\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":1740,"y":2000,"wires":[["bedc6bcc.24b118"]]},{"id":"29533485.d2df0c","type":"ui_button","z":"11ddb64.fa7864a","name":"","group":"4d5b23c0.57e1ac","order":0,"width":0,"height":0,"passthru":false,"label":"Create New","tooltip":"","color":"","bgcolor":"","icon":"add","payload":"","payloadType":"str","topic":"create","x":2170,"y":2000,"wires":[["e332cf2b.b133d"]]},{"id":"17e86287.3c629d","type":"ui_button","z":"11ddb64.fa7864a","name":"","group":"4d5b23c0.57e1ac","order":0,"width":0,"height":0,"passthru":false,"label":"Update selected","tooltip":"","color":"","bgcolor":"#FF9000","icon":"mode_edit","payload":"","payloadType":"str","topic":"update","x":2178,"y":1956,"wires":[["e332cf2b.b133d"]]},{"id":"b5782f17.d1546","type":"ui_button","z":"11ddb64.fa7864a","name":"","group":"4d5b23c0.57e1ac","order":0,"width":0,"height":0,"passthru":false,"label":"Delete selected","tooltip":"","color":"","bgcolor":"#ff5555","icon":"delete","payload":"","payloadType":"str","topic":"delete","x":2179,"y":1908,"wires":[["e332cf2b.b133d"]]},{"id":"5f1627fb.ad5fe8","type":"ui_toast","z":"11ddb64.fa7864a","position":"top right","displayTime":"3","highlight":"","sendall":false,"outputs":0,"ok":"OK","cancel":"","raw":false,"topic":"","name":"","x":2540,"y":1840,"wires":[]},{"id":"5ca3976e.3f31a8","type":"function","z":"11ddb64.fa7864a","name":"Save selection","func":"msg.topic = \"selectedname\";\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":1940,"y":1720,"wires":[["e332cf2b.b133d"]]},{"id":"6310c4c1.e7da6c","type":"function","z":"11ddb64.fa7864a","name":"Null screen","func":"\nreturn [{\"topic\": \"clear\", \"payload\": \"\"}];","outputs":1,"noerr":0,"initialize":"","finalize":"","x":1730,"y":1720,"wires":[["8f18a8d0.95f3b8","8ae8c5c0.7708e8","9d257b32.0d0478","28f63a7f.4e2856","93092c4e.21669","c824ace7.efa96","bedc6bcc.24b118"]]},{"id":"6efad0a3.243ec","type":"function","z":"11ddb64.fa7864a","name":"Get Data","func":"let data = msg.payload.id;\nif (data===undefined) {\n    msg.payload = \"No IDs\";\n    return msg;\n}\nif (data.length===0) {\n    msg.payload = \"No IDs\";\n    return msg;\n}\nmsg.payload = \"<table><tr><th>Type</th><th>ID</th></tr>\";\nfor (let i=0;i<data.length;i++) {\n    for (const key in data[i]) {\n        msg.payload = msg.payload + \"<tr><td>\" + key + \"</td><td>\" + data[i][key] + \"</td></tr>\";\n    }    \n}\nmsg.payload = msg.payload + \"</table>\";\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":1740,"y":2040,"wires":[["46368298.72e9ac"]]},{"id":"46368298.72e9ac","type":"ui_template","z":"11ddb64.fa7864a","group":"4d5b23c0.57e1ac","name":"","order":11,"width":"6","height":"4","format":"<div ng-bind-html=\"msg.payload\"></div>","storeOutMessages":true,"fwdInMessages":true,"resendOnRefresh":true,"templateScope":"local","x":1920,"y":2040,"wires":[[]]},{"id":"1a9d0a65.87fb36","type":"ui_button","z":"11ddb64.fa7864a","name":"Add Last Key","group":"4d5b23c0.57e1ac","order":0,"width":0,"height":0,"passthru":false,"label":"Add last ID","tooltip":"Add the last scanned ID to this user","color":"","bgcolor":"","icon":"vpn_key","payload":"","payloadType":"str","topic":"addlast","x":2170,"y":2040,"wires":[["e332cf2b.b133d"]]},{"id":"cea5258a.b34038","type":"mqtt-broker","z":"","broker":"192.168.1.80","port":"1883","clientid":"node-red","usetls":false,"compatmode":true,"keepalive":"60","cleansession":true,"birthTopic":"","birthQos":"0","birthPayload":"","willTopic":"","willQos":"0","willPayload":""},{"id":"1c25415d.b8427f","type":"sqlitedb","z":0,"db":"/home/pi/sqlite/nodered"},{"id":"dcf77e46.abcf4","type":"ui_group","z":"","name":"Last 10 event log entry","tab":"27b59b20.41e794","order":4,"disp":true,"width":"12","collapse":false},{"id":"75146709.571928","type":"ui_group","z":"","name":"Access Log Report","tab":"27b59b20.41e794","order":3,"disp":true,"width":"12","collapse":false},{"id":"be6fecf1.19027","type":"ui_group","z":"","name":"User Management","tab":"27b59b20.41e794","order":1,"disp":true,"width":"6","collapse":false},{"id":"4d5b23c0.57e1ac","type":"ui_group","z":"","name":"Event Management","tab":"27b59b20.41e794","order":2,"disp":true,"width":"6","collapse":false},{"id":"27b59b20.41e794","type":"ui_tab","z":"","name":"Access Control","icon":"vpn_key","disabled":false,"hidden":false}]

Flow Info

Created 4 years, 1 month ago
Updated 3 years, 8 months ago
Rating: 5 4

Owner

Actions

Rate:

Node Types

Core
  • change (x14)
  • comment (x5)
  • debug (x4)
  • delay (x1)
  • function (x51)
  • inject (x21)
  • mqtt in (x4)
  • mqtt out (x7)
  • mqtt-broker (x1)
  • switch (x3)
  • template (x2)
Other
  • sqlite (x8)
  • sqlitedb (x1)
  • tab (x1)
  • ui_button (x9)
  • ui_dropdown (x10)
  • ui_group (x4)
  • ui_tab (x1)
  • ui_template (x4)
  • ui_text_input (x10)
  • ui_toast (x2)

Tags

  • access
  • control
  • rfid
Copy this flow JSON to your clipboard and then import into Node-RED using the Import From > Clipboard (Ctrl-I) menu option