From a414f98c56efaec65c1d732eaf6729e779011411 Mon Sep 17 00:00:00 2001 From: Mitchell R Date: Sat, 23 May 2026 01:39:22 +0200 Subject: [PATCH] feat(events): dedup ONVIF events within 2s window (Hikvision double-fire fix) --- server/src/plugins/service-api-http/index.ts | 22 ++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/server/src/plugins/service-api-http/index.ts b/server/src/plugins/service-api-http/index.ts index cebe87c..0007977 100644 --- a/server/src/plugins/service-api-http/index.ts +++ b/server/src/plugins/service-api-http/index.ts @@ -275,6 +275,9 @@ function registerPairingRoutes( // ---- Kiosk routes (require Bearer kiosk key) -------------------------------- +// Event deduplication cache: key → last-seen timestamp (ms). +const eventDedupCache = new Map(); + function registerKioskRoutes( app: H3, repo: Repository, @@ -530,6 +533,25 @@ function registerKioskRoutes( if (!body?.topic) throw createError({ statusCode: 400, statusMessage: "topic required" }); + // Dedup: Hikvision cameras send duplicate ONVIF events within ~1s. + // Key = kiosk_id:camera_id:topic:source_keys_hash. Window = 2s. + const dedupKey = `${kiosk.id}:${body.camera_id ?? 0}:${body.topic}:${JSON.stringify(body.payload?.["source"] ?? "")}`; + const now = Date.now(); + if (eventDedupCache.has(dedupKey)) { + const lastSeen = eventDedupCache.get(dedupKey)!; + if (now - lastSeen < 2000) { + return { ok: true, event_id: null, deduplicated: true }; + } + } + eventDedupCache.set(dedupKey, now); + // Trim cache periodically (prevent unbounded growth). + if (eventDedupCache.size > 10_000) { + const cutoff = now - 5000; + for (const [k, v] of eventDedupCache) { + if (v < cutoff) eventDedupCache.delete(k); + } + } + const eventId = repo.insertEvent({ source_kiosk_id: kiosk.id, source_camera_id: body.camera_id ?? null,