node-red-contrib-hikvision-ultimate
Advanced tools
Comparing version 1.1.22 to 1.2.0-beta.1
@@ -6,4 +6,8 @@ <p align="center"><img src='https://raw.githubusercontent.com/Supergiovane/node-red-contrib-hikvision-ultimate/master/img/logo.png' width="40%"></p> | ||
<p> | ||
<b>Version 1.2.0-beta.1</b> September 2024<br/> | ||
- RAW Event Node (BETA): on the third pin, outputs the IMAGE related to the event (if any). The node will now filter the heartbeat signals.<br/> | ||
- Event Node (BETA): on the third pin, outputs the IMAGE related to the event (if any).<br/> | ||
</p> | ||
<p> | ||
<b>Version 1.1.22</b> August 2024<br/> | ||
@@ -10,0 +14,0 @@ - IP Speaker node: added help.<br/> |
@@ -10,3 +10,5 @@ | ||
const https = require('https'); | ||
const Dicer = require('dicer'); | ||
function Hikvisionconfig(config) { | ||
@@ -71,3 +73,3 @@ RED.nodes.createNode(this, config) | ||
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. | ||
timeout: 15000, // 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 | ||
@@ -174,2 +176,7 @@ size: 0, // maximum response body size in bytes. 0 to disable | ||
//#region ALARMSTREAM | ||
// Funzione per estrarre il boundary dal Content-Type | ||
function extractBoundary(contentType) { | ||
const match = contentType.match(/boundary=(.*)$/); | ||
return match ? match[1] : null; | ||
} | ||
var clientAlarmStream; | ||
@@ -195,3 +202,3 @@ async function startAlarmStream() { | ||
follow: 20, // maximum redirect count. 0 to not follow redirect | ||
timeout: 0, // req/res timeout in ms, it resets on redirect. 0 to disable (OS limit applies). Signal is recommended instead. | ||
timeout: 15000, // 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 | ||
@@ -205,13 +212,13 @@ size: 0, // maximum response body size in bytes. 0 to disable | ||
const response = await clientAlarmStream.fetch(node.protocol + "://" + node.host + "/ISAPI/Event/notification/alertStream", optionsAlarmStream); | ||
const res = await clientAlarmStream.fetch(node.protocol + "://" + node.host + "/ISAPI/Event/notification/alertStream", optionsAlarmStream); | ||
if (response.status >= 200 && response.status <= 300) { | ||
if (res.status >= 200 && res.status <= 300) { | ||
node.setAllClientsStatus({ fill: "green", shape: "ring", text: "Waiting for event." }); | ||
} else { | ||
node.setAllClientsStatus({ fill: "red", shape: "ring", text: response.statusText || " unknown response code" }); | ||
node.setAllClientsStatus({ fill: "red", shape: "ring", text: res.statusText || " unknown response code" }); | ||
// if (node.debug) RED.log.error("BANANA Error response " + response.statusText); | ||
node.errorDescription = "StatusResponse problem " + (response.statusText || " unknown status response code"); | ||
throw new Error("StatusResponse " + (response.statusText || " unknown response code")); | ||
node.errorDescription = "StatusResponse problem " + (res.statusText || " unknown status response code"); | ||
throw new Error("StatusResponse " + (res.statusText || " unknown response code")); | ||
} | ||
if (response.ok) { | ||
if (res.ok) { | ||
if (!node.isConnected) { | ||
@@ -225,42 +232,81 @@ node.setAllClientsStatus({ fill: "green", shape: "ring", text: "Connected." }); | ||
node.isConnected = true; | ||
node.resetHeartBeatTimer(); | ||
try { | ||
if (node.debug) RED.log.info("Hikvision-config: before Pipelining..."); | ||
if (oReadable !== null) oReadable.removeAllListeners() // 09/01/2023 | ||
oReadable = readableStr.from(response.body, { encoding: 'utf8' }); | ||
let result = ""; | ||
oReadable.on('data', async (chunk) => { | ||
result += chunk; | ||
if (result.indexOf("--boundary") > -1) { | ||
const contentType = res.headers.get('content-type'); | ||
if (!contentType) { | ||
if (node.debug) RED.log.error("Hikvision-config: No Content-Type in response"); | ||
// 11/01/2022 let's do some other checks on the event stream text | ||
let bMessageCanBeHandled = false; | ||
if (result.includes("</EventNotificationAlert>")) { | ||
// Is the XML | ||
bMessageCanBeHandled = true; | ||
} else if (result.includes("}")) { | ||
// Should be the JSON | ||
bMessageCanBeHandled = true; | ||
} | ||
} | ||
if (bMessageCanBeHandled) { | ||
try { | ||
await handleChunk(result); | ||
} catch (error) { | ||
if (node.debug) RED.log.error("Hikvision-config: Error handleChunk " + (error.message || "") + node.protocol + "://" + node.host); | ||
} | ||
result = ""; | ||
} | ||
if (contentType.includes('multipart')) { | ||
const boundary = extractBoundary(contentType); | ||
if (!boundary) { | ||
if (node.debug) RED.log.error("Hikvision-config: Failed to extract boundary from multipart stream"); | ||
} | ||
}); | ||
oReadable.on('end', function () { | ||
// For some reason, some NVRs do end the stream. I must restart it. | ||
console.log(result) | ||
if (node.debug) RED.log.info("Hikvision-config: streamPipeline: STREAMING HAS ENDED."); | ||
startAlarmStream(); | ||
}); | ||
oReadable.on('error', function (error) { | ||
RED.log.error("Hikvision-config: streamPipeline: " + (error.message || " unknown error") + node.protocol + "://" + node.host); | ||
}); | ||
//console.log(`Receiving multipart stream with boundary: ${boundary}`); | ||
// Inizializza Dicer per il parsing del multipart | ||
const dicer = new Dicer({ boundary }); | ||
dicer.on('part', (part) => { | ||
let partData = []; | ||
let extension = 'bin'; // Default estensione per parti non riconosciute | ||
part.on('header', (header) => { | ||
//console.log('Part headers:', header); | ||
node.resetHeartBeatTimer(); | ||
// Verifica il tipo di parte | ||
if (header['content-type'] && (header['content-type'][0].includes('image/jpeg') || header['content-type'][0].includes('image/jpg'))) { | ||
extension = 'jpg'; // Estensione corretta per immagini JPEG | ||
} else if (header['content-type'] && header['content-type'][0].includes('image/png')) { | ||
extension = 'png'; // Estensione corretta per immagini PNG | ||
} else if (header['content-type'] && header['content-type'][0].includes('application/xml')) { | ||
extension = 'xml'; // Estensione corretta per immagini PNG | ||
} else if (header['content-type'] && header['content-type'][0].includes('application/json')) { | ||
extension = 'json'; // Estensione corretta per immagini PNG | ||
} | ||
}); | ||
part.on('data', (data) => { | ||
node.resetHeartBeatTimer(); | ||
partData.push(data); // Aggiungi i chunk di dati alla parte | ||
}); | ||
part.on('end', () => { | ||
node.resetHeartBeatTimer(); | ||
const fullData = Buffer.concat(partData); // Unisci i chunk di dati | ||
switch (extension) { | ||
case 'xml': | ||
handleXML(fullData); | ||
break; | ||
case 'json': | ||
handleJSON(fullData); | ||
break; | ||
case 'jpg' || 'png': | ||
//const filename = generateFilename(extension); | ||
//saveFile(fullData, filename); // Salva l'immagine su disco | ||
handleIMG(fullData, extension); | ||
break; | ||
default: | ||
break; | ||
} | ||
}); | ||
}); | ||
dicer.on('finish', () => { | ||
console.log('Finished parsing multipart stream.'); | ||
}); | ||
dicer.on('error', (err) => { | ||
console.error('Error in Dicer:', err); | ||
}); | ||
// Pipa lo stream multipart in Dicer | ||
res.body.pipe(dicer); | ||
} else { | ||
//throw new Error('Unsupported Content-Type'); | ||
} | ||
} catch (error) { | ||
@@ -282,74 +328,38 @@ if (node.debug) RED.log.error("Hikvision-config: streamPipeline: Please be sure to have the latest Node.JS version installed: " + (error.message || " unknown error")); | ||
//#region "HANDLE STREAM MESSAGE" | ||
// Handle the complete stream message, enclosed into the --boundary stream string | ||
// If there is more boundary, process each one separately | ||
// Handle the complete stream message | ||
// ################################### | ||
async function handleChunk(result) { | ||
async function handleIMG(result, extension) { | ||
try { | ||
// 05/12/2020 process the data | ||
var aResults = result.split("--boundary"); | ||
if (node.debug) RED.log.info("SPLITTATO RESULT COUNT: ####### " + aResults.length + " ###################### FINE SPLITTATO RESULT"); | ||
aResults.forEach(async sRet => { | ||
if (sRet.trim() !== "") { | ||
if (node.debug) RED.log.error("BANANA PROCESSING" + sRet); | ||
try { | ||
//sRet = sRet.replace(/--boundary/g, ''); | ||
var i = sRet.includes("Content-Type: application/xml"); | ||
if (i > -1) { | ||
sRet = sRet.substring(i); | ||
// 11/01/2023 new parser XML to Json | ||
try { | ||
const parser = new XMLParser(); | ||
let result = parser.parse(sRet); | ||
if (node.debug) RED.log.error("BANANA SBANANATO XML -> JSON " + JSON.stringify(result)); | ||
if (result !== null && result !== undefined && result.hasOwnProperty("EventNotificationAlert")) { | ||
node.nodeClients.forEach(oClient => { | ||
if (result !== undefined) oClient.sendPayload({ topic: oClient.topic || "", payload: result.EventNotificationAlert }); | ||
}); | ||
} | ||
} catch (error) { | ||
sRet = ""; | ||
if (node.debug) RED.log.error("BANANA ERRORE fast-xml-parser(sRet, function (err, result) " + error.message || ""); | ||
} | ||
} else if (sRet.includes("Content-Type: application/json")) { | ||
i = sRet.indexOf("{") // It's a Json | ||
if (node.debug) RED.log.error("BANANA SBANANATO JSON " + sRet); | ||
sRet = sRet.substring(i); | ||
try { | ||
sRet = JSON.parse(sRet); | ||
// if (node.debug) RED.log.error("BANANA JSONATO: " + sRet); | ||
if (sRet !== null && sRet !== undefined) { | ||
node.nodeClients.forEach(oClient => { | ||
oClient.sendPayload({ topic: oClient.topic || "", payload: sRet }); | ||
}) | ||
} | ||
} catch (error) { | ||
sRet = ""; | ||
} | ||
} else { | ||
// Invalid body | ||
if (node.debug) RED.log.info("Hikvision-config: DecodingBody Info only: 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) { | ||
// if (node.debug) RED.log.error("BANANA startAlarmStream decodifica body: " + error); | ||
if (node.debug) RED.log.error("Hikvision-config: DecodingBody error: " + (error.message || " unknown error")); | ||
throw (error); | ||
} | ||
} else { | ||
if (node.debug) RED.log.info("SPLITTATO RESULT EMPTY: ####### " + sRet + " ###################### FINE SPLITTATO RESULT"); | ||
} | ||
if (node.debug) RED.log.error("BANANA SBANANATO IMG -> JSON " + JSON.stringify(oXML)); | ||
node.nodeClients.forEach(oClient => { | ||
oClient.sendPayload({ topic: oClient.topic || "", payload: result, type: 'img', extension: extension }); | ||
}); | ||
} catch (error) { | ||
if (node.debug) RED.log.info("Hikvision-config: readStream error: " + (error.message || " unknown error")); | ||
node.errorDescription = "readStream error " + (error.message || " unknown error"); | ||
throw (error); | ||
if (node.debug) RED.log.error("BANANA ERRORE fast-xml-parser(sRet, function (err, result) " + error.message || ""); | ||
} | ||
} | ||
async function handleXML(result) { | ||
try { | ||
const parser = new XMLParser(); | ||
const oXML = parser.parse(result); | ||
if (node.debug) RED.log.error("BANANA SBANANATO XML -> JSON " + JSON.stringify(oXML)); | ||
node.nodeClients.forEach(oClient => { | ||
if (oXML !== undefined) oClient.sendPayload({ topic: oClient.topic || "", payload: oXML.EventNotificationAlert, type: 'event' }); | ||
}); | ||
} catch (error) { | ||
if (node.debug) RED.log.error("BANANA ERRORE fast-xml-parser(sRet, function (err, result) " + error.message || ""); | ||
} | ||
} | ||
async function handleJSON(result) { | ||
try { | ||
const oJSON = JSON.parse(result); | ||
if (oJSON !== null && oJSON !== undefined) { | ||
node.nodeClients.forEach(oClient => { | ||
oClient.sendPayload({ topic: oClient.topic || "", payload: oJSON, type: 'event' }); | ||
}) | ||
} | ||
} catch (error) { | ||
if (node.debug) RED.log.error("BANANA ERRORE fast-xml-parser(sRet, function (err, result) " + error.message || ""); | ||
} | ||
} | ||
// ################################### | ||
@@ -380,3 +390,3 @@ //#endregion | ||
follow: 20, // maximum redirect count. 0 to not follow redirect | ||
timeout: 8000, // req/res timeout in ms, it resets on redirect. 0 to disable (OS limit applies). Signal is recommended instead. | ||
timeout: 15000, // 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 | ||
@@ -383,0 +393,0 @@ size: 0, // maximum response body size in bytes. 0 to disable |
@@ -43,3 +43,3 @@ | ||
node.setNodeStatus({ fill: "red", shape: "dot", text: "Zone " + node.currentAlarmMSG.zone + " alarm" }); | ||
node.send([node.currentAlarmMSG, null]); | ||
node.send([node.currentAlarmMSG, null, null]); | ||
} else { node.total_alarmfilterduration = 0; } | ||
@@ -68,8 +68,13 @@ } | ||
// Called from config node, to send output to the flow | ||
node.sendPayload = (_msg) => { | ||
node.sendPayload = (_msg, extension = '') => { | ||
if (_msg === null || _msg === undefined) return; | ||
_msg.topic = node.topic; | ||
if (_msg.hasOwnProperty("errorDescription")) { node.send([null, _msg]); return; }; // It's a connection error/restore comunication. | ||
if (_msg.hasOwnProperty("errorDescription")) { node.send([null, _msg, null]); return; }; // It's a connection error/restore comunication. | ||
if (!_msg.hasOwnProperty("payload") || (_msg.hasOwnProperty("payload") && _msg.payload === undefined)) return; | ||
if (_msg.type === 'img') { | ||
_msg.extension = extension; | ||
node.send([null, null, _msg]); | ||
return; | ||
} | ||
var oRetMsg = {}; // Return message | ||
@@ -302,3 +307,3 @@ | ||
if (node.alarmfilterduration == 0) { | ||
node.send([oRetMsg, null]); | ||
node.send([oRetMsg, null, null]); | ||
} else { | ||
@@ -308,3 +313,3 @@ // Sends the false only in case the isNodeInAlarm is true. | ||
if (oRetMsg.payload === false && node.isNodeInAlarm) { | ||
node.send([oRetMsg, null]); | ||
node.send([oRetMsg, null, null]); | ||
node.currentAlarmMSG = {}; | ||
@@ -311,0 +316,0 @@ node.isNodeInAlarm = false; |
@@ -16,8 +16,19 @@ | ||
// Called from config node, to send output to the flow | ||
node.sendPayload = (_msg) => { | ||
node.sendPayload = (_msg, extension = '') => { | ||
if (_msg === null || _msg === undefined) return; | ||
_msg.topic = node.topic; | ||
if (_msg.hasOwnProperty("errorDescription")) { node.send([null, _msg]); return; }; // It's a connection error/restore comunication. | ||
if (_msg.hasOwnProperty("errorDescription")) { node.send([null, _msg, null]); return; }; // It's a connection error/restore comunication. | ||
if (!_msg.hasOwnProperty("payload") || (_msg.hasOwnProperty("payload") && _msg.payload === undefined)) return; | ||
if (_msg.type !== undefined && _msg.type === 'img') { | ||
_msg.extension = extension; | ||
node.send([null, null, _msg]); | ||
return; | ||
} | ||
// Heartbeat discard | ||
// <activePostCount>0</activePostCount> | ||
// <eventType>videoloss</eventType> | ||
// <eventState>inactive</eventState> | ||
if (_msg.hasOwnProperty("payload") | ||
@@ -27,3 +38,3 @@ && _msg.payload.hasOwnProperty("eventType") | ||
&& _msg.payload.eventState.toString().toLowerCase() === "inactive" | ||
&& _msg.payload.hasOwnProperty("activePostCount") && Number(_msg.payload.activePostCount) === 0) { | ||
&& _msg.payload.hasOwnProperty("activePostCount") && (Number(_msg.payload.activePostCount) === 0 || Number(_msg.payload.activePostCount) === 1)) { | ||
// It's a HertBeat, exit. | ||
@@ -35,3 +46,3 @@ node.setNodeStatus({ fill: "green", shape: "ring", text: "Waiting for alert..." }); | ||
node.setNodeStatus({ fill: "green", shape: "dot", text: "Alert received" }); | ||
node.send([_msg, null]); | ||
node.send([_msg, null, null]); | ||
} | ||
@@ -38,0 +49,0 @@ |
@@ -42,2 +42,10 @@ | ||
if (_msg === null || _msg === undefined) return; | ||
if (_msg.type !== undefined && _msg.type === 'img') { | ||
_msg.extension = extension; | ||
node.send([null, null, _msg]); | ||
return; | ||
} | ||
_msg.topic = node.topic; | ||
@@ -44,0 +52,0 @@ _msg.payload = true; |
@@ -219,2 +219,7 @@ | ||
if (_msg === null || _msg === undefined) return; | ||
if (_msg.type !== undefined && _msg.type === 'img') { | ||
// Coming from an event containing an image | ||
return; | ||
} | ||
// 01/09/2022 Add the previous input message | ||
@@ -221,0 +226,0 @@ _msg.previousInputMessage = node.previousInputMessage; |
@@ -18,2 +18,8 @@ module.exports = function (RED) { | ||
if (_msg === null || _msg === undefined) return; | ||
if (_msg.type !== undefined && _msg.type === 'img') { | ||
// The payload is an image, exit. | ||
return; | ||
} | ||
_msg.topic = node.topic; | ||
@@ -55,3 +61,3 @@ if (_msg.hasOwnProperty("errorDescription")) { node.send([null, _msg]); return; }; // It's a connection error/restore comunication. | ||
node.setNodeStatus({ fill: "green", shape: "dot", text: "Preset passed by msg input." }); | ||
recallPTZ(); | ||
recallPTZ(); | ||
} | ||
@@ -58,0 +64,0 @@ } |
@@ -20,2 +20,6 @@ | ||
node.sendPayload = (_msg) => { | ||
if (_msg.type !== undefined && _msg.type === 'img') { | ||
// The payload is an image, exit. | ||
return; | ||
} | ||
node.setNodeStatus({ fill: "green", shape: "ring", text: "Received response" }); | ||
@@ -44,3 +48,3 @@ if (_msg === null || _msg === undefined) return; | ||
node.on("close", function (done) { | ||
@@ -47,0 +51,0 @@ done(); |
{ | ||
"name": "node-red-contrib-hikvision-ultimate", | ||
"version": "1.1.22", | ||
"version": "1.2.0-beta.1", | ||
"description": "A native set of nodes for Hikvision (and compatible) Cameras, Alarms, Radars, NVR, Doorbells, etc.", | ||
@@ -12,3 +12,4 @@ "author": "Supergiovane (https://github.com/Supergiovane)", | ||
"lodash": "4.17.21", | ||
"fast-xml-parser": "4.0.13" | ||
"fast-xml-parser": "4.0.13", | ||
"dicer": "0.3.1" | ||
}, | ||
@@ -15,0 +16,0 @@ "keywords": [ |
@@ -50,2 +50,3 @@ | ||
For RADAR device types, you can filter improper/false alams as well.<br/> | ||
On the third pin, the node will output the event's picture (if any). You can save it directly to disk or user where you want.<br/> | ||
@@ -132,2 +133,11 @@ <img src='https://raw.githubusercontent.com/Supergiovane/node-red-contrib-hikvision-ultimate/master/img/GenericAlarm.png' > | ||
``` | ||
**Output PIN 3 (Image)** | ||
```javascript | ||
msg = { | ||
"topic": "", | ||
"payload": image, | ||
"extension": "jpg" // Can be "jpg" or "png" | ||
} | ||
``` | ||
<br/> | ||
@@ -520,2 +530,3 @@ <br/> | ||
The RAW CAMERA Event node reacts to every message sent by the device. You can use this node when the other nodes doesn't fit your needs. It connects to ***NVR, Camera, Radars etc...*** and outputs the event received. <br/> | ||
On the third pin, the node will output the event's picture (if any). You can save it directly to disk or user where you want.<br/> | ||
@@ -535,2 +546,3 @@ <img src='https://raw.githubusercontent.com/Supergiovane/node-red-contrib-hikvision-ultimate/master/img/RawAlarm.png' > | ||
"topic": "", | ||
"type": "event", | ||
"payload": { | ||
@@ -563,4 +575,3 @@ "ipAddress": "192.168.1.25", | ||
] | ||
}, | ||
"_msgid": "dba1850a.2dc5e8" | ||
} | ||
} | ||
@@ -574,6 +585,15 @@ ``` | ||
"errorDescription": "", // This will contain the error rescription, in case of errors. | ||
"payload": false, // Or TRUE if error | ||
"_msgid": "dd5b3622.884a78" | ||
"payload": false // Or TRUE if error | ||
} | ||
``` | ||
**Output PIN 3 (Image)** | ||
```javascript | ||
msg = { | ||
"topic": "", | ||
"type": "img", | ||
"payload": image, | ||
"extension": "jpg" // Can be "jpg" or "png" | ||
} | ||
``` | ||
<br/> | ||
@@ -580,0 +600,0 @@ <br/> |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
3492379
95
6804
835
7
2
10
+ Addeddicer@0.3.1
+ Addeddicer@0.3.1(transitive)
+ Addedstreamsearch@1.1.0(transitive)