mirror of
https://github.com/BetterCorp/BetterFrame.git
synced 2026-05-26 17:56:34 +00:00
feat(nodered): motion + ANPR + generic ONVIF event trigger nodes
Three new Node-RED trigger nodes in BetterFrame Triggers palette: bf-trigger-motion (red) — fires on MotionAlarm, CellMotionDetector, VideoAnalytics/Motion, FieldDetector topics. Outputs msg.active (true/false) for motion start/stop. Camera ID filter optional. bf-trigger-anpr (blue) — fires on LicensePlateRecognition, Plate, ANPR, LPR, NumberPlate topics. Extracts msg.plate (string) and msg.confidence (number) from vendor-specific payload fields (Hikvision PlateNumber, Dahua plateNumber, etc.). Camera ID filter. bf-trigger-event (green) — generic catch-all. Topic substring filter + camera ID filter. Outputs msg.source + msg.data as key-value objects parsed from ONVIF SimpleItems. Use for line crossing, intrusion, digital input, tamper, audio detection, or any unknown topic. Server side: ONVIF events (source_type=onvif) now additionally forward to the fixed onvif.event route so all three nodes receive events without needing per-topic Node-RED route registration.
This commit is contained in:
parent
cc24eb14fc
commit
82ef29a23d
8 changed files with 405 additions and 1 deletions
|
|
@ -24,7 +24,10 @@
|
||||||
"bf-config-get": "src/bf-config-get.js",
|
"bf-config-get": "src/bf-config-get.js",
|
||||||
"bf-config-set": "src/bf-config-set.js",
|
"bf-config-set": "src/bf-config-set.js",
|
||||||
"bf-status": "src/bf-status.js",
|
"bf-status": "src/bf-status.js",
|
||||||
"bf-snapshot": "src/bf-snapshot.js"
|
"bf-snapshot": "src/bf-snapshot.js",
|
||||||
|
"bf-trigger-motion": "src/bf-trigger-motion.js",
|
||||||
|
"bf-trigger-anpr": "src/bf-trigger-anpr.js",
|
||||||
|
"bf-trigger-event": "src/bf-trigger-event.js"
|
||||||
},
|
},
|
||||||
"icons": [
|
"icons": [
|
||||||
"icons"
|
"icons"
|
||||||
|
|
|
||||||
42
nodered/src/bf-trigger-anpr.html
Normal file
42
nodered/src/bf-trigger-anpr.html
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
<script type="text/javascript">
|
||||||
|
RED.nodes.registerType("bf-trigger-anpr", {
|
||||||
|
category: "BetterFrame Triggers",
|
||||||
|
color: "#99ccff",
|
||||||
|
defaults: {
|
||||||
|
name: { value: "" },
|
||||||
|
camera_id: { value: "" },
|
||||||
|
},
|
||||||
|
inputs: 0,
|
||||||
|
outputs: 1,
|
||||||
|
icon: "betterframe.svg",
|
||||||
|
label: function () {
|
||||||
|
return this.name || "ANPR Trigger";
|
||||||
|
},
|
||||||
|
paletteLabel: "ANPR Trigger",
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script type="text/html" data-template-name="bf-trigger-anpr">
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
||||||
|
<input type="text" id="node-input-name" placeholder="(optional)" />
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-camera_id"><i class="fa fa-video-camera"></i> Camera ID</label>
|
||||||
|
<input type="number" id="node-input-camera_id" placeholder="(blank = all cameras)" />
|
||||||
|
</div>
|
||||||
|
<div class="form-tips">
|
||||||
|
Fires on ONVIF license plate recognition events. Outputs
|
||||||
|
<code>msg.plate</code> (string) and <code>msg.confidence</code> (number).
|
||||||
|
Works with Hikvision ANPR cameras (PlateNumber field), Dahua (plateNumber),
|
||||||
|
and other ONVIF-compliant vendors.
|
||||||
|
</div>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script type="text/html" data-help-name="bf-trigger-anpr">
|
||||||
|
<p>Triggers on ONVIF ANPR (Automatic Number Plate Recognition) events.</p>
|
||||||
|
<p>Matches topics containing: LicensePlateRecognition, Plate, ANPR, LPR, NumberPlate.</p>
|
||||||
|
<p><b>msg.plate</b> contains the recognized plate string (or null if not parsed).
|
||||||
|
<b>msg.confidence</b> is the recognition confidence score (or null).</p>
|
||||||
|
<p>Use downstream nodes for plate allowlist/blocklist matching, logging, or gate control.</p>
|
||||||
|
</script>
|
||||||
89
nodered/src/bf-trigger-anpr.js
Normal file
89
nodered/src/bf-trigger-anpr.js
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
/**
|
||||||
|
* bf-trigger-anpr — fires on ONVIF license plate recognition events.
|
||||||
|
*
|
||||||
|
* Matches topics containing: LicensePlateRecognition, Plate, ANPR, LPR.
|
||||||
|
* Extracts plate number + confidence from payload data.
|
||||||
|
*
|
||||||
|
* Output: { topic, kiosk_id, camera_id, plate, confidence, payload }
|
||||||
|
*/
|
||||||
|
const { readJsonBody } = require("./_http-body.js");
|
||||||
|
|
||||||
|
const ANPR_PATTERNS = [
|
||||||
|
"LicensePlateRecognition",
|
||||||
|
"Plate",
|
||||||
|
"ANPR",
|
||||||
|
"LPR",
|
||||||
|
"NumberPlate",
|
||||||
|
];
|
||||||
|
|
||||||
|
module.exports = function (RED) {
|
||||||
|
const ROUTE = "/api/internal/onvif.anpr";
|
||||||
|
|
||||||
|
function BfTriggerAnprNode(config) {
|
||||||
|
RED.nodes.createNode(this, config);
|
||||||
|
const node = this;
|
||||||
|
const filterCam = config.camera_id ? Number(config.camera_id) : null;
|
||||||
|
|
||||||
|
async function handler(req, res) {
|
||||||
|
const body = await readJsonBody(req);
|
||||||
|
const topic = String(body.topic || "");
|
||||||
|
|
||||||
|
if (!ANPR_PATTERNS.some((p) => topic.includes(p))) {
|
||||||
|
return res.status(200).end();
|
||||||
|
}
|
||||||
|
|
||||||
|
const cameraId = body.camera_id ?? body.source_camera_id ?? null;
|
||||||
|
if (filterCam !== null && Number(cameraId) !== filterCam) {
|
||||||
|
return res.status(200).end();
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = body.payload?.data ?? body.payload ?? {};
|
||||||
|
// Hikvision uses PlateNumber, other vendors may vary.
|
||||||
|
const plate = data.PlateNumber ?? data.plateNumber ?? data.Plate
|
||||||
|
?? data.plate ?? data.Value ?? null;
|
||||||
|
const confidence = data.Confidence ?? data.confidence
|
||||||
|
?? data.Score ?? data.score ?? null;
|
||||||
|
|
||||||
|
const out = {
|
||||||
|
topic,
|
||||||
|
kiosk_id: body.kiosk_id ?? body.source_kiosk_id ?? null,
|
||||||
|
camera_id: cameraId,
|
||||||
|
plate: plate ? String(plate) : null,
|
||||||
|
confidence: confidence != null ? Number(confidence) : null,
|
||||||
|
payload: body.payload ?? body,
|
||||||
|
};
|
||||||
|
node.status({
|
||||||
|
fill: plate ? "blue" : "yellow",
|
||||||
|
shape: "dot",
|
||||||
|
text: plate || "plate detected",
|
||||||
|
});
|
||||||
|
node.send(out);
|
||||||
|
res.status(200).end();
|
||||||
|
}
|
||||||
|
|
||||||
|
RED.httpNode.post(ROUTE, handler);
|
||||||
|
|
||||||
|
const GENERIC_ROUTE = "/api/internal/onvif.event";
|
||||||
|
RED.httpNode.post(GENERIC_ROUTE, handler);
|
||||||
|
|
||||||
|
node.on("close", function (done) {
|
||||||
|
const stack = RED.httpNode?._router?.stack;
|
||||||
|
if (stack) {
|
||||||
|
for (let i = stack.length - 1; i >= 0; i--) {
|
||||||
|
const layer = stack[i];
|
||||||
|
if (!layer?.route) continue;
|
||||||
|
if (layer.route.path !== ROUTE && layer.route.path !== GENERIC_ROUTE) continue;
|
||||||
|
const inner = layer.route.stack;
|
||||||
|
if (Array.isArray(inner)) {
|
||||||
|
for (let j = inner.length - 1; j >= 0; j--) {
|
||||||
|
if (inner[j]?.handle === handler) inner.splice(j, 1);
|
||||||
|
}
|
||||||
|
if (inner.length === 0) stack.splice(i, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
RED.nodes.registerType("bf-trigger-anpr", BfTriggerAnprNode);
|
||||||
|
};
|
||||||
56
nodered/src/bf-trigger-event.html
Normal file
56
nodered/src/bf-trigger-event.html
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
<script type="text/javascript">
|
||||||
|
RED.nodes.registerType("bf-trigger-event", {
|
||||||
|
category: "BetterFrame Triggers",
|
||||||
|
color: "#ccddaa",
|
||||||
|
defaults: {
|
||||||
|
name: { value: "" },
|
||||||
|
camera_id: { value: "" },
|
||||||
|
topic_filter: { value: "" },
|
||||||
|
},
|
||||||
|
inputs: 0,
|
||||||
|
outputs: 1,
|
||||||
|
icon: "betterframe.svg",
|
||||||
|
label: function () {
|
||||||
|
if (this.name) return this.name;
|
||||||
|
if (this.topic_filter) return "Event: " + this.topic_filter;
|
||||||
|
return "ONVIF Event";
|
||||||
|
},
|
||||||
|
paletteLabel: "ONVIF Event",
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script type="text/html" data-template-name="bf-trigger-event">
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
||||||
|
<input type="text" id="node-input-name" placeholder="(optional)" />
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-camera_id"><i class="fa fa-video-camera"></i> Camera ID</label>
|
||||||
|
<input type="number" id="node-input-camera_id" placeholder="(blank = all cameras)" />
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-topic_filter"><i class="fa fa-filter"></i> Topic Filter</label>
|
||||||
|
<input type="text" id="node-input-topic_filter" placeholder="e.g. LineCrossing, DigitalInput" />
|
||||||
|
</div>
|
||||||
|
<div class="form-tips">
|
||||||
|
Generic ONVIF event trigger. Fires on any event topic from any camera.
|
||||||
|
Use <b>Topic Filter</b> for substring matching (e.g. <code>LineCrossing</code>,
|
||||||
|
<code>DigitalInput</code>, <code>Tamper</code>). Leave blank for all events.
|
||||||
|
Outputs <code>msg.source</code> (source SimpleItems) and <code>msg.data</code>
|
||||||
|
(data SimpleItems) as key-value objects.
|
||||||
|
</div>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script type="text/html" data-help-name="bf-trigger-event">
|
||||||
|
<p>Generic catch-all for ONVIF events not covered by the specialized
|
||||||
|
Motion or ANPR trigger nodes.</p>
|
||||||
|
<p>Common topic filters:</p>
|
||||||
|
<ul>
|
||||||
|
<li><code>LineDetector/Crossed</code> — line crossing</li>
|
||||||
|
<li><code>FieldDetector/ObjectsInside</code> — intrusion zone</li>
|
||||||
|
<li><code>Device/Trigger/DigitalInput</code> — physical I/O</li>
|
||||||
|
<li><code>Tamper</code> — camera tamper detection</li>
|
||||||
|
<li><code>AudioDetection</code> — audio analytics</li>
|
||||||
|
</ul>
|
||||||
|
<p>Leave filter blank to receive ALL events from ALL cameras.</p>
|
||||||
|
</script>
|
||||||
76
nodered/src/bf-trigger-event.js
Normal file
76
nodered/src/bf-trigger-event.js
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
/**
|
||||||
|
* bf-trigger-event — fires on ANY ONVIF event from any camera.
|
||||||
|
*
|
||||||
|
* Generic catch-all for event topics not handled by the specialized
|
||||||
|
* motion/ANPR nodes. Configurable topic filter (substring match) and
|
||||||
|
* camera ID filter.
|
||||||
|
*
|
||||||
|
* Output: { topic, kiosk_id, camera_id, source, data, payload }
|
||||||
|
*/
|
||||||
|
const { readJsonBody } = require("./_http-body.js");
|
||||||
|
|
||||||
|
module.exports = function (RED) {
|
||||||
|
const ROUTE = "/api/internal/onvif.event";
|
||||||
|
|
||||||
|
function BfTriggerEventNode(config) {
|
||||||
|
RED.nodes.createNode(this, config);
|
||||||
|
const node = this;
|
||||||
|
const filterCam = config.camera_id ? Number(config.camera_id) : null;
|
||||||
|
const filterTopic = (config.topic_filter || "").trim();
|
||||||
|
|
||||||
|
async function handler(req, res) {
|
||||||
|
const body = await readJsonBody(req);
|
||||||
|
const topic = String(body.topic || "");
|
||||||
|
|
||||||
|
if (filterTopic && !topic.includes(filterTopic)) {
|
||||||
|
return res.status(200).end();
|
||||||
|
}
|
||||||
|
|
||||||
|
const cameraId = body.camera_id ?? body.source_camera_id ?? null;
|
||||||
|
if (filterCam !== null && Number(cameraId) !== filterCam) {
|
||||||
|
return res.status(200).end();
|
||||||
|
}
|
||||||
|
|
||||||
|
const source = body.payload?.source ?? {};
|
||||||
|
const data = body.payload?.data ?? {};
|
||||||
|
|
||||||
|
const out = {
|
||||||
|
topic,
|
||||||
|
kiosk_id: body.kiosk_id ?? body.source_kiosk_id ?? null,
|
||||||
|
camera_id: cameraId,
|
||||||
|
source,
|
||||||
|
data,
|
||||||
|
payload: body.payload ?? body,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Show the topic in the node status badge.
|
||||||
|
const shortTopic = topic.length > 40
|
||||||
|
? "..." + topic.slice(topic.length - 37)
|
||||||
|
: topic;
|
||||||
|
node.status({ fill: "green", shape: "dot", text: shortTopic });
|
||||||
|
node.send(out);
|
||||||
|
res.status(200).end();
|
||||||
|
}
|
||||||
|
|
||||||
|
RED.httpNode.post(ROUTE, handler);
|
||||||
|
|
||||||
|
node.on("close", function (done) {
|
||||||
|
const stack = RED.httpNode?._router?.stack;
|
||||||
|
if (stack) {
|
||||||
|
for (let i = stack.length - 1; i >= 0; i--) {
|
||||||
|
const layer = stack[i];
|
||||||
|
if (!layer?.route || layer.route.path !== ROUTE) continue;
|
||||||
|
const inner = layer.route.stack;
|
||||||
|
if (Array.isArray(inner)) {
|
||||||
|
for (let j = inner.length - 1; j >= 0; j--) {
|
||||||
|
if (inner[j]?.handle === handler) inner.splice(j, 1);
|
||||||
|
}
|
||||||
|
if (inner.length === 0) stack.splice(i, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
RED.nodes.registerType("bf-trigger-event", BfTriggerEventNode);
|
||||||
|
};
|
||||||
41
nodered/src/bf-trigger-motion.html
Normal file
41
nodered/src/bf-trigger-motion.html
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
<script type="text/javascript">
|
||||||
|
RED.nodes.registerType("bf-trigger-motion", {
|
||||||
|
category: "BetterFrame Triggers",
|
||||||
|
color: "#ff9999",
|
||||||
|
defaults: {
|
||||||
|
name: { value: "" },
|
||||||
|
camera_id: { value: "" },
|
||||||
|
},
|
||||||
|
inputs: 0,
|
||||||
|
outputs: 1,
|
||||||
|
icon: "betterframe.svg",
|
||||||
|
label: function () {
|
||||||
|
return this.name || "Motion Trigger";
|
||||||
|
},
|
||||||
|
paletteLabel: "Motion Trigger",
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script type="text/html" data-template-name="bf-trigger-motion">
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
||||||
|
<input type="text" id="node-input-name" placeholder="(optional)" />
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-camera_id"><i class="fa fa-video-camera"></i> Camera ID</label>
|
||||||
|
<input type="number" id="node-input-camera_id" placeholder="(blank = all cameras)" />
|
||||||
|
</div>
|
||||||
|
<div class="form-tips">
|
||||||
|
Fires on ONVIF motion detection events (MotionAlarm, CellMotionDetector,
|
||||||
|
VideoAnalytics/Motion, FieldDetector). Outputs <code>msg.active</code>
|
||||||
|
(true/false) for motion start/stop. Leave Camera ID blank for all cameras.
|
||||||
|
</div>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script type="text/html" data-help-name="bf-trigger-motion">
|
||||||
|
<p>Triggers on ONVIF motion events from cameras connected via BetterFrame kiosks.</p>
|
||||||
|
<p>Matches event topics containing: MotionAlarm, CellMotionDetector,
|
||||||
|
VideoAnalytics, FieldDetector.</p>
|
||||||
|
<p><b>msg.active</b> is <code>true</code> when motion starts, <code>false</code>
|
||||||
|
when it clears. Use a switch node downstream to filter on active only.</p>
|
||||||
|
</script>
|
||||||
89
nodered/src/bf-trigger-motion.js
Normal file
89
nodered/src/bf-trigger-motion.js
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
/**
|
||||||
|
* bf-trigger-motion — fires on ONVIF motion detection events.
|
||||||
|
*
|
||||||
|
* Matches topics containing: MotionAlarm, CellMotionDetector, Motion,
|
||||||
|
* VideoAnalytics/Motion. Camera ID filter optional.
|
||||||
|
*
|
||||||
|
* Output: { topic, kiosk_id, camera_id, active: boolean, payload }
|
||||||
|
*/
|
||||||
|
const { readJsonBody } = require("./_http-body.js");
|
||||||
|
|
||||||
|
const MOTION_PATTERNS = [
|
||||||
|
"MotionAlarm",
|
||||||
|
"CellMotionDetector",
|
||||||
|
"VideoAnalytics",
|
||||||
|
"Motion",
|
||||||
|
"FieldDetector",
|
||||||
|
];
|
||||||
|
|
||||||
|
module.exports = function (RED) {
|
||||||
|
const ROUTE = "/api/internal/onvif.motion";
|
||||||
|
|
||||||
|
function BfTriggerMotionNode(config) {
|
||||||
|
RED.nodes.createNode(this, config);
|
||||||
|
const node = this;
|
||||||
|
const filterCam = config.camera_id ? Number(config.camera_id) : null;
|
||||||
|
|
||||||
|
async function handler(req, res) {
|
||||||
|
const body = await readJsonBody(req);
|
||||||
|
const topic = String(body.topic || "");
|
||||||
|
|
||||||
|
// Only fire for motion-related topics.
|
||||||
|
if (!MOTION_PATTERNS.some((p) => topic.includes(p))) {
|
||||||
|
return res.status(200).end();
|
||||||
|
}
|
||||||
|
|
||||||
|
const cameraId = body.camera_id ?? body.source_camera_id ?? null;
|
||||||
|
if (filterCam !== null && Number(cameraId) !== filterCam) {
|
||||||
|
return res.status(200).end();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect active/inactive from payload data.
|
||||||
|
const data = body.payload?.data ?? body.payload ?? {};
|
||||||
|
const active = data.IsMotion === "true" || data.State === "true"
|
||||||
|
|| data.Value === "true" || data.LogicalState === "true"
|
||||||
|
|| data.IsInside === "true";
|
||||||
|
|
||||||
|
const out = {
|
||||||
|
topic,
|
||||||
|
kiosk_id: body.kiosk_id ?? body.source_kiosk_id ?? null,
|
||||||
|
camera_id: cameraId,
|
||||||
|
active,
|
||||||
|
payload: body.payload ?? body,
|
||||||
|
};
|
||||||
|
node.status({
|
||||||
|
fill: active ? "red" : "green",
|
||||||
|
shape: "dot",
|
||||||
|
text: active ? "Motion detected" : "Clear",
|
||||||
|
});
|
||||||
|
node.send(out);
|
||||||
|
res.status(200).end();
|
||||||
|
}
|
||||||
|
|
||||||
|
RED.httpNode.post(ROUTE, handler);
|
||||||
|
|
||||||
|
// Also listen on the generic onvif event route as fallback.
|
||||||
|
const GENERIC_ROUTE = "/api/internal/onvif.event";
|
||||||
|
RED.httpNode.post(GENERIC_ROUTE, handler);
|
||||||
|
|
||||||
|
node.on("close", function (done) {
|
||||||
|
const stack = RED.httpNode?._router?.stack;
|
||||||
|
if (stack) {
|
||||||
|
for (let i = stack.length - 1; i >= 0; i--) {
|
||||||
|
const layer = stack[i];
|
||||||
|
if (!layer?.route) continue;
|
||||||
|
if (layer.route.path !== ROUTE && layer.route.path !== GENERIC_ROUTE) continue;
|
||||||
|
const inner = layer.route.stack;
|
||||||
|
if (Array.isArray(inner)) {
|
||||||
|
for (let j = inner.length - 1; j >= 0; j--) {
|
||||||
|
if (inner[j]?.handle === handler) inner.splice(j, 1);
|
||||||
|
}
|
||||||
|
if (inner.length === 0) stack.splice(i, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
RED.nodes.registerType("bf-trigger-motion", BfTriggerMotionNode);
|
||||||
|
};
|
||||||
|
|
@ -600,12 +600,20 @@ function registerKioskRoutes(
|
||||||
camera_id: body.camera_id ?? null,
|
camera_id: body.camera_id ?? null,
|
||||||
source_type: body.source_type ?? "system",
|
source_type: body.source_type ?? "system",
|
||||||
property_op: body.property_op ?? null,
|
property_op: body.property_op ?? null,
|
||||||
|
topic: body.topic,
|
||||||
payload: body.payload ?? {},
|
payload: body.payload ?? {},
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
source: "kiosk",
|
source: "kiosk",
|
||||||
};
|
};
|
||||||
nodered.forward(body.topic, out, markForwarded);
|
nodered.forward(body.topic, out, markForwarded);
|
||||||
mqtt.publishEvent(kiosk.id, body.topic, out);
|
mqtt.publishEvent(kiosk.id, body.topic, out);
|
||||||
|
|
||||||
|
// ONVIF events: also forward to the fixed onvif.event route so the
|
||||||
|
// bf-trigger-motion / bf-trigger-anpr / bf-trigger-event nodes
|
||||||
|
// receive them without needing per-topic route registration.
|
||||||
|
if (body.source_type === "onvif") {
|
||||||
|
nodered.forward("onvif.event", out);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { ok: true, event_id: eventId };
|
return { ok: true, event_id: eventId };
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue