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.
2026-05-23 00:17:05 +00:00
|
|
|
/**
|
|
|
|
|
* 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;
|
2026-05-26 13:38:30 +00:00
|
|
|
const filterCam = config.camera_id ? String(config.camera_id).trim() : null;
|
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.
2026-05-23 00:17:05 +00:00
|
|
|
|
|
|
|
|
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;
|
2026-05-26 13:38:30 +00:00
|
|
|
if (filterCam !== null && String(cameraId) !== filterCam) {
|
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.
2026-05-23 00:17:05 +00:00
|
|
|
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);
|
|
|
|
|
};
|