From 9129613920ed97d9af7791983c1f91073678042e Mon Sep 17 00:00:00 2001 From: Mitchell R Date: Thu, 21 May 2026 11:57:38 +0200 Subject: [PATCH] feat(cameras): sync entity name on rename + ONVIF device name from GetDeviceInformation Two fixes: 1. When admin renames a camera, the linked entity's name now syncs automatically so the entity list doesn't drift from the camera list. 2. ONVIF discovery now calls GetDeviceInformation before GetProfiles (best-effort, catches auth-gated devices). Pulls Manufacturer + Model and uses the combined string as the camera's proposed name instead of the raw IP. E.g. "Hikvision DS-2CD2146G2" instead of "192.168.74.8". Falls back to host IP when the device omits the info. --- .../service-admin-http/routes-admin.ts | 8 +++++ server/src/shared/onvif.ts | 32 ++++++++++++++++--- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/server/src/plugins/service-admin-http/routes-admin.ts b/server/src/plugins/service-admin-http/routes-admin.ts index 1a5e89f..a8d6320 100644 --- a/server/src/plugins/service-admin-http/routes-admin.ts +++ b/server/src/plugins/service-admin-http/routes-admin.ts @@ -1317,6 +1317,14 @@ export function registerAdminRoutes(app: H3, deps: AdminDeps): void { }); } } + // Sync entity name when camera name changes. + if (patch["name"]) { + const ent = deps.repo.getEntityForCamera(id); + if (ent && ent.name !== patch["name"]) { + deps.repo.updateEntity(ent.id, { name: patch["name"] } as any); + } + } + notifyKiosks(); deps.nodered.forward("camera.changed", { camera_id: id, event: "updated", source: "server" }); diff --git a/server/src/shared/onvif.ts b/server/src/shared/onvif.ts index f3d8436..17a80fc 100644 --- a/server/src/shared/onvif.ts +++ b/server/src/shared/onvif.ts @@ -278,20 +278,21 @@ function profileGroupKey(profileName: string, sourceToken: string | null, stream return match?.[1] ? `channel:${match[1]}` : "source:default"; } -function groupProfiles(host: string, profiles: DiscoveredProfile[]): DiscoveredCamera[] { +function groupProfiles(host: string, deviceName: string | null, profiles: DiscoveredProfile[]): DiscoveredCamera[] { const groups = new Map(); for (const profile of profiles) { const key = profileGroupKey(profile.profile_name, profile.source_token, profile.stream_uri); groups.set(key, [...(groups.get(key) ?? []), profile]); } + const base = deviceName || host; const out: DiscoveredCamera[] = []; let i = 1; for (const [key, group] of groups) { const sourceToken = group.find((p) => p.source_token)?.source_token ?? null; const name = groups.size === 1 - ? host - : sourceToken ? `${host} ${sourceToken}` : `${host} camera ${String(i)}`; + ? base + : sourceToken ? `${base} ${sourceToken}` : `${base} camera ${String(i)}`; out.push({ name, source_token: sourceToken ?? (key.startsWith("channel:") ? key.slice("channel:".length) : null), @@ -316,6 +317,29 @@ export async function discover(input: DiscoverInput): Promise`); + const devInfoXml = await soap( + endpoint.deviceUrl, + "http://www.onvif.org/ver10/device/wsdl/GetDeviceInformation", + devInfoEnv, + timeoutMs, + input.soapTransport, + ); + // Try Manufacturer + Model as combined name (e.g. "Hikvision DS-2CD2146G2") + const manufacturer = pickAll(devInfoXml, "Manufacturer")[0]?.trim() ?? null; + const model = pickAll(devInfoXml, "Model")[0]?.trim() ?? null; + if (manufacturer && model) { + deviceName = `${manufacturer} ${model}`; + } else { + deviceName = manufacturer ?? model ?? null; + } + } catch { + // Device info is optional — some cameras gate it behind auth or omit it. + } + // ---- GetProfiles ----------------------------------------------------------- const profilesEnv = buildEnvelope(header, ``); const profilesXml = await soap( @@ -403,5 +427,5 @@ export async function discover(input: DiscoverInput): Promise