node-red-contrib-hikvision-ultimate
Advanced tools
Comparing version 0.0.16 to 0.0.17
@@ -5,2 +5,8 @@ # node-red-contrib-hikvision-ultimate | ||
<p> | ||
<b>Version 0.0.17 BETA</b> December 2020<br/> | ||
- BUGFIX: fixed a vaccata in handling boundary of stream pipeline.<br/> | ||
- BUGFIX: fixed a minchiata in ANPR node, where if the alarmed zone was 0, the node outputs "unknown zone".<br/> | ||
- Fixed Radar alarm zone. Zone number in ISAPI is base 0, while in the UI is base 1.<br/> | ||
</p> | ||
<p> | ||
<b>Version 0.0.16 BETA</b> December 2020<br/> | ||
@@ -7,0 +13,0 @@ - NEW: added Radar Node for trapping radar zone's alarms.<br/> |
@@ -21,3 +21,3 @@ | ||
function nextStatus(oClient) { | ||
oClient.setNodeStatus({ fill: fill, shape: shape, text: text }) | ||
oClient.setNodeStatus({ fill: fill, shape: shape, text: text }); | ||
} | ||
@@ -27,2 +27,19 @@ node.nodeClients.map(nextStatus); | ||
// Sort the plates, in any case, even if the anpr camera returns a sorted list. It's not always true! | ||
function sortPlates(a, b) { | ||
try { | ||
if (a.Plate.picName < b.Plate.picName) { | ||
return -1; | ||
} | ||
if (a.Plate.picName > b.Plate.picName) { | ||
return 1; | ||
} | ||
return 0; | ||
} catch (error) { | ||
return 0; | ||
} | ||
} | ||
// Function to get the plate list from the camera | ||
@@ -52,54 +69,64 @@ async function getPlates(_lastPicName) { | ||
if (response.status >= 200 && response.status <= 300) { | ||
node.setAllClientsStatus({ fill: "green", shape: "ring", text: "Connected." }); | ||
//node.setAllClientsStatus({ fill: "green", shape: "ring", text: "Connected." }); | ||
} else { | ||
node.setAllClientsStatus({ fill: "red", shape: "ring", text: response.statusText }); | ||
// console.log("BANANA Error response " + response.statusText); | ||
console.log("BANANA Error response " + response.statusText); | ||
throw ("Error response: " + response.statusText); | ||
} | ||
//#region "BODY" | ||
const body = await response.text(); | ||
var sRet = ""; | ||
sRet = body.toString(); | ||
//// console.log("BANANA " + sRet); | ||
var oPlates = null; | ||
try { | ||
var i = sRet.indexOf("<"); // Get only the XML, starting with "<" | ||
if (i > -1) { | ||
sRet = sRet.substring(i); | ||
// By xml2js | ||
xml2js(sRet, function (err, result) { | ||
oPlates = result; | ||
}); | ||
} else { | ||
i = sRet.indexOf("{") // It's a Json | ||
if (response.ok) { | ||
var body = ""; | ||
body = await response.text(); | ||
var sRet = body.toString(); | ||
console.log("BANANA ANPR: " + sRet); | ||
var oPlates = null; | ||
try { | ||
var i = sRet.indexOf("<"); // Get only the XML, starting with "<" | ||
if (i > -1) { | ||
sRet = sRet.substring(i); | ||
oPlates = JSON.parse(result); | ||
// By xml2js | ||
xml2js(sRet, function (err, result) { | ||
oPlates = result; | ||
}); | ||
} else { | ||
// Invalid body | ||
RED.log.info("ANPR-config: DecodingBody: Invalid Json " + sRet); | ||
// console.log("BANANA ANPR-config: DecodingBody: Invalid Json " + sRet); | ||
throw ("Error Invalid Json: " + sRet); | ||
i = sRet.indexOf("{") // It's a Json | ||
if (i > -1) { | ||
sRet = sRet.substring(i); | ||
oPlates = JSON.parse(result); | ||
} else { | ||
// Invalid body | ||
RED.log.info("ANPR-config: DecodingBody: Invalid Json " + sRet); | ||
// console.log("BANANA ANPR-config: DecodingBody: Invalid Json " + sRet); | ||
throw ("Error Invalid Json: " + sRet); | ||
} | ||
} | ||
} | ||
// console.log("BANANA GIASONE " + JSON.stringify(oPlates)); | ||
// Working the plates. Must be sure, that no error occurs, before acknolwedging the plate last picName | ||
if (oPlates.Plates !== null) { | ||
node.setAllClientsStatus({ fill: "green", shape: "ring", text: "Waiting for vehicle..." }); | ||
if (!node.isConnected) { | ||
node.nodeClients.forEach(oClient => { | ||
oClient.sendPayload({ topic: oClient.topic || "", payload: null, connected: true }); | ||
}) | ||
// console.log("BANANA GIASONE " + JSON.stringify(oPlates)); | ||
// Working the plates. Must be sure, that no error occurs, before acknolwedging the plate last picName | ||
if (oPlates.Plates !== null && oPlates.Plates !== undefined) { | ||
if (!node.isConnected) { | ||
node.nodeClients.forEach(oClient => { | ||
oClient.sendPayload({ topic: oClient.topic || "", payload: null, connected: true }); | ||
}) | ||
} | ||
node.isConnected = true; | ||
//console.log("BANANA JSON PLATES: " + JSON.stringify(oPlates)); | ||
if (oPlates.Plates.hasOwnProperty("Plate")) { | ||
// Returns a sorted list, always. | ||
oPlates.Plates.Plate = oPlates.Plates.Plate.sort(sortPlates); | ||
console.log("BANANA PLATES ORDINATE:" + JSON.stringify(oPlates)); | ||
return oPlates; | ||
} else { | ||
// Returns the object, empty. | ||
return oPlates; | ||
} | ||
} else { | ||
// Error in parsing XML | ||
throw ("Error: oPlates.Plates is null"); | ||
} | ||
node.isConnected = true; | ||
return oPlates; | ||
} else { | ||
// Error in parsing XML | ||
throw ("Error: oPlates.Plates is null"); | ||
} catch (error) { | ||
RED.log.error("ANPR-config: ERRORE CATCHATO initPlateReader:" + error); | ||
// console.log("BANANA ANPR-config: ERRORE CATCHATO initPlateReader: " + error); | ||
throw ("Error initPlateReader: " + error); | ||
} | ||
} catch (error) { | ||
RED.log.error("ANPR-config: ERRORE CATCHATO initPlateReader:" + error); | ||
// console.log("BANANA ANPR-config: ERRORE CATCHATO initPlateReader: " + error); | ||
throw ("Error initPlateReader: " + error); | ||
} | ||
@@ -130,5 +157,6 @@ //#endregion | ||
node.initPlateReader = () => { | ||
// console.log("BANANA INITPLATEREADER"); | ||
node.setAllClientsStatus({ fill: "grey", shape: "ring", text: "Getting prev list to be ignored..." }); | ||
(async () => { | ||
//node.setAllClientsStatus({ fill: "grey", shape: "ring", text: "Getting prev list to be ignored..." }); | ||
(async () => { | ||
var oPlates = await getPlates("202001010101010000"); | ||
@@ -142,2 +170,3 @@ if (oPlates === null) { | ||
node.lastPicName = oPlates.Plates.Plate[oPlates.Plates.Plate.length - 1].picName; | ||
console.log("BANANA PLATES IGNORATE: " + oPlates.Plates.Plate.length + " ignored plates. Last was " + node.lastPicName); | ||
node.setAllClientsStatus({ fill: "grey", shape: "ring", text: "Found " + oPlates.Plates.Plate.length + " ignored plates. Last was " + node.lastPicName }); | ||
@@ -154,2 +183,3 @@ } catch (error) { | ||
} | ||
setTimeout(() => node.setAllClientsStatus({ fill: "green", shape: "ring", text: "Waiting for vehicle..." }),2000); | ||
setTimeout(node.queryForPlates, 2000); // Start main polling thread | ||
@@ -156,0 +186,0 @@ } |
@@ -44,3 +44,3 @@ | ||
setTimeout(startAlarmStream, 5000); // Reconnect | ||
}, 25000); | ||
}, 40000); | ||
} | ||
@@ -73,3 +73,6 @@ | ||
//#region "HANDLE STREAM MESSAGE" | ||
// Async get the body, called by streamPipeline(response.body, readStream); | ||
// Handle the complete stream message, enclosed into the --boundary stream string | ||
// ################################### | ||
@@ -79,43 +82,50 @@ const streamPipeline = util.promisify(require('stream').pipeline); | ||
try { | ||
let result = ""; // The complete message, as soon as --boudary is received. | ||
for await (const chunk of stream) { | ||
var sRet = ""; | ||
sRet = chunk.toString(); | ||
// // console.log("BANANA " + sRet); | ||
try { | ||
sRet = sRet.replace(/--boundary/g, ''); | ||
var i = sRet.indexOf("<"); // Get only the XML, starting with "<" | ||
if (i > -1) { | ||
sRet = sRet.substring(i); | ||
// // // console.log("BANANA SBANANATO " + sRet); | ||
// By xml2js | ||
xml2js(sRet, function (err, result) { | ||
node.nodeClients.forEach(oClient => { | ||
if (result !== undefined) oClient.sendPayload({ topic: oClient.topic || "", payload: result.EventNotificationAlert, connected: true }); | ||
}) | ||
}); | ||
} else { | ||
i = sRet.indexOf("{") // It's a Json | ||
result += chunk.toString(); | ||
// console.log("BANANA CHUNK " + chunk.toString()); | ||
// Gotta --boundary, process the message | ||
if (result.toString().indexOf("--boundary") > -1) { | ||
// console.log("BANANA FOUND BOUNDARY"); | ||
var sRet = result.toString(); | ||
result = ""; // Reset the result | ||
// console.log("BANANA PROCESSING" + sRet); | ||
try { | ||
sRet = sRet.replace(/--boundary/g, ''); | ||
var i = sRet.indexOf("<"); // Get only the XML, starting with "<" | ||
if (i > -1) { | ||
sRet = sRet.substring(i); | ||
//sRet = sRet.replace(/(['"])?([a-z0-9A-Z_]+)(['"])?:/g, '"$2": '); // Fix numbers and chars invalid in JSON | ||
// // console.log("BANANA JSONATO: " + sRet); | ||
node.nodeClients.forEach(oClient => { | ||
oClient.sendPayload({ topic: oClient.topic || "", payload: JSON.parse(sRet), connected: true }); | ||
}) | ||
// // // // console.log("BANANA SBANANATO " + sRet); | ||
// By xml2js | ||
xml2js(sRet, function (err, result) { | ||
node.nodeClients.forEach(oClient => { | ||
if (result !== undefined) oClient.sendPayload({ topic: oClient.topic || "", payload: result.EventNotificationAlert, connected: true }); | ||
}) | ||
}); | ||
} else { | ||
// Invalid body | ||
RED.log.info("Hikvision-config: DecodingBody: Invalid Json " + sRet); | ||
i = sRet.indexOf("{") // It's a Json | ||
if (i > -1) { | ||
sRet = sRet.substring(i); | ||
//sRet = sRet.replace(/(['"])?([a-z0-9A-Z_]+)(['"])?:/g, '"$2": '); // Fix numbers and chars invalid in JSON | ||
// // // console.log("BANANA JSONATO: " + sRet); | ||
node.nodeClients.forEach(oClient => { | ||
oClient.sendPayload({ topic: oClient.topic || "", payload: JSON.parse(sRet), connected: true }); | ||
}) | ||
} else { | ||
// Invalid body | ||
RED.log.info("Hikvision-config: DecodingBody: Invalid Json " + sRet); | ||
} | ||
} | ||
// All is fine. Reset and restart the hearbeat timer | ||
// Hikvision sends an heartbeat alarm (videoloss), depending on firmware, every 300ms or more. | ||
// If this HeartBeat isn't received, abort the stream request and restart. | ||
node.resetHeartBeatTimer(); | ||
} catch (error) { | ||
// console.log("BANANA startAlarmStream decodifica body: " + error); | ||
RED.log.error("Hikvision-config: DecodingBody error: " + error); | ||
} | ||
// All is fine. Reset and restart the hearbeat timer | ||
// Hikvision sends an heartbeat alarm (videoloss), depending on firmware, every 300ms or more. | ||
// If this HeartBeat isn't received, abort the stream request and restart. | ||
node.resetHeartBeatTimer(); | ||
} catch (error) { | ||
// // console.log("BANANA startAlarmStream decodifica body: " + error); | ||
RED.log.info("Hikvision-config: DecodingBody error: " + error); | ||
} | ||
} | ||
} catch (error) { | ||
// // console.log("BANANA NEL BODY errore " + error); | ||
// console.log("BANANA NEL BODY errore " + error); | ||
return; | ||
@@ -125,16 +135,20 @@ } | ||
// ################################### | ||
//#endregion | ||
const response = await client.fetch("http://" + node.host + "/ISAPI/Event/notification/alertStream", options); | ||
if (response.status >= 200 && response.status <= 300) { | ||
node.setAllClientsStatus({ fill: "green", shape: "ring", text: "Connected. Waiting for Alarm." }); | ||
node.setAllClientsStatus({ fill: "green", shape: "ring", text: "Waiting for Alarm." }); | ||
} else { | ||
node.setAllClientsStatus({ fill: "red", shape: "ring", text: response.statusText }); | ||
// // console.log("BANANA Error response " + response.statusText); | ||
// // // console.log("BANANA Error response " + response.statusText); | ||
throw ("Error response: " + response.statusText); | ||
} | ||
node.isConnected = true; | ||
streamPipeline(response.body, readStream); | ||
if (response.ok) { | ||
node.isConnected = true; | ||
streamPipeline(response.body, readStream); | ||
} | ||
} catch (err) { | ||
// Main Error | ||
// // console.log("BANANA MAIN ERROR: " + err); | ||
// // // console.log("BANANA MAIN ERROR: " + err); | ||
// Abort request | ||
@@ -141,0 +155,0 @@ try { |
@@ -10,3 +10,3 @@ | ||
node.currentPlate = ""; // Stores the current plate (for the avoidsameplatetime function) | ||
node.timerAvoidSamePlate; // Timer for avoiding repeating plate | ||
node.timerAvoidSamePlate = null; // Timer for avoiding repeating plate | ||
node.bAvoidSamePlate = false; | ||
@@ -32,3 +32,3 @@ | ||
// ########################## | ||
try { clearTimeout(node.timerAvoidSamePlate); } catch (error) { }; | ||
if (node.timerAvoidSamePlate !== null) clearTimeout(node.timerAvoidSamePlate); | ||
node.bAvoidSamePlate = true; | ||
@@ -35,0 +35,0 @@ node.timerAvoidSamePlate = setTimeout(() => { |
@@ -54,3 +54,3 @@ | ||
oRetMsg.alarm = _msg.payload; // Put the full alarm description here. | ||
oRetMsg.zone = _msg.payload.CIDEvent.zone || "unknown"; | ||
oRetMsg.zone = _msg.payload.CIDEvent.zone + 1; // The zone on device's ISAPI is base 0, while the zone on UI is base 1. | ||
if (Number(node.filterzone) === 0 || Number(node.filterzone) === Number(oRetMsg.zone)) { // Filter only selcted zones | ||
@@ -57,0 +57,0 @@ // Get the Hikvision alarm codes, that differs from standard SIA codes. |
{ | ||
"name": "node-red-contrib-hikvision-ultimate", | ||
"version": "0.0.16", | ||
"version": "0.0.17", | ||
"description": "A native set of node for Hikvision Cameras, Alarms, Radars etc.", | ||
@@ -5,0 +5,0 @@ "author": "Supergiovane (https://github.com/Supergiovane)", |
@@ -21,2 +21,3 @@ # node-red-contrib-hikvision-ultimate | ||
There are currently **only two nodes**, one that traps the alarms, in RAW mode, and outputs a JSON and the other that outputs the ANPR License Plate numbers.<br/> | ||
The node uses pipelines to handle streams, so you need at least **Node V.10.0.0**, or newer, installed. To check Node version, type **node -v** in a command prompt.<br/> | ||
@@ -35,3 +36,3 @@ ***THIS NODE IS IN BETA***<br/> | ||
<img src='https://raw.githubusercontent.com/Supergiovane/node-red-contrib-hikvision-ultimate/master/img/RawAlarm.png' width="50%"> | ||
<img src='https://raw.githubusercontent.com/Supergiovane/node-red-contrib-hikvision-ultimate/master/img/RawAlarm.png' width="80%"> | ||
@@ -42,2 +43,3 @@ **Output message** | ||
This below, is only an example (in this case, a movement detected from a radar)</br> | ||
**Caution**: the node actively checks if the device is connected to the network. In case of disconnection/reconnection, the nodes will output a message with **msg.connected = false** with a ***null*** payload if disconnected, and **msg.connected = true** if connected.<br/> | ||
@@ -85,8 +87,8 @@ ```javascript | ||
<img src='https://raw.githubusercontent.com/Supergiovane/node-red-contrib-hikvision-ultimate/master/img/ANPR.png' width="50%"> | ||
<img src='https://raw.githubusercontent.com/Supergiovane/node-red-contrib-hikvision-ultimate/master/img/ANPR.png' width="80%"> | ||
**Output message** | ||
The node outputs this msg.</br> | ||
The payload contains the license plate number and the property "plate" contains other useful informations.</br> | ||
**Caution**: the node actively checks if the ANPR camera is connected to the network. In case of disconnection/reconnection, the nodes will output a message with **msg.connected = false** with a ***null*** payload if disconnected, and **msg.connected = true** if connected.<br/> | ||
@@ -115,3 +117,3 @@ ```javascript | ||
<img src='https://raw.githubusercontent.com/Supergiovane/node-red-contrib-hikvision-ultimate/master/img/Radar.png' width="50%"> | ||
<img src='https://raw.githubusercontent.com/Supergiovane/node-red-contrib-hikvision-ultimate/master/img/Radar.png' width="80%"> | ||
@@ -124,2 +126,3 @@ **Output message** | ||
In an **unknown CID event** arrives from the Radar, the node will output a message containing the CID code, the full alarm and a null payload.</br> | ||
**Caution**: the node actively checks if the radar is connected to the network. In case of disconnection/reconnection, the nodes will output a message with **msg.connected = false** with a ***null*** payload if disconnected, and **msg.connected = true** if connected.<br/> | ||
@@ -126,0 +129,0 @@ ```javascript |
270064
606
177