node-red-contrib-hikvision-ultimate
Version:
A native set of nodes for Hikvision (and compatible) Cameras, Alarms, Radars, NVR, Doorbells, etc.
290 lines (248 loc) • 11.7 kB
JavaScript
module.exports = function (RED) {
function hikvisionUltimateIntelligent(config) {
RED.nodes.createNode(this, config);
var node = this;
node.topic = config.topic || config.name;
node.server = RED.nodes.getNode(config.server)
const isDebug = node.server && node.server.debug;
const logDebug = (text) => {
if (isDebug) RED.log.info(`hikvisionUltimateIntelligent: ${text}`);
};
// Current state of the node
node.currentAlarmMSG = {}; // Stores the current alarm object
node.total_alarmfilterduration = 0; // stores the total time an alarm has been true in the alarmfilterperiod time.
node.isNodeInAlarm = false; // Stores the current state of the filtered alarm.
node.isRunningTimerFilterPeriod = false; // Indicates wether the period timer is running;
node.timer_alarmfilterduration = null;
node.timer_alarmfilterperiod = null;
// Get the node config
node.channelID = config.channelID || "0";
node.intelligentevent = config.intelligentevent || "all";
node.alarmfilterduration = config.alarmfilterduration !== undefined ? Number(config.alarmfilterduration) : 0;
node.alarmfilterperiod = config.alarmfilterperiod !== undefined ? Number(config.alarmfilterperiod) : 0;
const EVENT_DESCRIPTION_MAP = {
illaccess: "Illegal access"
};
const getIntelligentEventDescription = (event) => {
if (!event || typeof event !== "object") return "Intelligent event";
const rawType = event.eventType ? event.eventType.toString() : "";
const normalizedType = rawType.toLowerCase().replace(/\s+/g, "").replace(/_/g, "");
const rawDescription = event.eventDescription ? event.eventDescription.toString() : "";
const mappedBase = EVENT_DESCRIPTION_MAP[normalizedType];
if (mappedBase) {
if (rawDescription) {
const descLower = rawDescription.toLowerCase().trim();
// Example: \"illaccess alarm\" -> \"Illegal access alarm\"
if (descLower.startsWith(normalizedType) && descLower.endsWith("alarm")) {
return `${mappedBase} alarm`;
}
// Otherwise keep the camera-provided description
return rawDescription;
}
return mappedBase;
}
// Fallback to original description/type if no mapping is available
if (rawDescription) return rawDescription;
if (rawType) return rawType;
return "Intelligent event";
};
node.setNodeStatus = ({ fill, shape, text }) => {
var dDate = new Date();
node.status({ fill: fill, shape: shape, text: text + " (" + dDate.getDate() + ", " + dDate.toLocaleTimeString() + ")" })
}
// Starts the timer that counts the time the alarm has been true
const startTimerAlarmFilterDuration = () => {
node.timer_alarmfilterduration = setInterval(() => {
if (node.currentAlarmMSG.hasOwnProperty("payload")) {
if (node.currentAlarmMSG.payload === true) {
node.total_alarmfilterduration += 1;
if (node.isRunningTimerFilterPeriod) node.setNodeStatus({ fill: "red", shape: "ring", text: "Zone " + node.currentAlarmMSG.zone + " pre alert count " + node.total_alarmfilterduration });
if (node.total_alarmfilterduration >= node.alarmfilterduration) {
if (!node.isNodeInAlarm) {
// Emit alarm
if (node.timer_alarmfilterperiod !== null) clearTimeout(node.timer_alarmfilterperiod);
node.isRunningTimerFilterPeriod = false;
node.isNodeInAlarm = true;
node.total_alarmfilterduration = 0;
node.setNodeStatus({ fill: "red", shape: "dot", text: "Zone " + node.currentAlarmMSG.zone + " alarm" });
node.send([node.currentAlarmMSG, null, null]);
} else { node.total_alarmfilterduration = 0; }
}
}
}
}, 1000);
}
// This timer resets the node.total_alarmfilterduration
const startTimerAlarmFilterPeriod = () => {
node.isRunningTimerFilterPeriod = true;
node.total_alarmfilterduration = 0;
node.timer_alarmfilterperiod = setTimeout(() => {
node.total_alarmfilterduration = 0;
node.isRunningTimerFilterPeriod = false;
}, node.alarmfilterperiod * 1000);
}
// If filter is enabled, start timer
if (node.alarmfilterduration !== 0) startTimerAlarmFilterDuration();
// Called from config node, to send output to the flow
node.sendPayload = (_msg, extension = '') => {
if (_msg === null || _msg === undefined) return;
if (_msg.hasOwnProperty("errorDescription")) {
logDebug(`Connection status message: ${_msg.errorDescription || ""}`);
_msg.topic = node.topic;
node.send([null, _msg, null]);
return;
}; // It's a connection error/restore comunication.
if (!_msg.hasOwnProperty("payload") || (_msg.hasOwnProperty("payload") && _msg.payload === undefined)) {
logDebug("Discarded incoming message without payload");
return;
}
if (_msg.type === 'img') {
_msg.topic = node.topic;
_msg.extension = extension;
logDebug("Forwarding image payload");
node.send([null, null, _msg]);
return;
}
var oRetMsg = {}; // Return message
var sEventType = "";
var bAlarmStatus = false;
// Check for smart events in JSON or XML-parsed format
if (_msg.type === 'event' && typeof _msg.payload === 'object') {
const payload = _msg.payload;
// Based on research, smart events might come in a 'SmartEvent' wrapper (JSON) or 'EventNotificationAlert' (XML)
const event = payload.SmartEvent || payload.EventNotificationAlert || payload;
if (event.hasOwnProperty("eventType") && event.hasOwnProperty("eventState")) {
sEventType = event.eventType.toString().toLowerCase();
// Ignore basic events like plain motion/videoloss: they are handled by the standard Alarm node
const basicEventTypes = ["vmd", "videoloss"];
if (basicEventTypes.includes(sEventType)) {
logDebug(`Ignoring basic event type ${sEventType} on Intelligent node`);
return;
}
// Filter channel
let sChannelID = event.channelID || event.dynChannelID || "0";
// Filter regionID (Zone)
let iRegionID = 0;
let oDetectionRegionEntry = null;
if (event.hasOwnProperty("DetectionRegionList") && event.DetectionRegionList.hasOwnProperty("DetectionRegionEntry")) {
oDetectionRegionEntry = event.DetectionRegionList.DetectionRegionEntry;
if (Array.isArray(oDetectionRegionEntry)) oDetectionRegionEntry = oDetectionRegionEntry[0];
}
if (oDetectionRegionEntry && oDetectionRegionEntry.hasOwnProperty("regionID")) iRegionID = Number(oDetectionRegionEntry.regionID);
if (Number(node.channelID) === 0 || Number(node.channelID) === Number(sChannelID)) {
// Filter by object type (human/vehicle)
let sHumanReadableObjectType = "";
if (event.hasOwnProperty("customData") && event.customData.hasOwnProperty("objectType")) {
sHumanReadableObjectType = event.customData.objectType.toString().toLowerCase(); // should be "person" or "vehicle"
} else if (event.hasOwnProperty("targetType")) {
sHumanReadableObjectType = event.targetType.toString().toLowerCase(); // should be "human" or "vehicle"
}
let bObjectMatch = false;
const hasClassification = sHumanReadableObjectType && sHumanReadableObjectType.length > 0;
if (hasClassification) {
if (node.intelligentevent === "all") {
bObjectMatch = true;
} else if (node.intelligentevent === "human" && (sHumanReadableObjectType.includes("person") || sHumanReadableObjectType.includes("human"))) {
bObjectMatch = true;
} else if (node.intelligentevent === "vehicle" && sHumanReadableObjectType.includes("vehicle")) {
bObjectMatch = true;
} else if (node.intelligentevent === "animal" && sHumanReadableObjectType.includes("zoology")) {
bObjectMatch = true;
}
} else {
// No object classification available; accept only if user selected "all"
// (and basic events like VMD/videoloss have already been filtered out above)
if (node.intelligentevent === "all") {
bObjectMatch = true;
}
}
if (bObjectMatch) {
bAlarmStatus = (event.eventState.toString().toLowerCase() === "active" ? true : false);
oRetMsg.payload = bAlarmStatus;
oRetMsg.topic = node.topic;
oRetMsg.channelid = sChannelID;
oRetMsg.zone = iRegionID;
oRetMsg.description = getIntelligentEventDescription(event);
// Attach full parsed event for downstream processing
oRetMsg.event = event;
// Try to extract image-related info (if provided by the camera)
try {
let imageName = null;
let imageUrl = null;
// Common direct fields
if (event.picName !== undefined && event.picName !== null) {
imageName = event.picName.toString();
}
if (event.picUrl !== undefined && event.picUrl !== null) {
imageUrl = event.picUrl.toString();
}
// Variants with different casing
if (!imageUrl && event.PicUrl) imageUrl = event.PicUrl.toString();
if (!imageUrl && event.picURL) imageUrl = event.picURL.toString();
if (!imageUrl && event.PictureURL) imageUrl = event.PictureURL.toString();
// Sometimes nested inside customData
if (event.customData) {
if (!imageName && event.customData.picName) {
imageName = event.customData.picName.toString();
}
if (!imageUrl && event.customData.picUrl) {
imageUrl = event.customData.picUrl.toString();
}
}
if (imageName) {
oRetMsg.imageName = imageName;
}
if (imageUrl) {
oRetMsg.imageUrl = imageUrl;
}
} catch (err) {
// Ignore errors while extracting optional image info
}
logDebug(`Event ${sEventType} state ${bAlarmStatus ? "active" : "inactive"} channel ${sChannelID} zone ${iRegionID} object ${sHumanReadableObjectType}`);
}
}
}
}
// Check whether the filter is enabled or not
if (oRetMsg.hasOwnProperty("payload")) {
if (node.alarmfilterduration == 0) {
// No filter, send message immediately
const fill = oRetMsg.payload ? "red" : "green";
const shape = oRetMsg.payload ? "dot" : "ring";
node.setNodeStatus({ fill: fill, shape: shape, text: oRetMsg.description + " Ch: " + oRetMsg.channelid + " Zone: " + oRetMsg.zone });
node.send([oRetMsg, null, null]);
logDebug(`Forwarded alarm immediately (${oRetMsg.description || "no description"})`);
} else {
// Filter is enabled, handle debounce logic
node.currentAlarmMSG = oRetMsg;
if (oRetMsg.payload === false && node.isNodeInAlarm) {
node.send([oRetMsg, null, null]);
node.currentAlarmMSG = {};
node.isNodeInAlarm = false;
logDebug("Alarm reset forwarded after filter duration");
node.setNodeStatus({ fill: "green", shape: "ring", text: `Reset ${oRetMsg.description}` });
} else if (oRetMsg.payload === true && !node.isNodeInAlarm) {
if (!node.isRunningTimerFilterPeriod) {
startTimerAlarmFilterPeriod();
logDebug("Alarm filter timer started");
}
}
}
}
}
// On each deploy, unsubscribe+resubscribe
if (node.server) {
node.server.removeClient(node);
node.server.addClient(node);
}
node.on("close", function (done) {
if (node.timer_alarmfilterduration !== null) clearInterval(node.timer_alarmfilterduration);
if (node.timer_alarmfilterperiod !== null) clearTimeout(node.timer_alarmfilterperiod);
if (node.server) {
node.server.removeClient(node);
}
done();
});
}
RED.nodes.registerType("hikvisionUltimateIntelligent", hikvisionUltimateIntelligent);
}