UNPKG

node-red-contrib-hikvision-ultimate

Version:

A native set of nodes for Hikvision (and compatible) Cameras, Alarms, Radars, NVR, Doorbells, etc.

408 lines (356 loc) 20.9 kB
module.exports = (RED) => { const { createHttpClient } = require('./utils/httpClient'); // const AbortController = require('abort-controller'); const https = require('https'); const fs = require('fs'); const hikvisionDate = require('./utils/dateManagement'); const { XMLParser } = require("fast-xml-parser"); function AccessControlConfig(config) { RED.nodes.createNode(this, config) var node = this node.port = config.port || 80; const rawHost = (config.host || "").toString().toLowerCase(); const hostHasBanana = rawHost.indexOf("banana") > -1; node.debug = (config.debuglevel === "yes") || hostHasBanana; node.host = rawHost.replace("banana", "") + ":" + node.port; node.protocol = config.protocol || "http"; node.nodeClients = []; // Stores the registered clients node.isConnected = true; // Assume it's connected, to signal the disconnection on start node.isClosing = false; node.timerInitACTEventReader = null; node.timerQueryForACTEvents = null; node.lastACTEventSerialNo = 0; // This contains the evend number. It's an unique counter, that the device gives to the event, to didentify it and check for missing events. node.errorDescription = ""; // Contains the error description in case of connection error. node.authentication = config.authentication || "digest"; var controller = null; // Abortcontroller node.setAllClientsStatus = ({ fill, shape, text }) => { function nextStatus(oClient) { oClient.setNodeStatus({ fill: fill, shape: shape, text: text }); } node.nodeClients.map(nextStatus); } const buildClient = (authentication, username, password) => { const mode = (authentication || "digest").toLowerCase(); return createHttpClient({ username, password, authentication: mode === "basic" ? "basic" : "digest", logger: node.debug ? RED.log : undefined }); }; // 14/07/2021 custom agent as global variable, to avoid issue with self signed certificates const customHttpsAgent = new https.Agent({ rejectUnauthorized: false }); // Read the filters const MAJOR_ALARM01 = require('./utils/AccessControlEvents/MAJOR_ALARM-0x1.json'); const MAJOR_EXCEPTION02 = require('./utils/AccessControlEvents/MAJOR_EXCEPTION-0x2.json'); const MAJOR_OPERATION03 = require('./utils/AccessControlEvents/MAJOR_OPERATION-0x3.json'); const MAJOR_EVENT05 = require('./utils/AccessControlEvents/MAJOR_EVENT-0x5.json'); // Get the infos from the device RED.httpAdmin.get("/hikvisionUltimateAccessControlTerminal", RED.auth.needsPermission('AccessControlConfig.read'), function (req, res) { var jParams = JSON.parse(decodeURIComponent(req.query.params));// Retrieve node.id of the config node. var _nodeServer = null; var clientInfo; let passwordToUse = jParams.password; if (jParams.password === "__PWRD__") { _nodeServer = RED.nodes.getNode(req.query.nodeID); if (!_nodeServer || !_nodeServer.credentials || !_nodeServer.credentials.password) { res.json({ error: "Missing stored credentials" }); return; } passwordToUse = _nodeServer.credentials.password; } clientInfo = buildClient(jParams.authentication, jParams.user, passwordToUse); var opt = { // These properties are part of the Fetch Standard method: "GET", headers: {}, // request headers. format is the identical to that accepted by the Headers constructor (see below) body: null, // request body. can be null, a string, a Buffer, a Blob, or a Node.js Readable stream redirect: 'follow', // set to `manual` to extract redirect headers, `error` to reject redirect signal: null, // pass an instance of AbortSignal to optionally abort requests // The following properties are node-fetch extensions follow: 20, // maximum redirect count. 0 to not follow redirect timeout: 5000, // req/res timeout in ms, it resets on redirect. 0 to disable (OS limit applies). Signal is recommended instead. compress: false, // support gzip/deflate content encoding. false to disable size: 0, // maximum response body size in bytes. 0 to disable agent: jParams.protocol === "https" ? customHttpsAgent : null // http(s).Agent instance or function that returns an instance (see below) }; try { (async () => { try { const resInfo = await clientInfo.fetch(jParams.protocol + "://" + jParams.host + ":" + jParams.port + "/ISAPI/System/deviceInfo", opt); const body = await resInfo.text(); const parser = new XMLParser(); try { let jObj = parser.parse(body); res.json(jObj); return; } catch (error) { res.json(error); return; } } catch (error) { RED.log.error("Errore hikvisionUltimateAccessControlTerminal " + error.message); res.json(error); } })(); } catch (err) { res.json(err); } }); // Get the infos from the device RED.httpAdmin.get("/hikvisionUltimateAccessControlTerminalGetEvents", RED.auth.needsPermission('AccessControlConfig.read'), function (req, res) { let majorEvent = Number(req.query.majorevent);// Retrieve major event id if (majorEvent === 1) res.json(JSON.stringify(MAJOR_ALARM01)); if (majorEvent === 2) res.json(JSON.stringify(MAJOR_EXCEPTION02)); if (majorEvent === 3) res.json(JSON.stringify(MAJOR_OPERATION03)); if (majorEvent === 5) res.json(JSON.stringify(MAJOR_EVENT05)); }); // Function to get the plate list from the camera async function getACTEvents() { var client; client = buildClient(node.authentication, node.credentials.user, node.credentials.password); // // Add 1 second to the last date // var dt = new Date(_lastDateTime); // dt.setUTCSeconds(dt.getUTCSeconds() + 1); // _lastDateTime = hikvisionDate.toHikvisionISODateString(dt); var jSonSearch = { "AcsEventCond": { "searchID": node.id.toString() + new Date().toISOString(), "searchResultPosition": 0, "maxResults": 15, "major": 0, "minor": 0, //"startTime": _lastDateTime, //"endTime": "2023-03-18T23:59:59", "timeReverseOrder": true } }; controller = new globalThis.AbortController(); // For aborting the stream request var options = { // These properties are part of the Fetch Standard method: 'POST', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' }, // request headers. format is the identical to that accepted by the Headers constructor (see below) body: JSON.stringify(jSonSearch), redirect: 'follow', // set to `manual` to extract redirect headers, `error` to reject redirect signal: controller.signal, // pass an instance of AbortSignal to optionally abort requests // The following properties are node-fetch extensions follow: 20, // maximum redirect count. 0 to not follow redirect timeout: 10000, // req/res timeout in ms, it resets on redirect. 0 to disable (OS limit applies). Signal is recommended instead. compress: false, // support gzip/deflate content encoding. false to disable size: 0, // maximum response body size in bytes. 0 to disable agent: node.protocol === "https" ? customHttpsAgent : null // http(s).Agent instance or function that returns an instance (see below) }; try { const response = await client.fetch(node.protocol + "://" + node.host + "/ISAPI/AccessControl/AcsEvent?format=json", options); if (response.status >= 200 && response.status <= 300) { //node.setAllClientsStatus({ fill: "green", shape: "ring", text: "Connected." }); } else { node.setAllClientsStatus({ fill: "red", shape: "ring", text: response.statusText || " unknown response code" }); //console.log("BANANA Error response " + response.statusText); throw new Error("Error response: " + response.statusText || " unknown response code"); } //#region "BODY" if (response.ok) { var body = ""; body = await response.text(); var oEvents = JSON.parse(body.toString()); // 31/03/2023 Order by serialNo (because the events are not really ordered by the device) oEvents.AcsEvent.InfoList = oEvents.AcsEvent.InfoList.sort((a, b) => b.serialNo - a.serialNo); // Reverse order // console.log("BANANA AccessControlTerminal: " + sRet); try { // console.log("BANANA GIASONE " + JSON.stringify(oEvents)); // Working the plates. Must be sure, that no error occurs, before acknolwedging the plate last picName if (oEvents.AcsEvent !== null && oEvents.AcsEvent !== undefined) { // Send connection OK if (!node.isConnected) { node.nodeClients.forEach(oClient => { oClient.sendPayload({ topic: oClient.topic || "", errorDescription: "", payload: false }); }) } node.errorDescription = ""; // Reset the error message node.isConnected = true; return oEvents; } else { // Error in parsing if (node.debug) RED.log.info("AccessControl-config: Error: oEvents.AcsEvent is null"); throw new Error("Error: oEvents.AcsEvent is null"); } } catch (error) { if (node.debug) RED.log.warn("AccessControl-config: ERRORE CATCHATO getEvent:" + (error.message || "")); throw new Error("Error getEvent: " + (error.message || "")); } } //#endregion } catch (err) { // Main Error node.errorDescription = err.message || " unknown error"; node.setAllClientsStatus({ fill: "grey", shape: "ring", text: "Server unreachable: " + node.errorDescription + " Retry..." }); if (node.isConnected) { node.nodeClients.forEach(oClient => { oClient.sendPayload({ topic: oClient.topic || "", errorDescription: node.errorDescription, payload: true }); }) } // Abort request if (controller !== null) { try { controller.abort(); } catch (error) { } } node.isConnected = false; return null; }; }; // 29/03/2023 Return the last serialNo (serialNo is the unique event counter received from the device) async function returnMostRecentEventSerialNoFromList(_ACTEvents) { // Is there events? try { let iRet = Number(_ACTEvents.AcsEvent.InfoList[0].serialNo || 0); setTimeout(() => { if (node.isClosing) return; node.setAllClientsStatus({ fill: "grey", shape: "ring", text: "Last event number: " + iRet }); }, 1000); return iRet; } catch (error) { // Return default return 0; } } // At start, reads the last recognized event and starts listening from the time last event was fired. node.initACTEventReader = () => { if (node.isClosing) return; (async () => { var oEvents = null; try { oEvents = await getACTEvents(); } catch (error) { oEvents = null; } if (oEvents === null) { if (node.isClosing) return; if (node.timerInitACTEventReader !== null) clearTimeout(node.timerInitACTEventReader); node.timerInitACTEventReader = setTimeout(node.initACTEventReader, 10000); // Restart initPlateReader } else { try { node.lastACTEventSerialNo = await returnMostRecentEventSerialNoFromList(oEvents); } catch (error) { if (node.isClosing) return; if (node.timerInitACTEventReader !== null) clearTimeout(node.timerInitACTEventReader); node.timerInitACTEventReader = setTimeout(node.initACTEventReader, 10000); // Restart initPlateReader return; } setTimeout(() => { if (node.isClosing) return; node.setAllClientsStatus({ fill: "green", shape: "ring", text: "Waiting for Access events..." }); }, 2000); if (node.timerQueryForACTEvents !== null) clearTimeout(node.timerQueryForACTEvents); node.timerQueryForACTEvents = setTimeout(node.queryForACTEvents, 2000); // Start main polling thread } })(); }; node.queryForACTEvents = () => { if (node.isClosing) return; (async () => { var oEvents = null; try { oEvents = await getACTEvents(); } catch (error) { oEvents = null; } if (oEvents === null) { // An error was occurred. if (node.isClosing) return; if (node.timerInitACTEventReader !== null) clearTimeout(node.timerInitACTEventReader); node.timerInitACTEventReader = setTimeout(node.initACTEventReader, 10000); // Restart initPlateReader from scratch } else { if (oEvents.AcsEvent.hasOwnProperty("InfoList")) { if (Array.isArray(oEvents.AcsEvent.InfoList) && oEvents.AcsEvent.InfoList.length > 0) { // Send the message to the child nodes for (let index = oEvents.AcsEvent.InfoList.length - 1; index >= 0; index--) { let oACTCurrentEvent = oEvents.AcsEvent.InfoList[index]; if (node.lastACTEventSerialNo < Number(oACTCurrentEvent.serialNo || 0)) { // Get only events past the last // Set the last serialNo (serialNo is the unique event counter received from the device) node.lastACTEventSerialNo = Number(oACTCurrentEvent.serialNo || 0); // 29/03/2023 Add event descriptor try { let majorEvent = Number(oACTCurrentEvent.major);// Retrieve major event id let minorEvent = parseInt(oACTCurrentEvent.minor).toString(16);// Retrieve major event id let jDesc = {}; let descMajor = ""; let descMinor = ""; if (majorEvent === 1) jDesc = MAJOR_ALARM01; descMajor = "ALARM"; if (majorEvent === 2) jDesc = MAJOR_EXCEPTION02; descMajor = "EXCEPTION"; if (majorEvent === 3) jDesc = MAJOR_OPERATION03; descMajor = "OPERATION"; if (majorEvent === 5) jDesc = MAJOR_EVENT05; descMajor = "EVENT"; descMinor = jDesc.find(a => a.Value === '0x' + minorEvent).Description; oACTCurrentEvent.eventDescription = '(' + descMajor + ') ' + descMinor; } catch (error) { oACTCurrentEvent.eventDescription = "unknown event: " + error.message; } node.nodeClients.forEach(oClient => { oClient.sendPayload({ topic: oClient.topic || "", payload: oACTCurrentEvent, connected: true }); }) } else { if (node.debug) RED.log.info("AccessControl-config: Discarded old event in oEvents.AcsEvent.InfoList:" + JSON.stringify(oACTCurrentEvent)); } } } } else { // No new events found } if (node.isClosing) return; if (node.timerQueryForACTEvents !== null) clearTimeout(node.timerQueryForACTEvents); node.timerQueryForACTEvents = setTimeout(node.queryForACTEvents, 2000); // Call the function again. } })(); }; // Start! if (node.timerInitACTEventReader !== null) clearTimeout(node.timerInitACTEventReader); node.timerInitACTEventReader = setTimeout(node.initACTEventReader, 10000); // First connection. //#endregion //#region "FUNCTIONS" node.on('close', function (removed, done) { node.isClosing = true; if (controller !== null) { try { controller.abort(); } catch (error) { } } if (node.timerInitACTEventReader !== null) clearTimeout(node.timerInitACTEventReader); if (node.timerQueryForACTEvents !== null) clearTimeout(node.timerQueryForACTEvents); done(); }); node.addClient = (_Node) => { // Check if node already exists if (node.nodeClients.filter(x => x.id === _Node.id).length === 0) { // Add _Node to the clients array node.nodeClients.push(_Node) } try { _Node.setNodeStatus({ fill: "grey", shape: "ring", text: "Waiting for connection" }); } catch (error) { } }; node.removeClient = (_Node) => { // Remove the client node from the clients array //if (node.debug) RED.log.info( "BEFORE Node " + _Node.id + " has been unsubscribed from receiving KNX messages. " + node.nodeClients.length); try { node.nodeClients = node.nodeClients.filter(x => x.id !== _Node.id) } catch (error) { } //if (node.debug) RED.log.info("AFTER Node " + _Node.id + " has been unsubscribed from receiving KNX messages. " + node.nodeClients.length); // If no clien nodes, disconnect from bus. if (node.nodeClients.length === 0) { } }; //#endregion } RED.nodes.registerType("AccessControl-config", AccessControlConfig, { credentials: { user: { type: "text" }, password: { type: "password" } } }); }