UNPKG

node-red-contrib-onvif-nodes

Version:

Node Red nodes for communicating with OnVif compliant IP devices

279 lines (242 loc) 15.2 kB
/** * Copyright 2018 Bart Butenaers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. **/ module.exports = function(RED) { var settings = RED.settings; const onvif = require('onvif'); const utils = require('./utils'); function OnVifEventsNode(config) { RED.nodes.createNode(this, config); this.action = config.action; var node = this; // Retrieve the config node, where the device is configured node.deviceConfig = RED.nodes.getNode(config.deviceConfig); if (node.deviceConfig) { node.listener = function(onvifStatus) { utils.setNodeStatus(node, 'event', onvifStatus); if (onvifStatus !== "connected" && node.eventListener) { // When the device isn't connected anymore, stop listening to events from the camera node.deviceConfig.cam.removeListener('events', node.eventListener); node.eventListener = null; } } // Start listening for Onvif config nodes status changes node.deviceConfig.addListener("onvif_status", node.listener); // Show the current Onvif config node status already utils.setNodeStatus(node, 'event', node.deviceConfig.onvifStatus); node.deviceConfig.initialize(); } node.on("input", function(msg) { var newMsg = {}; // Note: the node's config screen has no 'action' input field yet ... var action = node.action || msg.action; if (!action) { // When no action specified in the node, it should be specified in the msg.action node.error("No action specified (in node or msg)"); return; } // Don't perform these checks when e.g. the device is currently disconnected (because then e.g. no capabilities are loaded yet) if (action !== "reconnect") { if (!node.deviceConfig || node.deviceConfig.onvifStatus != "connected") { node.error("This node is not connected to a device"); return; } if (!utils.hasService(node.deviceConfig.cam, 'event')) { node.error("The device has no support for an event service"); return; } } // Seems that some Axis cams support pull point, although they return WSPullPointSupport 'false' /*if (!node.deviceConfig.cam.capabilities.events.WSPullPointSupport == true) { //console.warn('Ignoring input message since the device does not support pull point subscription'); return; }*/ newMsg.xaddr = this.deviceConfig.xaddress; newMsg.action = action; try { switch (action) { case "start": if (node.eventListener) { node.error("This node is already listening to device events"); return; } // Overwrite the device status text node.status({fill:"green",shape:"dot",text:"listening"}); node.eventListener = function(camMessage) { var sourceName = null; var sourceValue = null; var dataName = null; var dataValue = null; // Events have a Topic // Events have (optionally) a Source, a Key and Data fields // The Source,Key and Data fields can be single items or an array of items // The Source,Key and Data fields can be of type SimpleItem or a Complex Item // - Topic // - Message/Message/$ // - Message/Message/Source... // - Message/Message/Key... // - Message/Message/Data/SimpleItem/[index]/$/name (array of items) // OR - Message/Message/Data/SimpleItem/$/name (single item) // - Message/Message/Data/SimpleItem/[index]/$/value (array of items) // OR - Message/Message/Data/SimpleItem/$/value (single item) var eventTopic = camMessage.topic._; // Strip the namespaces from the topic (e.g. tns1:MediaControl/tnsavg:ConfigurationUpdateAudioEncCfg) // Split on '/', then remove any namespace for each part, and at the end recombine parts that were split with '/' let parts = eventTopic.split('/'); eventTopic = ""; for (var index = 0; index < parts.length; index++) { var stringNoNamespace = parts[index].split(':').pop(); if (eventTopic.length == 0) { eventTopic += stringNoNamespace; } else { eventTopic += '/' + stringNoNamespace; } } var outputMsg = { topic: eventTopic, time: camMessage.message.message.$.UtcTime, property: camMessage.message.message.$.PropertyOperation // Initialized, Deleted or Changed but missing/undefined on the Avigilon 4 channel encoder }; // Only handle simpleItem // Only handle one 'source' item // Ignore the 'key' item (nothing I own produces it) // Handle all the 'Data' items // SOURCE (Name:Value) if (camMessage.message.message.source && camMessage.message.message.source.simpleItem) { if (Array.isArray(camMessage.message.message.source.simpleItem)) { // TODO : currently we only process the first event source item ... outputMsg.source = { name: camMessage.message.message.source.simpleItem[0].$.Name, value: camMessage.message.message.source.simpleItem[0].$.Value } } else { outputMsg.source = { name: camMessage.message.message.source.simpleItem.$.Name, value: camMessage.message.message.source.simpleItem.$.Value } } } //KEY if (camMessage.message.message.key) { outputMsg.key = camMessage.message.message.key; } // DATA (Name:Value) if (camMessage.message.message.data && camMessage.message.message.data.simpleItem) { if (Array.isArray(camMessage.message.message.data.simpleItem)) { for (var x = 0; x < camMessage.message.message.data.simpleItem.length; x++) { outputMsg.data = { name: camMessage.message.message.data.simpleItem[x].$.Name, value: camMessage.message.message.data.simpleItem[x].$.Value } } } else { outputMsg.data = { name: camMessage.message.message.data.simpleItem.$.Name, value: camMessage.message.message.data.simpleItem.$.Value } } } else if (camMessage.message.message.data && camMessage.message.message.data.elementItem) { outputMsg.data = { dataName: 'elementItem', dataValue: JSON.stringify(camMessage.message.message.data.elementItem) } } // As soon as we get an event from the camera, we will send it to the output of this node node.send(outputMsg); } // Start listening to events from the camera node.deviceConfig.cam.on('event', node.eventListener); break; case "stop": if (!node.eventListener) { node.error("This node was not listening to events anyway"); return; } // Stop listening to events from the camera node.deviceConfig.cam.removeListener('event', node.eventListener); node.eventListener = null; // Overwrite the device status text node.status({fill:"green",shape:"ring",text:"not listening"}); break; case "getEventProperties": node.deviceConfig.cam.getEventProperties(function(err, date, xml) { if (!err) { var simplifiedDate = {}; // Simplify the soap message to a compact message, by keeping only all relevant information function simplifyNode(node, simplifiedDateChild) { // loop over all the child nodes in this node for (const child in node) { switch (child) { case "$": // Continue to the next child in the list (same level) continue; case "messageDescription": // Collect the details that belong to the event var source = ''; var date = ''; if (node[child].source && node[child].source.simpleItemDescription) { simplifiedDateChild.source = node[child].source.simpleItemDescription.$; } if (node[child].data && node[child].data.simpleItemDescriptio) { simplifiedDateChild.date = node[child].data.simpleItemDescription.$; } return; default: // Decend recursively into the child node, looking for the messageDescription simplifiedDateChild[child] = {}; simplifyNode(node[child], simplifiedDateChild[child]); } } } simplifyNode(date.topicSet, simplifiedDate) } utils.handleResult(node, err, simplifiedDate, null, newMsg); }); break; case "getEventServiceCapabilities": node.deviceConfig.cam.getEventServiceCapabilities(function(err, date, xml) { utils.handleResult(node, err, date, xml, newMsg); }); break; case "reconnect": node.deviceConfig.cam.connect(function(err) { utils.handleResult(node, err, "", null, newMsg); }); break default: //node.status({fill:"red",shape:"dot",text: "unsupported action"}); node.error("Action " + action + " is not supported"); } } catch (exc) { node.error("Action " + action + " failed: " + exc); } }); node.on("close",function() { if (node.listener) { node.deviceConfig.removeListener("onvif_status", node.listener); } // Stop listening to events from the camera if (node.eventListener) { node.deviceConfig.cam.removeListener('event', node.eventListener); node.eventListener = null; } }); } RED.nodes.registerType("onvif-events",OnVifEventsNode); }