fix: Node-RED event forwarding + parse ONVIF Key section (PlateNumber)

Server bridge was forwarding to raw topic paths that no Node-RED node
listens on. Now forwards to fixed routes: camera.event, onvif.event,
onvif.motion, onvif.anpr — matching what trigger nodes register.

ONVIF XML parser now extracts Key section SimpleItems (PlateNumber,
etc.) into the data map alongside Data section items. Previously only
parsed Source and Data, missing Key-section fields like plate numbers.

Node-RED trigger nodes: camera_id filter changed from Number() to
String() comparison for UUIDv7 compatibility.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Mitchell R 2026-05-26 15:38:30 +02:00
parent eb8abbdff9
commit 488a0ffc3c
No known key found for this signature in database
6 changed files with 16 additions and 12 deletions

View file

@ -448,6 +448,11 @@ fn parse_notification_messages(xml: &str) -> Vec<OnvifEvent> {
source.insert(name, value); source.insert(name, value);
} }
} }
if let Some(key_block) = extract_section(block, "Key") {
for (name, value) in parse_simple_items(&key_block) {
data.insert(name, value);
}
}
if let Some(data_block) = extract_section(block, "Data") { if let Some(data_block) = extract_section(block, "Data") {
for (name, value) in parse_simple_items(&data_block) { for (name, value) in parse_simple_items(&data_block) {
data.insert(name, value); data.insert(name, value);

View file

@ -26,8 +26,7 @@ module.exports = function (RED) {
function BfKioskCameraEventNode(config) { function BfKioskCameraEventNode(config) {
RED.nodes.createNode(this, config); RED.nodes.createNode(this, config);
const node = this; const node = this;
const filterIdRaw = (config.camera_id || "").toString().trim(); const filterId = (config.camera_id || "").toString().trim() || null;
const filterId = filterIdRaw && !isNaN(Number(filterIdRaw)) ? Number(filterIdRaw) : null;
async function handler(req, res) { async function handler(req, res) {
const body = await readJsonBody(req); const body = await readJsonBody(req);
@ -37,7 +36,7 @@ module.exports = function (RED) {
const cameraId = body.camera_id !== undefined ? body.camera_id const cameraId = body.camera_id !== undefined ? body.camera_id
: body.source_camera_id !== undefined ? body.source_camera_id : body.source_camera_id !== undefined ? body.source_camera_id
: null; : null;
if (filterId !== null && Number(cameraId) !== filterId) { if (filterId !== null && String(cameraId) !== filterId) {
return res.status(200).end(); return res.status(200).end();
} }
const out = { const out = {

View file

@ -22,7 +22,7 @@ module.exports = function (RED) {
function BfTriggerAnprNode(config) { function BfTriggerAnprNode(config) {
RED.nodes.createNode(this, config); RED.nodes.createNode(this, config);
const node = this; const node = this;
const filterCam = config.camera_id ? Number(config.camera_id) : null; const filterCam = config.camera_id ? String(config.camera_id).trim() : null;
async function handler(req, res) { async function handler(req, res) {
const body = await readJsonBody(req); const body = await readJsonBody(req);
@ -33,7 +33,7 @@ module.exports = function (RED) {
} }
const cameraId = body.camera_id ?? body.source_camera_id ?? null; const cameraId = body.camera_id ?? body.source_camera_id ?? null;
if (filterCam !== null && Number(cameraId) !== filterCam) { if (filterCam !== null && String(cameraId) !== filterCam) {
return res.status(200).end(); return res.status(200).end();
} }

View file

@ -15,7 +15,7 @@ module.exports = function (RED) {
function BfTriggerEventNode(config) { function BfTriggerEventNode(config) {
RED.nodes.createNode(this, config); RED.nodes.createNode(this, config);
const node = this; const node = this;
const filterCam = config.camera_id ? Number(config.camera_id) : null; const filterCam = config.camera_id ? String(config.camera_id).trim() : null;
const filterTopic = (config.topic_filter || "").trim(); const filterTopic = (config.topic_filter || "").trim();
async function handler(req, res) { async function handler(req, res) {
@ -27,7 +27,7 @@ module.exports = function (RED) {
} }
const cameraId = body.camera_id ?? body.source_camera_id ?? null; const cameraId = body.camera_id ?? body.source_camera_id ?? null;
if (filterCam !== null && Number(cameraId) !== filterCam) { if (filterCam !== null && String(cameraId) !== filterCam) {
return res.status(200).end(); return res.status(200).end();
} }

View file

@ -22,7 +22,7 @@ module.exports = function (RED) {
function BfTriggerMotionNode(config) { function BfTriggerMotionNode(config) {
RED.nodes.createNode(this, config); RED.nodes.createNode(this, config);
const node = this; const node = this;
const filterCam = config.camera_id ? Number(config.camera_id) : null; const filterCam = config.camera_id ? String(config.camera_id).trim() : null;
async function handler(req, res) { async function handler(req, res) {
const body = await readJsonBody(req); const body = await readJsonBody(req);
@ -34,7 +34,7 @@ module.exports = function (RED) {
} }
const cameraId = body.camera_id ?? body.source_camera_id ?? null; const cameraId = body.camera_id ?? body.source_camera_id ?? null;
if (filterCam !== null && Number(cameraId) !== filterCam) { if (filterCam !== null && String(cameraId) !== filterCam) {
return res.status(200).end(); return res.status(200).end();
} }

View file

@ -735,11 +735,11 @@ function registerKioskRoutes(
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 nodered.forward("camera.event", out);
// bf-trigger-motion / bf-trigger-anpr / bf-trigger-event nodes
// receive them without needing per-topic route registration.
if (body.source_type === "onvif") { if (body.source_type === "onvif") {
nodered.forward("onvif.event", out); nodered.forward("onvif.event", out);
nodered.forward("onvif.motion", out);
nodered.forward("onvif.anpr", out);
} }
} }