fix(onvif): import discovered cameras as type=onvif with credentials

importDiscoveredCamera was hardcoded to type="rtsp", losing ONVIF
identity. Camera edit showed RTSP fields, ONVIF event subscription
skipped (checks cam_type=="onvif"), re-discovery impossible.

Now creates type="onvif" with onvif_host/port/username/password stored
on the camera row. Streams still go into camera_streams (unchanged).
Bundle ships onvif fields → kiosk subscribes to PullPoint events.

Also passes host + port as hidden form fields from discover results
page so the add handler has them available. Basic manual camera
creation via UI stays rtsp-only (simpler); discovery flow produces
onvif type.
This commit is contained in:
Mitchell R 2026-05-22 18:30:41 +02:00
parent 2e40e78413
commit 05ca368f29
No known key found for this signature in database
2 changed files with 16 additions and 4 deletions

View file

@ -174,6 +174,8 @@ function parseDiscoveredStreams(raw: string): DiscoverAddStream[] {
function importDiscoveredCamera( function importDiscoveredCamera(
deps: AdminDeps, deps: AdminDeps,
rawName: string, rawName: string,
onvifHost: string,
onvifPort: number,
username: string, username: string,
password: string, password: string,
streams: DiscoverAddStream[], streams: DiscoverAddStream[],
@ -185,9 +187,13 @@ function importDiscoveredCamera(
const cam = deps.repo.createCamera({ const cam = deps.repo.createCamera({
name, name,
type: "rtsp", type: "onvif",
rtsp_url: mainRtspUrl, rtsp_url: mainRtspUrl,
}); onvif_host: onvifHost,
onvif_port: onvifPort,
onvif_username: username,
onvif_password: password,
} as any);
for (const stream of streams) { for (const stream of streams) {
const width = stream.width == null ? null : Number(stream.width); const width = stream.width == null ? null : Number(stream.width);
const height = stream.height == null ? null : Number(stream.height); const height = stream.height == null ? null : Number(stream.height);
@ -587,6 +593,7 @@ export function registerAdminRoutes(app: H3, deps: AdminDeps): void {
return htmlPage(CameraDiscoverResultsPage({ return htmlPage(CameraDiscoverResultsPage({
user: user.username, user: user.username,
host, host,
port,
username, username,
password, password,
cameras, cameras,
@ -603,6 +610,8 @@ export function registerAdminRoutes(app: H3, deps: AdminDeps): void {
app.post("/admin/cameras/discover/add", async (event) => { app.post("/admin/cameras/discover/add", async (event) => {
const body = await readBody<Record<string, string | string[]>>(event); const body = await readBody<Record<string, string | string[]>>(event);
const onvifHost = formValue(body?.["host"]).trim();
const onvifPort = parseInt(formValue(body?.["port"]) || "80", 10) || 80;
const username = formValue(body?.["username"]).trim(); const username = formValue(body?.["username"]).trim();
const password = formValue(body?.["password"]); const password = formValue(body?.["password"]);
let imported = 0; let imported = 0;
@ -613,7 +622,7 @@ export function registerAdminRoutes(app: H3, deps: AdminDeps): void {
const rawName = formValue(body?.[`camera_${idx}_name`]).trim() || "ONVIF camera"; const rawName = formValue(body?.[`camera_${idx}_name`]).trim() || "ONVIF camera";
const streams = parseDiscoveredStreams(formValue(body?.[`camera_${idx}_streams_json`])); const streams = parseDiscoveredStreams(formValue(body?.[`camera_${idx}_streams_json`]));
if (streams.length === 0) continue; if (streams.length === 0) continue;
const camId = importDiscoveredCamera(deps, rawName, username, password, streams); const camId = importDiscoveredCamera(deps, rawName, onvifHost, onvifPort, username, password, streams);
if (camId != null) { if (camId != null) {
deps.nodered.forward("camera.changed", { camera_id: camId, event: "created", source: "server" }); deps.nodered.forward("camera.changed", { camera_id: camId, event: "created", source: "server" });
} }
@ -623,7 +632,7 @@ export function registerAdminRoutes(app: H3, deps: AdminDeps): void {
const rawName = formValue(body?.["name"]).trim() || "ONVIF camera"; const rawName = formValue(body?.["name"]).trim() || "ONVIF camera";
const streams = parseDiscoveredStreams(formValue(body?.["streams_json"])); const streams = parseDiscoveredStreams(formValue(body?.["streams_json"]));
if (streams.length > 0) { if (streams.length > 0) {
const camId = importDiscoveredCamera(deps, rawName, username, password, streams); const camId = importDiscoveredCamera(deps, rawName, onvifHost, onvifPort, username, password, streams);
if (camId != null) { if (camId != null) {
deps.nodered.forward("camera.changed", { camera_id: camId, event: "created", source: "server" }); deps.nodered.forward("camera.changed", { camera_id: camId, event: "created", source: "server" });
} }

View file

@ -313,6 +313,7 @@ interface DiscoveredCameraRow {
interface CameraDiscoverResultsProps { interface CameraDiscoverResultsProps {
user: string; user: string;
host: string; host: string;
port?: number;
username: string; username: string;
password: string; password: string;
cameras: DiscoveredCameraRow[]; cameras: DiscoveredCameraRow[];
@ -438,6 +439,8 @@ export function CameraDiscoverResultsPage(props: CameraDiscoverResultsProps) {
<div class="card" style="text-align:center; color:#999; padding:2rem">No profiles returned</div> <div class="card" style="text-align:center; color:#999; padding:2rem">No profiles returned</div>
) : ( ) : (
<form method="post" action="/admin/cameras/discover/add"> <form method="post" action="/admin/cameras/discover/add">
<input type="hidden" name="host" value={props.host} />
<input type="hidden" name="port" value={String(props.port ?? 80)} />
<input type="hidden" name="username" value={props.username} /> <input type="hidden" name="username" value={props.username} />
<input type="hidden" name="password" value={props.password} /> <input type="hidden" name="password" value={props.password} />
<div style="display:flex; gap:0.5rem; align-items:center; margin-bottom:1rem; flex-wrap:wrap"> <div style="display:flex; gap:0.5rem; align-items:center; margin-bottom:1rem; flex-wrap:wrap">