UNPKG

node-red-contrib-snap4city-milestone

Version:

Node-Red integration to communicate with Milestone XProtect VMS

266 lines (244 loc) 9.74 kB
const fetch = require('node-fetch'); const { v4: uuidv4 } = require('uuid'); const https = require('https'); const { xml2json } = require('xml-js'); const Gateway = require('./xprotect-gateway.js'); //functions to use in node-red blocks module.exports = { //Return JSON of array where each array contains an alarm line getAlarmList: async function (tokenSOAP, hostname, port, maxLines, order, target) { let list; const resSession = await getAlarmLines(tokenSOAP, hostname, port, maxLines, order, target); if (typeof resSession !== 'object') { list = { "error": resSession.split(', reason: ').shift(), "description": resSession.split('reason: ').pop() } } else { const xmlList = await resSession.text(); const jsonList = JSON.parse(xml2json(xmlList)); if (resSession.status !== 200) { const err = jsonList.elements[0].elements[0].elements[0].elements[1].elements[0].text; const error = resSession.status + ' ' + resSession.statusText; list = { "error": error, "description": err }; } else { list = []; const alarm = new Map(); const alarms = jsonList.elements[0].elements[0].elements[0].elements[0].elements; for (let k = 0; k < alarms.length; k++) { const line = lookup(alarms[k], 'elements')[1]; for (let i = 0; i < line.length; i++) { if (line[i].hasOwnProperty('elements')) { alarm.set((lookup(line[i], 'name')[1].substring(2)), lookup(line[i], 'elements')[1][0].text); } } list.push(Object.fromEntries(alarm)); } } } return list; }, //Get access token for the MIP VMS RESTful API gateway. getTokenREST: async function (username, password, serverUrl) { let token = null; const idpUrl = serverUrl + "/API/IDP/connect/token"; const urlencoded = new URLSearchParams(); urlencoded.append("grant_type", "password"); urlencoded.append("username", username); urlencoded.append("password", password); urlencoded.append("client_id", "GrantValidatorClient"); const httpsAgent = new https.Agent({ rejectUnauthorized: false, }); await fetch(idpUrl, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: urlencoded, agent: httpsAgent }).then(async function (response) { const res = await response; token = res; }).catch(function (error) { const msg = "Failed to retrieve token - " + error; token = msg; }); return token; }, //Get access token for the MIPS VMS SOAP service getTokenSOAP: async function (username, password, serverUrl) { let token = null; const idpUrl = serverUrl + "/ManagementServer/ServerCommandService.svc"; const payload = loginXML(); const auth = Buffer.from(username + ":" + password).toString('base64') const httpsAgent = new https.Agent({ rejectUnauthorized: false, }); await fetch(idpUrl, { method: 'POST', headers: { 'Content-Type': 'text/xml', 'SOAPAction': 'http://videoos.net/2/XProtectCSServerCommand/IServerCommandService/Login', 'Authorization': `Basic ${auth}` }, body: payload, agent: httpsAgent }).then(async function (response) { const res = await response; token = res; }).catch(function (error) { const msg = "Failed to retrieve token: " + error; token = msg; }); return token; }, //send and trigger an Analytic Event through an XML file sendXML: async function (access_token, guid, name, hostname, description ,port, serverUrl) { let checkName = false; let events; let xmlres = null; const api_gateway = new Gateway(serverUrl); //check if exist an Analytic event with this name const res = await api_gateway.getAllEvents(access_token); if (res.status === 200) { events = await res.json(); } for (let i = 0; i < events.array.length; i++) { if (events.array[i].displayName === name) { checkName = true; } } if (checkName) { const url = "http://" + hostname + ":" + port; const xml = eventXML(guid, name, description); await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/xml' }, body: xml }).then(async function (response) { const res = await response; xmlres = res; }).catch(function (err) { const msg = "Connection error: " + err xmlres = msg; }); } else { xmlres = "Connection error: the event '" + name + "' doesn't exist" } return xmlres; }, }; //utility functions useful for methods above async function getAlarmLines(tokenSOAP, hostname, port, maxLines, order, target) { const url = "http://" + hostname + ":" + port + "/Central/AlarmServiceToken"; const payload = getXML(tokenSOAP, maxLines, order, target); let lines; await fetch(url, { method: 'POST', headers: { 'Content-Type': 'text/xml', 'SOAPAction': 'http://videoos.net/2/CentralServerAlarmCommand/IAlarmCommandToken/GetAlarmLines', }, body: payload }).then(async function (response) { const res = await response; lines = res; }).catch(function (error) { const msg = ""+error; lines = msg; }); return lines; } function getXML(tokenSOAP, maxLines, order, target) { const xml = '' + '<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">' + ' <s:Body>' + ' <GetAlarmLines xmlns="http://videoos.net/2/CentralServerAlarmCommand">' + ' <token>' + tokenSOAP + '</token>' + ' <from>0</from>' + ' <maxCount>' + maxLines + '</maxCount>' + ' <filter xmlns:a="http://schemas.datacontract.org/2004/07/VideoOS.Platform.Proxy.Alarm" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">' + ' <a:Conditions />' + ' <a:Orders>' + ' <a:OrderBy>' + ' <a:Order>' + order + '</a:Order>' + ' <a:Target>' + target + '</a:Target>' + ' </a:OrderBy>' + ' </a:Orders>' + ' </filter>' + ' </GetAlarmLines>' + ' </s:Body>' + '</s:Envelope>' return xml; } function eventXML(guid, name, description) { const timestamp = new Date().toISOString(); const xml = '' + '<?xml version="1.0" encoding="utf-8"?>' + '<AnalyticsEvent xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="urn:milestone-systems">' + ' <EventHeader>' + ' <ID>00000000-0000-0000-0000-000000000000</ID>' + ' <Timestamp>' + timestamp + '</Timestamp>' + ' <Type>MyType</Type>' + ' <!-- Insert Event Message here -->' + ' <Message>' + name + '</Message>' + ' <CustomTag>TagFromXML</CustomTag>' + ' <Source>' + ' <!-- Insert camera URI here, if you not have the GUID. -->' + ' <!-- (For multichannel devices, URI may contain channel number after ,) -->' + ' <!-- <Name>example192.168.0.199</Name> -->' + ' <FQID>' + ' <!-- Insert camera GUID here, if you have it -->' + ' <ObjectId>' + guid + '</ObjectId>' + ' </FQID>' + ' </Source>' + ' </EventHeader>' + ' <Description>' + description +'</Description>' + ' <Location>' + ' Event location 1' + ' </Location>' + ' <Vendor>' + ' <Name>My Smart Video</Name>' + ' </Vendor>' + '</AnalyticsEvent>' return xml; } function loginXML() { const istanceID = uuidv4(); const xml = '' + '<?xml version="1.0" encoding="utf-8"?>' + '<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xpr="http://videoos.net/2/XProtectCSServerCommand">' + '<soapenv:Header/>' + ' <soapenv:Body>' + ' <xpr:Login>' + ' <xpr:instanceId>' + istanceID + '</xpr:instanceId>' + ' </xpr:Login>' + ' </soapenv:Body>' + '</soapenv:Envelope>' return xml; } function lookup(obj, k) { for (let key in obj) { let value = obj[key]; if (k == key) { return [k, value]; } if (typeof (value) === "object" && !Array.isArray(value)) { let y = lookup(value, k); if (y && y[0] == k) return y; } if (Array.isArray(value)) { for (let i = 0; i < value.length; ++i) { let x = lookup(value[i], k); if (x && x[0] == k) return x; } } } return null; }