BetterFrame/nodered/src/bf-kiosk-camera-event.js
Mitchell R 55b11f2ffa
Some checks are pending
release / meta (push) Waiting to run
release / build (push) Blocked by required conditions
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>
2026-05-26 15:38:30 +02:00

75 lines
2.8 KiB
JavaScript

/**
* bf-kiosk-camera-event — fires on kiosk-originated camera events forwarded
* from the BetterFrame server (ONVIF motion, object detection, line crossing,
* GPIO pulse tagged to a camera, etc.).
*
* The server's api-http `/api/kiosk/events` endpoint persists each kiosk
* event then calls `nodered.forward(topic, payload)` which POSTs to
* `${noderedUrl}/in/<topic>`. This node self-registers a POST handler at a
* fixed route — no upstream `http in` node required.
*
* Optional config:
* - camera_id: only fire for that camera id
*
* Output msg shape (kept compatible with the previous filter version):
* { topic, kiosk_id, camera_id, source_type, payload }
*/
const { readJsonBody } = require("./_http-body.js");
module.exports = function (RED) {
// Fixed ingest route. Server-side forwarders that want this node to receive
// their event should POST to /in/camera.event. (Previous releases used a
// glob-pattern filter over upstream http-in messages; that path is gone.)
const TOPIC = "camera.event";
const ROUTE = "/api/internal/" + TOPIC;
function BfKioskCameraEventNode(config) {
RED.nodes.createNode(this, config);
const node = this;
const filterId = (config.camera_id || "").toString().trim() || null;
async function handler(req, res) {
const body = await readJsonBody(req);
const kioskId = body.kiosk_id !== undefined ? body.kiosk_id
: body.source_kiosk_id !== undefined ? body.source_kiosk_id
: null;
const cameraId = body.camera_id !== undefined ? body.camera_id
: body.source_camera_id !== undefined ? body.source_camera_id
: null;
if (filterId !== null && String(cameraId) !== filterId) {
return res.status(200).end();
}
const out = {
topic: body.topic ? String(body.topic) : TOPIC,
kiosk_id: kioskId,
camera_id: cameraId,
source_type: body.source_type || null,
payload: body.payload !== undefined ? body.payload : body,
};
node.status({ fill: "green", shape: "dot", text: out.topic });
node.send(out);
res.status(200).end();
}
RED.httpNode.post(ROUTE, handler);
node.on("close", function (done) {
const stack = RED.httpNode && RED.httpNode._router && RED.httpNode._router.stack;
if (stack) {
for (let i = stack.length - 1; i >= 0; i--) {
const layer = stack[i];
if (!layer || !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] && inner[j].handle === handler) inner.splice(j, 1);
}
if (inner.length === 0) stack.splice(i, 1);
}
}
}
done();
});
}
RED.nodes.registerType("bf-kiosk-camera-event", BfKioskCameraEventNode);
};