Real Hikvision/Dahua camera driver; gate Backend-push-IP on capability
Replace the camera stub with HttpCamera: Hikvision ISAPI and Dahua CGI snapshots over client-side HTTP Digest (new drivers/http-digest.ts). healthCheck() now pulls a real frame instead of returning ready/stub. Snapshot carries bytes (driver fetches); storage/imageRef is the caller's job, keeping the adapter free of storage deps. Fix the cosmetic Backend-push-IP field: add pushesToBackend to DeviceDriver (only Dingtian sets it), expose as pushCapable in the catalog, and gate the wizard's backend-IP fetch + field on it so pull-only devices hide it. Verified on hardware (Hikvision 10.0.10.121): healthCheck ready, captureSnapshot returns a valid JPEG.
This commit is contained in:
@@ -78,6 +78,7 @@ export function SetupWizard() {
|
||||
noun={noun}
|
||||
entries={catalog[key]}
|
||||
discoverableIds={catalog.discoverable}
|
||||
pushCapableIds={catalog.pushCapable}
|
||||
assignments={assignments.filter((a) => a.category === key && a.lane === lane)}
|
||||
onChanged={reloadState}
|
||||
/>
|
||||
@@ -93,6 +94,7 @@ function CategorySection({
|
||||
noun,
|
||||
entries,
|
||||
discoverableIds,
|
||||
pushCapableIds,
|
||||
assignments,
|
||||
onChanged,
|
||||
}: {
|
||||
@@ -102,6 +104,7 @@ function CategorySection({
|
||||
noun: string;
|
||||
entries: CatalogEntry[];
|
||||
discoverableIds: string[];
|
||||
pushCapableIds: string[];
|
||||
assignments: Assignment[];
|
||||
onChanged: () => Promise<void> | void;
|
||||
}) {
|
||||
@@ -155,6 +158,7 @@ function CategorySection({
|
||||
category={category}
|
||||
entries={entries}
|
||||
discoverableIds={discoverableIds}
|
||||
pushCapableIds={pushCapableIds}
|
||||
onSaved={async (w) => {
|
||||
setWarnings(w);
|
||||
await onChanged();
|
||||
@@ -227,6 +231,7 @@ function DeviceForm({
|
||||
category,
|
||||
entries,
|
||||
discoverableIds,
|
||||
pushCapableIds,
|
||||
onSaved,
|
||||
onCancel,
|
||||
}: {
|
||||
@@ -234,12 +239,17 @@ function DeviceForm({
|
||||
category: DeviceCategory;
|
||||
entries: CatalogEntry[];
|
||||
discoverableIds: string[];
|
||||
pushCapableIds: string[];
|
||||
onSaved: (warnings: string[]) => Promise<void> | void;
|
||||
onCancel?: () => void;
|
||||
}) {
|
||||
const [selectedId, setSelectedId] = useState<string>("");
|
||||
const selected = entries.find((e) => e.id === selectedId);
|
||||
const canDiscover = selected != null && discoverableIds.includes(selected.id);
|
||||
// Only push-capable drivers (e.g. the Dingtian relay) call back to the
|
||||
// backend and need a backend IP. Pull-only devices (cameras, commanded relays)
|
||||
// must NOT show the field. See wiki/concepts/device-input-flow.md.
|
||||
const pushesToBackend = selected != null && pushCapableIds.includes(selected.id);
|
||||
|
||||
// Config values (auto-filled by discovery, editable by hand).
|
||||
const [config, setConfig] = useState<Record<string, string | number>>({});
|
||||
@@ -255,15 +265,16 @@ function DeviceForm({
|
||||
// Backend push IP: which of OUR addresses the device should call back on. We
|
||||
// auto-pick the NIC on the device's subnet, but surface it editable here so a
|
||||
// multi-NIC host can be corrected (the chosen IP is baked into the device on
|
||||
// save). Only relevant for drivers that push (the field hides if no candidates).
|
||||
// save). Only relevant for drivers that push back to us (pushesToBackend).
|
||||
const [backendIps, setBackendIps] = useState<BackendIpCandidate[] | null>(null);
|
||||
const [backendIp, setBackendIp] = useState<string>("");
|
||||
|
||||
// (Re)load backend-IP candidates whenever the device host changes after a
|
||||
// successful test (the test confirms the host is real + reachable).
|
||||
// successful test (the test confirms the host is real + reachable) — but only
|
||||
// for push-capable drivers; a pull-only device never calls back.
|
||||
const testedHost = tested ? String(mergedConfig().host ?? "") : "";
|
||||
useEffect(() => {
|
||||
if (!testedHost) {
|
||||
if (!testedHost || !pushesToBackend) {
|
||||
setBackendIps(null);
|
||||
return;
|
||||
}
|
||||
@@ -281,7 +292,7 @@ function DeviceForm({
|
||||
live = false;
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [testedHost]);
|
||||
}, [testedHost, pushesToBackend]);
|
||||
|
||||
function selectDriver(id: string) {
|
||||
setSelectedId(id);
|
||||
|
||||
@@ -96,6 +96,8 @@ export type DeviceCategory = "access" | "reader" | "camera" | "printer";
|
||||
export type Catalog = Record<DeviceCategory, CatalogEntry[]> & {
|
||||
/** Driver ids that support LAN discovery. */
|
||||
discoverable: string[];
|
||||
/** Driver ids that push to the backend (need a backend IP at assign time). */
|
||||
pushCapable: string[];
|
||||
};
|
||||
|
||||
export function fetchCatalog(): Promise<Catalog> {
|
||||
|
||||
Reference in New Issue
Block a user