Trigger ISS SecurOS Event

Trigger a Analytics Event in ISS SecurOS VMS

Overview

This node triggers a event in ISS SecureOS VMS based on specified trigger conditions. The event can be used to generate alerts, review incidents and more.

Inputs & Outputs

  • Inputs : 1, Media Format : Raw Video
  • Outputs : 1, Media Format: Raw Video
  • Output Metadata : None

Properties

PropertyDescriptionTypeDefaultRequired
enabledIf false, this node will be disabled and will not send any alarms.
Ex. false
booleantrueYes
iss_ipISS SecurOS Server Hostname or IPstringnullYes
iss_portISS SecurOS Server Portstring3000Yes
iss_passwordISS SecurOS authentication passwordstringnullYes
iss_camera_idsComma-separated list of Camera IDs from ISS SecurOS to associate with events. Leave blank to skip camera association.stringnullNo
iss_actionAction to perform in ISS SecurOS. Options: Create event only (event), Create event and record (event_and_record), Create event, record and add subtitle (event_and_record_and_subtitle)enum"event"No
record_durationRecording duration in seconds when recording is enabledinteger5No
event_sourceGenerate ISS SecurOS Events from events generated by other nodes or a Custom defined event. Options: Events from previous nodes (built_in), Custom Event (custom)enumbuilt_inYes
intervalMin. time between consecutive eventsfloat0Yes
triggerSend an event when this condition evaluates to true. Required when event_source is "custom".trigger-conditionnullYes
event_nameEvent name that shows up in ISS SecurOS console. Accepts templates. Required when event_source is custom.stringnullYes
event_descriptionEvent description that shows up in ISS SecurOS console. Accepts templates.textnullNo

Event Description Template

event_description uses Jinja2 Template Syntax for customizing the body of the Event description sent to Wave.

Variables available to the template:

VariableDescription
deployment_idDeployment ID.
deployment_nameDeployment Name.
application_idApplication ID.
application_nameApplication Name.
gateway_idGateway ID.
gateway_nameGateway Name.
node_idNode ID.
meta.nodesNode metadata.
meta.objectsObject metadata.
meta.custom_propertyCustom properties inserted using a Custom function.

Examples

Static text content:

Fire was detected in the parking lot

Insert description from an existing node:

{{ nodes.gpt1.fullframe.label }}

Metadata

Metadata PropertyDescription
NoneNone

ISS SecurOS Configuration

Install nodejs modules in SecureOS

  1. Installing additional packages in Windows OS To install the package, do the following:
  • Open a command line in administrator mode and navigate to the working directory of scripts (C:\Program Files (x86)\ISS\SecurOS\bin64\node.js).
  • Enter the command to run the npm installation script: .\npm.bat install express selfsigned
  1. Installing additional packages in Linux OS To install the package, do the following:
  • Open a command prompt and navigate to the working directory of scripts (node.js).
  • Enter the command to run the npm installation script: sudo ./npm.sh install express selfsigned

Add nodejs script for lumeo events

Copy the following script into a new nodejs script within SecureOS configuration under Integration & Automation section. Remember to change the default auth token to some random value and provide that to this node's configuration in Lumeo.

// server.js
// Express webhook → Securos actions/events
// One server mode only: HTTP or HTTPS (toggle via USE_HTTPS=true)
// If HTTPS and cert/key not found, auto-generate a self-signed cert.
// Mapping: event_type="VCA_EVENT" always; event_name→comment; event_description→description.

const fs = require('fs');
const path = require('path');
const http = require('http');
const https = require('https');
const express = require('express');
const securos = require('securos');
// npm i selfsigned
const selfsigned = require('selfsigned');

const AUTH_TOKEN = process.env.AUTH_TOKEN || 'lumeo'; // required. Set it to a random string.
const USE_HTTPS = true;
const PORT = Number(3443);


// HTTPS cert paths (used only if USE_HTTPS=true)
const SSL_KEY = process.env.SSL_KEY || './key.pem';
const SSL_CERT = process.env.SSL_CERT || './cert.pem';

// ---- Securos connection ----
let core = null;
securos.connect((c) => {
  core = c;
  console.log('[securos] connected');
});

const app = express();
app.use(express.json());

// ---- helpers ----
function assertCoreReady(res) {
  if (!core) {
    res.status(503).json({ error: 'securos core not ready' });
    return false;
  }
  return true;
}

function verifyAuth(req, res) {
  if (!AUTH_TOKEN) {
    res.status(500).json({ error: 'server misconfigured: AUTH_TOKEN missing' });
    return false;
  }
  const h = req.get('authorization') || req.get('Authorization');
  if (!h) return res.status(401).json({ error: 'missing Authorization header' });

  if (h.startsWith('Bearer ')) {
    const tok = h.slice(7).trim();
    if (tok === AUTH_TOKEN) return true;
  } else if (h.startsWith('Basic ')) {
    try {
      const decoded = Buffer.from(h.slice(6).trim(), 'base64').toString('utf8');
      const [u, p] = decoded.split(':', 2);
      if (u === AUTH_TOKEN || p === AUTH_TOKEN) return true;
    } catch {}
  }
  res.status(401).json({ error: 'invalid Authorization' });
  return false;
}

const toMs = (v, dflt) => {
  const n = Number(v);
  return Number.isFinite(n) && n >= 0 ? n*1000 : dflt;
};
const escHtml = (s = '') =>
  String(s)
    .replaceAll('&', '&')
    .replaceAll('<', '&lt;')
    .replaceAll('>', '&gt;')
    .replaceAll('"', '&quot;')
    .replaceAll("'", '&#39;');
const asArray = (x) => (Array.isArray(x) ? x : x == null ? [] : [x]);

// ---- endpoints ----
app.get('/health', (_req, res) => res.json({ ok: true, coreReady: !!core, https: USE_HTTPS }));

app.post('/lumeoevent', async (req, res) => {
  if (!assertCoreReady(res)) return;
  if (!verifyAuth(req, res)) return;

  const {
    camera_id,
    event_name,           // → comment.description
    event_description,    // → comment.comment
    event_duration,       // seconds    
    trigger_recording,    // boolean
    add_subtitle,         // boolean
    visualization,        // optional -> comment.visualization
    cams                  // optional passthrough
  } = req.body || {};

  const camsList = asArray(camera_id).map(String);
  if (camsList.length === 0) {
    return res.status(400).json({ error: 'camera_id is required' });
  }

  const eventName = event_name ? String(event_name) : 'Lumeo Event';
  const eventDescription = event_description ? String(event_description) : '';  
  const eventMs = toMs(event_duration, 10_000);

  const secureOsCommentField = JSON.stringify({ comment: eventDescription, 
                                                description: eventName, 
                                                visualization: visualization });

  const results = [];

  for (const camId of camsList) {
    const type = 'CAM';
    const id = camId;

    if (trigger_recording) {
      try {
        await core.doReact(type, id, 'REC', { hot_rec_time: eventMs });
        setTimeout(() => { try { core.doReact(type, id, 'REC_STOP'); } catch {} }, eventMs + 500);
      } catch (e) {
        results.push({ camera_id: camId, step: 'REC', error: String(e?.message || e) });
      }
    }

    if (add_subtitle) {
      try {
        await core.doReact(type, id, 'CLEAR_SUBTITLES');
        const html = `<p style="color:red;">${escHtml(eventName || 'Event')}</p>` +
                     (eventDescription ? `<p>${escHtml(eventDescription)}</p>` : '');
        await core.doReact(type, id, 'ADD_SUBTITLES', { command: html });
        setTimeout(() => { try { core.doReact(type, id, 'CLEAR_SUBTITLES'); } catch {} }, eventMs);
      } catch (e) {
        results.push({ camera_id: camId, step: 'SUBTITLES', error: String(e?.message || e) });
      }
    }

    try {
      await core.sendEvent(type, id, 'VCA_EVENT', {
        comment: secureOsCommentField,
        //description,
        cams
      });
      results.push({ camera_id: camId, step: 'EVENT', ok: true });
    } catch (e) {
      results.push({ camera_id: camId, step: 'EVENT', error: String(e?.message || e) });
    }
  }

  res.json({ ok: true, results });
});

// ---- server bootstrap (one mode only) ----
function ensureSelfSignedCertSync(keyPath, certPath) {
  const dir = path.dirname(path.resolve(keyPath));
  if (!fs.existsSync(keyPath) || !fs.existsSync(certPath)) {
    if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
    const attrs = [{ name: 'commonName', value: 'localhost' }];
    const pems = selfsigned.generate(attrs, {
      days: 365,
      keySize: 2048,
      algorithm: 'sha256',
      extensions: [{ name: 'basicConstraints', cA: false }],
    });
    fs.writeFileSync(keyPath, pems.private);
    fs.writeFileSync(certPath, pems.cert);
    console.log(`[https] generated self-signed cert at ${keyPath}, ${certPath}`);
  }
  return {
    key: fs.readFileSync(keyPath),
    cert: fs.readFileSync(certPath),
  };
}

if (USE_HTTPS) {
  const tlsOpts = ensureSelfSignedCertSync(SSL_KEY, SSL_CERT);
  https.createServer(tlsOpts, app).listen(PORT, () => {
    console.log(`[https] listening on :${PORT}`);
  });
} else {
  http.createServer(app).listen(PORT, () => {
    console.log(`[http] listening on :${PORT}`);
  });
}

/*
ENV:
  # HTTP
  AUTH_TOKEN=shh PORT=3000 node server.js

  # HTTPS (auto self-signed if missing)
  USE_HTTPS=true AUTH_TOKEN=shh SSL_KEY=./key.pem SSL_CERT=./cert.pem PORT=3443 node server.js

Test (Bearer):
  curl -k -X POST https://localhost:3443/lumeoevent \
    -H 'Authorization: Bearer shh' \
    -H 'Content-Type: application/json' \
    -d '{"camera_id":["1","2"],"event_name":"Intrusion","event_description":"Person at gate","trigger_recording":true,"recording_duration":12000,"add_subtitle":true,"subtitle_duration":4000,"visualization":"rect:10,10,30,40;color:255,0,0"}'

Note: Basic also accepted (token as username or password).
*/