@camstack/addon-provider-amcrest 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/icon.svg +2 -5
- package/dist/addon.js +446 -35
- package/dist/addon.mjs +446 -36
- package/dist/index.js +1 -0
- package/dist/index.mjs +2 -2
- package/package.json +1 -1
package/assets/icon.svg
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"
|
|
2
|
-
<rect width="48" height="48" rx="10" fill="#16a34a"/>
|
|
3
|
-
<path d="M14 18h13a3 3 0 0 1 3 3v9a3 3 0 0 1-3 3H14a3 3 0 0 1-3-3v-9a3 3 0 0 1 3-3Z" fill="#fff"/>
|
|
4
|
-
<path d="M30 22l6-3v11l-6-3v-5Z" fill="#fff"/>
|
|
5
|
-
<circle cx="20.5" cy="25.5" r="3.5" fill="#16a34a"/>
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
|
|
2
|
+
<image href="data:image/jpeg;base64,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" x="0" y="0" width="48" height="48" preserveAspectRatio="xMidYMid meet"/>
|
|
6
3
|
</svg>
|
package/dist/addon.js
CHANGED
|
@@ -19596,7 +19596,15 @@ var FrameworkPackageStatusSchema = object({
|
|
|
19596
19596
|
latestVersion: string().nullable(),
|
|
19597
19597
|
hasUpdate: boolean(),
|
|
19598
19598
|
/** Optional manifest description for the row tooltip. */
|
|
19599
|
-
description: string().optional()
|
|
19599
|
+
description: string().optional(),
|
|
19600
|
+
/**
|
|
19601
|
+
* Content build-id (md5 of the resolved `dist/` tree) of the code the hub
|
|
19602
|
+
* ACTUALLY loaded. Framework packages ship code changes without always
|
|
19603
|
+
* bumping `currentVersion`, so semver alone hides "same version, new code".
|
|
19604
|
+
* `null` when the dist can't be hashed (not installed / empty). The admin-UI
|
|
19605
|
+
* surfaces this so a stale-code hub is visible even at an unchanged version.
|
|
19606
|
+
*/
|
|
19607
|
+
buildId: string().nullable()
|
|
19600
19608
|
});
|
|
19601
19609
|
var LogStreamEntrySchema = object({
|
|
19602
19610
|
timestamp: string(),
|
|
@@ -26423,6 +26431,54 @@ var AmcrestCgiClient = class AmcrestCgiClient {
|
|
|
26423
26431
|
const body = await this.getText(`/cgi-bin/configManager.cgi?action=setConfig&${query}`);
|
|
26424
26432
|
if (!/\bOK\b/i.test(body)) throw new Error(`setConfig rejected: ${body.trim().slice(0, 200)}`);
|
|
26425
26433
|
}
|
|
26434
|
+
/**
|
|
26435
|
+
* Open the Dahua CGI event-manager attach stream — a long-lived
|
|
26436
|
+
* `multipart/x-mixed-replace` GET that pushes camera events
|
|
26437
|
+
* (`VideoMotion` Start/Stop, heartbeats, …) until the connection drops
|
|
26438
|
+
* or the caller aborts. Performs the same digest handshake as `send`
|
|
26439
|
+
* (absorbing the initial 401) but — critically — installs NO
|
|
26440
|
+
* per-request timeout: the long-poll must never self-cancel, so
|
|
26441
|
+
* teardown is driven solely by `signal.abort()`. `codes` is passed
|
|
26442
|
+
* through unescaped (Dahua requires the literal `[...]` brackets, same
|
|
26443
|
+
* as `setConfig`), e.g. `'[VideoMotion]'` or `'[All]'`.
|
|
26444
|
+
*/
|
|
26445
|
+
async openEventStream(codes, signal) {
|
|
26446
|
+
const path = `/cgi-bin/eventManager.cgi?action=attach&codes=${codes}`;
|
|
26447
|
+
const url = `${this.scheme}://${this.host}:${this.port}${path}`;
|
|
26448
|
+
const firstHeader = buildAuthHeader({
|
|
26449
|
+
state: this.digestState,
|
|
26450
|
+
method: "GET",
|
|
26451
|
+
uri: path,
|
|
26452
|
+
username: this.username,
|
|
26453
|
+
password: this.password
|
|
26454
|
+
});
|
|
26455
|
+
let res = await fetch(url, {
|
|
26456
|
+
method: "GET",
|
|
26457
|
+
headers: firstHeader ? { authorization: firstHeader } : {},
|
|
26458
|
+
signal
|
|
26459
|
+
});
|
|
26460
|
+
if (res.status === 401) {
|
|
26461
|
+
await res.body?.cancel().catch(() => {});
|
|
26462
|
+
const challenge = parseChallenge(res.headers.get("www-authenticate"));
|
|
26463
|
+
if (challenge) {
|
|
26464
|
+
this.digestState.challenge = challenge;
|
|
26465
|
+
this.digestState.nc = 0;
|
|
26466
|
+
const retryHeader = buildAuthHeader({
|
|
26467
|
+
state: this.digestState,
|
|
26468
|
+
method: "GET",
|
|
26469
|
+
uri: path,
|
|
26470
|
+
username: this.username,
|
|
26471
|
+
password: this.password
|
|
26472
|
+
});
|
|
26473
|
+
res = await fetch(url, {
|
|
26474
|
+
method: "GET",
|
|
26475
|
+
headers: retryHeader ? { authorization: retryHeader } : {},
|
|
26476
|
+
signal
|
|
26477
|
+
});
|
|
26478
|
+
}
|
|
26479
|
+
}
|
|
26480
|
+
return res;
|
|
26481
|
+
}
|
|
26426
26482
|
async getEncodeConfig() {
|
|
26427
26483
|
return this.getConfig("Encode");
|
|
26428
26484
|
}
|
|
@@ -26449,21 +26505,26 @@ var AmcrestCgiClient = class AmcrestCgiClient {
|
|
|
26449
26505
|
};
|
|
26450
26506
|
}
|
|
26451
26507
|
/** Begin a continuous PTZ move. `code` is a Dahua direction token
|
|
26452
|
-
* (`Left`/`Right`/`Up`/`Down`). `speed` maps to
|
|
26453
|
-
|
|
26454
|
-
|
|
26508
|
+
* (`Left`/`Right`/`Up`/`Down`/`ZoomTele`/`ZoomWide`). `speed` maps to
|
|
26509
|
+
* arg2 (1..8 typical). `timeoutMs` (optional) lets the caller pass a
|
|
26510
|
+
* short per-tick deadline so a slow/blocked camera round-trip fails
|
|
26511
|
+
* fast rather than hanging the whole cap call. The Dahua `start`
|
|
26512
|
+
* action is fire-and-return — the head keeps moving until a matching
|
|
26513
|
+
* `stop` is sent — so this must resolve quickly. */
|
|
26514
|
+
async ptzStart(channel, code, speed, timeoutMs) {
|
|
26515
|
+
await this.getText(`/cgi-bin/ptz.cgi?action=start&channel=${channel}&code=${code}&arg1=0&arg2=${speed}&arg3=0`, timeoutMs);
|
|
26455
26516
|
}
|
|
26456
|
-
async ptzStop(channel, code) {
|
|
26457
|
-
await this.getText(`/cgi-bin/ptz.cgi?action=stop&channel=${channel}&code=${code}&arg1=0&arg2=0&arg3=0
|
|
26517
|
+
async ptzStop(channel, code, timeoutMs) {
|
|
26518
|
+
await this.getText(`/cgi-bin/ptz.cgi?action=stop&channel=${channel}&code=${code}&arg1=0&arg2=0&arg3=0`, timeoutMs);
|
|
26458
26519
|
}
|
|
26459
|
-
async ptzGotoPreset(channel, presetId) {
|
|
26460
|
-
await this.getText(`/cgi-bin/ptz.cgi?action=start&channel=${channel}&code=GotoPreset&arg1=0&arg2=${presetId}&arg3=0
|
|
26520
|
+
async ptzGotoPreset(channel, presetId, timeoutMs) {
|
|
26521
|
+
await this.getText(`/cgi-bin/ptz.cgi?action=start&channel=${channel}&code=GotoPreset&arg1=0&arg2=${presetId}&arg3=0`, timeoutMs);
|
|
26461
26522
|
}
|
|
26462
|
-
async ptzSetPreset(channel, presetId) {
|
|
26463
|
-
await this.getText(`/cgi-bin/ptz.cgi?action=start&channel=${channel}&code=SetPreset&arg1=0&arg2=${presetId}&arg3=0
|
|
26523
|
+
async ptzSetPreset(channel, presetId, timeoutMs) {
|
|
26524
|
+
await this.getText(`/cgi-bin/ptz.cgi?action=start&channel=${channel}&code=SetPreset&arg1=0&arg2=${presetId}&arg3=0`, timeoutMs);
|
|
26464
26525
|
}
|
|
26465
|
-
async ptzClearPreset(channel, presetId) {
|
|
26466
|
-
await this.getText(`/cgi-bin/ptz.cgi?action=start&channel=${channel}&code=ClearPreset&arg1=0&arg2=${presetId}&arg3=0
|
|
26526
|
+
async ptzClearPreset(channel, presetId, timeoutMs) {
|
|
26527
|
+
await this.getText(`/cgi-bin/ptz.cgi?action=start&channel=${channel}&code=ClearPreset&arg1=0&arg2=${presetId}&arg3=0`, timeoutMs);
|
|
26467
26528
|
}
|
|
26468
26529
|
/** Read stored PTZ presets from the `PtzPreset` config block. */
|
|
26469
26530
|
async getPtzPresets(channel) {
|
|
@@ -26487,6 +26548,131 @@ var AmcrestCgiClient = class AmcrestCgiClient {
|
|
|
26487
26548
|
}));
|
|
26488
26549
|
}
|
|
26489
26550
|
};
|
|
26551
|
+
/**
|
|
26552
|
+
* Subscribe to a camera's Dahua CGI event-manager attach stream. Returns
|
|
26553
|
+
* an `AbortController` — `controller.abort()` tears the subscription
|
|
26554
|
+
* down. Reconnect logic is the caller's responsibility (the parser stays
|
|
26555
|
+
* simple; the device class owns lifecycle / backoff timing). Mirrors the
|
|
26556
|
+
* Hikvision provider's `subscribeAlarms` shape so both HTTP-stream
|
|
26557
|
+
* providers read the same way.
|
|
26558
|
+
*
|
|
26559
|
+
* Unlike Hikvision's XML alert stream, Dahua parts are `text/plain`
|
|
26560
|
+
* key=value blocks — `parseAmcrestEventBody` handles the projection.
|
|
26561
|
+
*/
|
|
26562
|
+
function subscribeAmcrestEvents(client, codes, handlers) {
|
|
26563
|
+
const controller = new AbortController();
|
|
26564
|
+
(async () => {
|
|
26565
|
+
try {
|
|
26566
|
+
const res = await client.openEventStream(codes, controller.signal);
|
|
26567
|
+
if (!res.ok || !res.body) {
|
|
26568
|
+
handlers.onError(/* @__PURE__ */ new Error(`event stream HTTP ${res.status} ${res.statusText}`));
|
|
26569
|
+
return;
|
|
26570
|
+
}
|
|
26571
|
+
const ct = res.headers.get("content-type") ?? "";
|
|
26572
|
+
const boundary = parseBoundary(ct);
|
|
26573
|
+
if (!boundary) {
|
|
26574
|
+
handlers.onError(/* @__PURE__ */ new Error(`event stream missing multipart boundary in Content-Type: "${ct}"`));
|
|
26575
|
+
return;
|
|
26576
|
+
}
|
|
26577
|
+
handlers.onConnected?.();
|
|
26578
|
+
await pumpEventStream(res.body, boundary, handlers);
|
|
26579
|
+
if (!controller.signal.aborted) handlers.onError(/* @__PURE__ */ new Error("event stream ended"));
|
|
26580
|
+
} catch (err) {
|
|
26581
|
+
if (controller.signal.aborted) return;
|
|
26582
|
+
handlers.onError(err);
|
|
26583
|
+
}
|
|
26584
|
+
})();
|
|
26585
|
+
return controller;
|
|
26586
|
+
}
|
|
26587
|
+
function parseBoundary(contentType) {
|
|
26588
|
+
const m = /boundary\s*=\s*"?([^";\s]+)"?/i.exec(contentType);
|
|
26589
|
+
return m ? m[1].trim() : null;
|
|
26590
|
+
}
|
|
26591
|
+
async function pumpEventStream(stream, boundary, handlers) {
|
|
26592
|
+
const reader = stream.getReader();
|
|
26593
|
+
const dashBoundary = `--${boundary}`;
|
|
26594
|
+
let bufferedBytes = new Uint8Array(0);
|
|
26595
|
+
try {
|
|
26596
|
+
while (true) {
|
|
26597
|
+
const { value, done } = await reader.read();
|
|
26598
|
+
if (done) break;
|
|
26599
|
+
if (!value || value.byteLength === 0) continue;
|
|
26600
|
+
bufferedBytes = concatBytes(bufferedBytes, value);
|
|
26601
|
+
const parts = splitOnBoundary(bufferedBytes, dashBoundary);
|
|
26602
|
+
bufferedBytes = parts.tail;
|
|
26603
|
+
for (const block of parts.blocks) {
|
|
26604
|
+
const parsed = parseMultipartBlock(block);
|
|
26605
|
+
if (!parsed) continue;
|
|
26606
|
+
const ev = parseAmcrestEventBody(parsed.bodyText);
|
|
26607
|
+
if (ev) handlers.onEvent(ev);
|
|
26608
|
+
}
|
|
26609
|
+
}
|
|
26610
|
+
} finally {
|
|
26611
|
+
try {
|
|
26612
|
+
reader.releaseLock();
|
|
26613
|
+
} catch {}
|
|
26614
|
+
}
|
|
26615
|
+
}
|
|
26616
|
+
function concatBytes(a, b) {
|
|
26617
|
+
const out = new Uint8Array(a.byteLength + b.byteLength);
|
|
26618
|
+
out.set(a, 0);
|
|
26619
|
+
out.set(b, a.byteLength);
|
|
26620
|
+
return out;
|
|
26621
|
+
}
|
|
26622
|
+
function splitOnBoundary(buf, dashBoundary) {
|
|
26623
|
+
const parts = Buffer.from(buf).toString("binary").split(dashBoundary);
|
|
26624
|
+
if (parts.length <= 1) return {
|
|
26625
|
+
blocks: [],
|
|
26626
|
+
tail: buf
|
|
26627
|
+
};
|
|
26628
|
+
const tail = Buffer.from(parts.pop(), "binary");
|
|
26629
|
+
parts.shift();
|
|
26630
|
+
return {
|
|
26631
|
+
blocks: parts.filter((p) => p.length > 0 && !p.startsWith("--")).map((p) => Buffer.from(p, "binary")),
|
|
26632
|
+
tail
|
|
26633
|
+
};
|
|
26634
|
+
}
|
|
26635
|
+
function parseMultipartBlock(block) {
|
|
26636
|
+
let start = 0;
|
|
26637
|
+
while (start < block.length && (block[start] === 13 || block[start] === 10)) start++;
|
|
26638
|
+
const headerEnd = findHeaderEnd(block, start);
|
|
26639
|
+
const bodyStart = headerEnd < 0 ? start : headerEnd + 4;
|
|
26640
|
+
const bodySlice = block.subarray(bodyStart);
|
|
26641
|
+
return { bodyText: Buffer.from(bodySlice).toString("utf8").replace(/\r?\n--$/m, "").trim() };
|
|
26642
|
+
}
|
|
26643
|
+
function findHeaderEnd(buf, from) {
|
|
26644
|
+
for (let i = from; i + 3 < buf.length; i++) if (buf[i] === 13 && buf[i + 1] === 10 && buf[i + 2] === 13 && buf[i + 3] === 10) return i;
|
|
26645
|
+
return -1;
|
|
26646
|
+
}
|
|
26647
|
+
/**
|
|
26648
|
+
* Parse a Dahua event block body. The salient line is a `;`-delimited
|
|
26649
|
+
* key=value list, e.g. `Code=VideoMotion;action=Start;index=0`. A
|
|
26650
|
+
* trailing multi-line `data={…}` JSON blob (present on some firmwares) is
|
|
26651
|
+
* ignored — motion needs only code/action/index. Returns null when no
|
|
26652
|
+
* `Code=` line is present (e.g. an empty heartbeat frame).
|
|
26653
|
+
*/
|
|
26654
|
+
function parseAmcrestEventBody(body) {
|
|
26655
|
+
const codeLine = body.split(/\r?\n/).find((l) => l.includes("Code=")) ?? "";
|
|
26656
|
+
if (!codeLine) return null;
|
|
26657
|
+
const fields = /* @__PURE__ */ new Map();
|
|
26658
|
+
for (const pair of codeLine.split(";")) {
|
|
26659
|
+
const eq = pair.indexOf("=");
|
|
26660
|
+
if (eq < 0) continue;
|
|
26661
|
+
fields.set(pair.slice(0, eq).trim().toLowerCase(), pair.slice(eq + 1).trim());
|
|
26662
|
+
}
|
|
26663
|
+
const code = fields.get("code");
|
|
26664
|
+
const action = fields.get("action");
|
|
26665
|
+
if (!code || !action) return null;
|
|
26666
|
+
const indexRaw = fields.get("index");
|
|
26667
|
+
const index = indexRaw !== void 0 && /^\d+$/.test(indexRaw) ? Number.parseInt(indexRaw, 10) : null;
|
|
26668
|
+
return {
|
|
26669
|
+
observedAt: Date.now(),
|
|
26670
|
+
code,
|
|
26671
|
+
action,
|
|
26672
|
+
index,
|
|
26673
|
+
raw: body
|
|
26674
|
+
};
|
|
26675
|
+
}
|
|
26490
26676
|
//#endregion
|
|
26491
26677
|
//#region src/amcrest-camera.ts
|
|
26492
26678
|
/**
|
|
@@ -26538,11 +26724,34 @@ var AmcrestCamera = class AmcrestCamera extends BaseDevice {
|
|
|
26538
26724
|
this.registerPtzCap();
|
|
26539
26725
|
this.registerStreamParamsCap();
|
|
26540
26726
|
this.registerMotionZonesCap();
|
|
26727
|
+
this.registerMotionCap();
|
|
26541
26728
|
this.registerPrivacyMaskCap();
|
|
26542
26729
|
this.registerOsdCap();
|
|
26543
26730
|
this.registerDayNightCap();
|
|
26544
26731
|
this.registerImageSettingsCap();
|
|
26545
26732
|
}
|
|
26733
|
+
/** Dahua CGI `eventManager` attach codes — VideoMotion is the base
|
|
26734
|
+
* onboard-motion signal (Start/Stop). Smart sub-codes
|
|
26735
|
+
* (SmartMotionHuman/Vehicle) are camera-AI detections handled by a
|
|
26736
|
+
* separate cap and intentionally out of scope here. */
|
|
26737
|
+
static MOTION_EVENT_CODES = "[VideoMotion]";
|
|
26738
|
+
/** Re-emit interval (ms) for a sustained motion window. Dahua fires a
|
|
26739
|
+
* single `VideoMotion action=Start` for the whole window (no per-tick
|
|
26740
|
+
* repeats), but the pipeline-runner re-arms its `motionCooldownMs` on
|
|
26741
|
+
* every `detected:true`. Without a heartbeat a >cooldown motion window
|
|
26742
|
+
* would lapse the runner back to `watching`; a bounded ~0.1 Hz re-emit
|
|
26743
|
+
* holds it `active` until the `Stop`. */
|
|
26744
|
+
static MOTION_KEEPALIVE_MS = 1e4;
|
|
26745
|
+
/** Active event-stream subscription; aborted on dispose / conn change. */
|
|
26746
|
+
eventStreamController = null;
|
|
26747
|
+
/** Reconnect attempt counter — drives exponential backoff. */
|
|
26748
|
+
eventStreamReconnectAttempts = 0;
|
|
26749
|
+
/** Pending reconnect timer. */
|
|
26750
|
+
eventStreamReconnectTimer = null;
|
|
26751
|
+
/** True between a `VideoMotion Start` and its `Stop`. */
|
|
26752
|
+
motionActive = false;
|
|
26753
|
+
/** Keepalive re-emit timer for a sustained motion window. */
|
|
26754
|
+
motionKeepaliveTimer = null;
|
|
26546
26755
|
/** Single-flight guard for the `stream-params` camera refresh. */
|
|
26547
26756
|
streamParamsRefreshInFlight = null;
|
|
26548
26757
|
/** Single-flight guard for the `motion-zones` camera refresh. */
|
|
@@ -26573,7 +26782,155 @@ var AmcrestCamera = class AmcrestCamera extends BaseDevice {
|
|
|
26573
26782
|
* (possibly just-edited) credentials/host. */
|
|
26574
26783
|
async applySettingsPatch(patch) {
|
|
26575
26784
|
await super.applySettingsPatch(patch);
|
|
26576
|
-
if ("host" in patch || "port" in patch || "https" in patch || "username" in patch || "password" in patch)
|
|
26785
|
+
if ("host" in patch || "port" in patch || "https" in patch || "username" in patch || "password" in patch) {
|
|
26786
|
+
this.clearPtzAutoStop();
|
|
26787
|
+
this.client = null;
|
|
26788
|
+
const hadStream = this.eventStreamController !== null || this.eventStreamReconnectTimer !== null;
|
|
26789
|
+
this.teardownEventSubscription();
|
|
26790
|
+
if (hadStream) this.ensureEventSubscription();
|
|
26791
|
+
}
|
|
26792
|
+
}
|
|
26793
|
+
/** Register the native `motion` cap. Its mere presence (a `kind:'native'`
|
|
26794
|
+
* `motion` binding) is what the orchestrator reads to default a
|
|
26795
|
+
* freshly-adopted camera to `motionSources:['onboard']`. */
|
|
26796
|
+
registerMotionCap() {
|
|
26797
|
+
this.ctx.registerNativeCap(motionCapability, { isDetected: async ({ deviceId }) => {
|
|
26798
|
+
if (deviceId !== this.id) throw new Error(`AmcrestCamera: motion deviceId mismatch, expected ${this.id}, got ${deviceId}`);
|
|
26799
|
+
return this.getCapSlice(motionCapability)?.detected ?? false;
|
|
26800
|
+
} });
|
|
26801
|
+
this.setCapSlice(motionCapability, {
|
|
26802
|
+
detected: false,
|
|
26803
|
+
lastDetectedAt: null,
|
|
26804
|
+
autoClearAfterMs: null
|
|
26805
|
+
});
|
|
26806
|
+
this.ctx.logger.info("amcrest: motion cap registered", { tags: { deviceId: this.id } });
|
|
26807
|
+
}
|
|
26808
|
+
/** Reusable event-source identity for every emit on this device. */
|
|
26809
|
+
eventSource() {
|
|
26810
|
+
return {
|
|
26811
|
+
type: "device",
|
|
26812
|
+
id: this.id,
|
|
26813
|
+
addonId: AMCREST_ADDON_ID,
|
|
26814
|
+
deviceId: this.id
|
|
26815
|
+
};
|
|
26816
|
+
}
|
|
26817
|
+
/** Emit an onboard motion transition on the bus. The runner subscribes
|
|
26818
|
+
* and drives its phase machine via `reportMotion`. */
|
|
26819
|
+
emitMotion(detected, timestamp) {
|
|
26820
|
+
this.ctx.eventBus.emit(createEvent(EventCategory.MotionOnMotionChanged, this.eventSource(), {
|
|
26821
|
+
deviceId: this.id,
|
|
26822
|
+
detected,
|
|
26823
|
+
timestamp,
|
|
26824
|
+
source: "onboard"
|
|
26825
|
+
}));
|
|
26826
|
+
}
|
|
26827
|
+
/** Idempotently open the eventManager attach stream. */
|
|
26828
|
+
ensureEventSubscription() {
|
|
26829
|
+
if (this.eventStreamController) return;
|
|
26830
|
+
const client = this.ensureClient();
|
|
26831
|
+
this.eventStreamController = subscribeAmcrestEvents(client, AmcrestCamera.MOTION_EVENT_CODES, {
|
|
26832
|
+
onConnected: () => {
|
|
26833
|
+
this.eventStreamReconnectAttempts = 0;
|
|
26834
|
+
if (this.motionActive) this.onMotionStop(Date.now());
|
|
26835
|
+
this.ctx.logger.info("amcrest: event stream connected", { tags: { deviceId: this.id } });
|
|
26836
|
+
},
|
|
26837
|
+
onEvent: (ev) => this.handleAmcrestEvent(ev),
|
|
26838
|
+
onError: (err) => {
|
|
26839
|
+
this.ctx.logger.warn("amcrest: event stream error — will reconnect", {
|
|
26840
|
+
tags: { deviceId: this.id },
|
|
26841
|
+
meta: { error: err instanceof Error ? err.message : String(err) }
|
|
26842
|
+
});
|
|
26843
|
+
this.eventStreamController = null;
|
|
26844
|
+
this.scheduleEventReconnect();
|
|
26845
|
+
}
|
|
26846
|
+
});
|
|
26847
|
+
}
|
|
26848
|
+
/** Single pending reconnect timer with capped exponential backoff
|
|
26849
|
+
* (1s → 60s) — same shape as the Hikvision alarm stream. */
|
|
26850
|
+
scheduleEventReconnect() {
|
|
26851
|
+
if (this.eventStreamReconnectTimer) return;
|
|
26852
|
+
const attempts = ++this.eventStreamReconnectAttempts;
|
|
26853
|
+
const delayMs = Math.min(6e4, 1e3 * 2 ** Math.min(attempts, 6));
|
|
26854
|
+
this.eventStreamReconnectTimer = setTimeout(() => {
|
|
26855
|
+
this.eventStreamReconnectTimer = null;
|
|
26856
|
+
this.ensureEventSubscription();
|
|
26857
|
+
}, delayMs);
|
|
26858
|
+
}
|
|
26859
|
+
handleAmcrestEvent(ev) {
|
|
26860
|
+
if (ev.code !== "VideoMotion") {
|
|
26861
|
+
this.ctx.logger.debug("amcrest: unhandled event", {
|
|
26862
|
+
tags: { deviceId: this.id },
|
|
26863
|
+
meta: {
|
|
26864
|
+
code: ev.code,
|
|
26865
|
+
action: ev.action
|
|
26866
|
+
}
|
|
26867
|
+
});
|
|
26868
|
+
return;
|
|
26869
|
+
}
|
|
26870
|
+
const action = ev.action.toLowerCase();
|
|
26871
|
+
if (action === "start" || action === "pulse") this.onMotionStart(ev.observedAt);
|
|
26872
|
+
else if (action === "stop") this.onMotionStop(ev.observedAt);
|
|
26873
|
+
}
|
|
26874
|
+
onMotionStart(now) {
|
|
26875
|
+
const wasActive = this.motionActive;
|
|
26876
|
+
this.motionActive = true;
|
|
26877
|
+
this.emitMotion(true, now);
|
|
26878
|
+
if (!wasActive) {
|
|
26879
|
+
this.setCapSlice(motionCapability, {
|
|
26880
|
+
detected: true,
|
|
26881
|
+
lastDetectedAt: now,
|
|
26882
|
+
autoClearAfterMs: null
|
|
26883
|
+
});
|
|
26884
|
+
this.ctx.logger.info("amcrest: onboard motion started", { tags: { deviceId: this.id } });
|
|
26885
|
+
}
|
|
26886
|
+
if (this.motionKeepaliveTimer === null) this.motionKeepaliveTimer = setInterval(() => {
|
|
26887
|
+
this.emitMotion(true, Date.now());
|
|
26888
|
+
}, AmcrestCamera.MOTION_KEEPALIVE_MS);
|
|
26889
|
+
}
|
|
26890
|
+
onMotionStop(now) {
|
|
26891
|
+
this.clearMotionKeepalive();
|
|
26892
|
+
if (!this.motionActive) return;
|
|
26893
|
+
this.motionActive = false;
|
|
26894
|
+
this.emitMotion(false, now);
|
|
26895
|
+
const lastDetectedAt = this.getCapSlice(motionCapability)?.lastDetectedAt ?? now;
|
|
26896
|
+
this.setCapSlice(motionCapability, {
|
|
26897
|
+
detected: false,
|
|
26898
|
+
lastDetectedAt,
|
|
26899
|
+
autoClearAfterMs: null
|
|
26900
|
+
});
|
|
26901
|
+
this.ctx.logger.info("amcrest: onboard motion stopped", { tags: { deviceId: this.id } });
|
|
26902
|
+
}
|
|
26903
|
+
clearMotionKeepalive() {
|
|
26904
|
+
if (this.motionKeepaliveTimer !== null) {
|
|
26905
|
+
clearInterval(this.motionKeepaliveTimer);
|
|
26906
|
+
this.motionKeepaliveTimer = null;
|
|
26907
|
+
}
|
|
26908
|
+
}
|
|
26909
|
+
/** Abort the stream + clear all motion timers. Idempotent. */
|
|
26910
|
+
teardownEventSubscription() {
|
|
26911
|
+
if (this.eventStreamReconnectTimer) {
|
|
26912
|
+
clearTimeout(this.eventStreamReconnectTimer);
|
|
26913
|
+
this.eventStreamReconnectTimer = null;
|
|
26914
|
+
}
|
|
26915
|
+
if (this.eventStreamController) {
|
|
26916
|
+
try {
|
|
26917
|
+
this.eventStreamController.abort();
|
|
26918
|
+
} catch {}
|
|
26919
|
+
this.eventStreamController = null;
|
|
26920
|
+
}
|
|
26921
|
+
this.clearMotionKeepalive();
|
|
26922
|
+
this.motionActive = false;
|
|
26923
|
+
}
|
|
26924
|
+
/** Phase 5 — device is live: open the onboard-motion event stream. */
|
|
26925
|
+
async onActivate() {
|
|
26926
|
+
this.ensureEventSubscription();
|
|
26927
|
+
}
|
|
26928
|
+
/** Teardown — stop the stream + motion timers, drop the client. */
|
|
26929
|
+
async removeDevice() {
|
|
26930
|
+
this.ctx.logger.info("Removing Amcrest camera", { tags: { deviceId: this.id } });
|
|
26931
|
+
this.teardownEventSubscription();
|
|
26932
|
+
this.clearPtzAutoStop();
|
|
26933
|
+
this.client = null;
|
|
26577
26934
|
}
|
|
26578
26935
|
registerStreamCatalogProvider() {
|
|
26579
26936
|
this.ctx.registerNativeCap(streamCatalogCapability, { getCatalog: async ({ deviceId }) => {
|
|
@@ -26680,16 +27037,41 @@ var AmcrestCamera = class AmcrestCamera extends BaseDevice {
|
|
|
26680
27037
|
invalidateCache: async () => {}
|
|
26681
27038
|
});
|
|
26682
27039
|
}
|
|
27040
|
+
/** Duration (ms) a one-shot `move` pulse runs before its safety
|
|
27041
|
+
* auto-stop fires. The `usePTZ` UI hook already issues its own paired
|
|
27042
|
+
* `stop` after a client-side pulse window; this timer is a backstop so
|
|
27043
|
+
* a lone `move` (no following `stop`) can't leave the head gliding. */
|
|
27044
|
+
static PTZ_PULSE_MS = 350;
|
|
27045
|
+
/** Short per-call deadline (ms) for the PTZ start/stop CGI so a slow or
|
|
27046
|
+
* blocked camera round-trip fails fast instead of exceeding the
|
|
27047
|
+
* hub-local-uds transport's abort window — the failure mode that
|
|
27048
|
+
* surfaced live as
|
|
27049
|
+
* `ptz.move ... hub-local-uds=This operation was aborted`. */
|
|
27050
|
+
static PTZ_CGI_TIMEOUT_MS = 2500;
|
|
26683
27051
|
/** Dahua `ptz.cgi` direction/zoom codes currently started — tracked so
|
|
26684
27052
|
* `stop()` / the next `continuousMove()` can cleanly halt exactly the
|
|
26685
27053
|
* codes that are actually moving instead of guessing. */
|
|
26686
27054
|
activePtzCodes = /* @__PURE__ */ new Set();
|
|
27055
|
+
/** Pending safety auto-stop for the last one-shot `move` pulse.
|
|
27056
|
+
* Cleared by any subsequent move/continuousMove/stop so pulses and
|
|
27057
|
+
* gestures never race a stale timer. */
|
|
27058
|
+
ptzAutoStopTimer = null;
|
|
27059
|
+
/** Cancel the pending one-shot `move` auto-stop, if any. */
|
|
27060
|
+
clearPtzAutoStop() {
|
|
27061
|
+
if (this.ptzAutoStopTimer !== null) {
|
|
27062
|
+
clearTimeout(this.ptzAutoStopTimer);
|
|
27063
|
+
this.ptzAutoStopTimer = null;
|
|
27064
|
+
}
|
|
27065
|
+
}
|
|
26687
27066
|
/** Resolve a cap `PtzMoveCommand` into 0-2 Dahua CGI move commands
|
|
26688
27067
|
* (one pan/tilt code — possibly diagonal — plus one zoom code).
|
|
26689
|
-
* `pan`/`tilt`/`zoom` are normalized -1..1
|
|
26690
|
-
*
|
|
26691
|
-
|
|
26692
|
-
|
|
27068
|
+
* `pan`/`tilt`/`zoom` are normalized -1..1. Speed maps to Dahua's 1-8
|
|
27069
|
+
* scale: the explicit cap-level `speed` (0..1, from the UI speed
|
|
27070
|
+
* slider) wins when supplied; otherwise the vector magnitude is used. */
|
|
27071
|
+
resolvePtzCodes(pan, tilt, zoom, speed) {
|
|
27072
|
+
const speedFor = (magnitude) => {
|
|
27073
|
+
return Math.max(1, Math.min(8, Math.round((speed !== void 0 ? Math.abs(speed) : Math.abs(magnitude)) * 8)));
|
|
27074
|
+
};
|
|
26693
27075
|
const out = [];
|
|
26694
27076
|
const p = pan ?? 0;
|
|
26695
27077
|
const t = tilt ?? 0;
|
|
@@ -26715,7 +27097,7 @@ var AmcrestCamera = class AmcrestCamera extends BaseDevice {
|
|
|
26715
27097
|
const channel = this.channel;
|
|
26716
27098
|
const codes = [...this.activePtzCodes];
|
|
26717
27099
|
this.activePtzCodes.clear();
|
|
26718
|
-
await Promise.all(codes.map((code) => client.ptzStop(channel, code).catch((err) => {
|
|
27100
|
+
await Promise.all(codes.map((code) => client.ptzStop(channel, code, AmcrestCamera.PTZ_CGI_TIMEOUT_MS).catch((err) => {
|
|
26719
27101
|
this.ctx.logger.debug("amcrest: ptzStop failed (continuing)", {
|
|
26720
27102
|
tags: { deviceId: this.id },
|
|
26721
27103
|
meta: {
|
|
@@ -26728,31 +27110,57 @@ var AmcrestCamera = class AmcrestCamera extends BaseDevice {
|
|
|
26728
27110
|
async startPtzCodes(codes) {
|
|
26729
27111
|
const client = this.ensureClient();
|
|
26730
27112
|
const channel = this.channel;
|
|
26731
|
-
|
|
27113
|
+
await Promise.all(codes.map(({ code, speed }) => {
|
|
26732
27114
|
this.activePtzCodes.add(code);
|
|
26733
|
-
|
|
26734
|
-
}
|
|
27115
|
+
return client.ptzStart(channel, code, speed, AmcrestCamera.PTZ_CGI_TIMEOUT_MS);
|
|
27116
|
+
}));
|
|
27117
|
+
}
|
|
27118
|
+
/** Serialized tail of PTZ CGI operations for this camera. */
|
|
27119
|
+
ptzChain = Promise.resolve();
|
|
27120
|
+
/** Serialize a PTZ CGI op onto the per-camera chain and return AT ONCE.
|
|
27121
|
+
* The UI models a short tap as `move`+`stop` and a long press as
|
|
27122
|
+
* `continuousMove`+`stop`. Two problems the chain solves together:
|
|
27123
|
+
* (1) awaiting the camera round-trip (digest handshake + CGI) in the cap
|
|
27124
|
+
* call makes the hub-local-uds transport abort it → `This operation was
|
|
27125
|
+
* aborted`, head never moves; (2) issuing start/stop as independent
|
|
27126
|
+
* fire-and-forget fetches RACES them — a `stop` fetch can land before its
|
|
27127
|
+
* `start`, leaving the head gliding or never moving (why tap "only worked
|
|
27128
|
+
* on long press"). Chaining runs each op to completion IN ORDER (start
|
|
27129
|
+
* before its paired stop) while the cap returns immediately. */
|
|
27130
|
+
enqueuePtz(label, op) {
|
|
27131
|
+
this.ptzChain = this.ptzChain.then(op).catch((err) => {
|
|
27132
|
+
this.ctx.logger.debug(`amcrest: ptz ${label} failed`, {
|
|
27133
|
+
tags: { deviceId: this.id },
|
|
27134
|
+
meta: { error: err instanceof Error ? err.message : String(err) }
|
|
27135
|
+
});
|
|
27136
|
+
});
|
|
26735
27137
|
}
|
|
26736
27138
|
registerPtzCap() {
|
|
26737
27139
|
this.ctx.registerNativeCap(ptzCapability, {
|
|
26738
|
-
move: async ({ deviceId, pan, tilt, zoom }) => {
|
|
27140
|
+
move: async ({ deviceId, pan, tilt, zoom, speed }) => {
|
|
26739
27141
|
if (deviceId !== this.id) return;
|
|
26740
|
-
|
|
26741
|
-
const codes = this.resolvePtzCodes(pan, tilt, zoom);
|
|
27142
|
+
this.clearPtzAutoStop();
|
|
27143
|
+
const codes = this.resolvePtzCodes(pan, tilt, zoom, speed);
|
|
26742
27144
|
if (codes.length === 0) return;
|
|
26743
|
-
|
|
26744
|
-
|
|
26745
|
-
|
|
27145
|
+
this.enqueuePtz("move", () => this.startPtzCodes(codes));
|
|
27146
|
+
this.ptzAutoStopTimer = setTimeout(() => {
|
|
27147
|
+
this.ptzAutoStopTimer = null;
|
|
27148
|
+
this.enqueuePtz("auto-stop", () => this.stopActivePtz());
|
|
27149
|
+
}, AmcrestCamera.PTZ_PULSE_MS);
|
|
26746
27150
|
},
|
|
26747
|
-
continuousMove: async ({ deviceId, pan, tilt, zoom }) => {
|
|
27151
|
+
continuousMove: async ({ deviceId, pan, tilt, zoom, speed }) => {
|
|
26748
27152
|
if (deviceId !== this.id) return;
|
|
26749
|
-
|
|
26750
|
-
const codes = this.resolvePtzCodes(pan, tilt, zoom);
|
|
26751
|
-
|
|
27153
|
+
this.clearPtzAutoStop();
|
|
27154
|
+
const codes = this.resolvePtzCodes(pan, tilt, zoom, speed);
|
|
27155
|
+
this.enqueuePtz("continuousMove", async () => {
|
|
27156
|
+
await this.stopActivePtz();
|
|
27157
|
+
if (codes.length > 0) await this.startPtzCodes(codes);
|
|
27158
|
+
});
|
|
26752
27159
|
},
|
|
26753
27160
|
stop: async ({ deviceId }) => {
|
|
26754
27161
|
if (deviceId !== this.id) return;
|
|
26755
|
-
|
|
27162
|
+
this.clearPtzAutoStop();
|
|
27163
|
+
this.enqueuePtz("stop", () => this.stopActivePtz());
|
|
26756
27164
|
},
|
|
26757
27165
|
getPresets: async ({ deviceId }) => {
|
|
26758
27166
|
if (deviceId !== this.id) return [];
|
|
@@ -26764,8 +27172,9 @@ var AmcrestCamera = class AmcrestCamera extends BaseDevice {
|
|
|
26764
27172
|
goToPreset: async ({ deviceId, presetId }) => {
|
|
26765
27173
|
if (deviceId !== this.id) return;
|
|
26766
27174
|
const id = Number.parseInt(presetId, 10);
|
|
26767
|
-
if (!Number.isFinite(id)) return;
|
|
26768
|
-
|
|
27175
|
+
if (!Number.isFinite(id) || id < 1) return;
|
|
27176
|
+
const client = this.ensureClient();
|
|
27177
|
+
this.enqueuePtz("goToPreset", () => client.ptzGotoPreset(this.channel, id, AmcrestCamera.PTZ_CGI_TIMEOUT_MS));
|
|
26769
27178
|
},
|
|
26770
27179
|
savePreset: async ({ deviceId, presetId, name }) => {
|
|
26771
27180
|
if (deviceId !== this.id) return;
|
|
@@ -26801,7 +27210,8 @@ var AmcrestCamera = class AmcrestCamera extends BaseDevice {
|
|
|
26801
27210
|
},
|
|
26802
27211
|
goHome: async ({ deviceId }) => {
|
|
26803
27212
|
if (deviceId !== this.id) return;
|
|
26804
|
-
|
|
27213
|
+
const client = this.ensureClient();
|
|
27214
|
+
this.enqueuePtz("goHome", () => client.ptzGotoPreset(this.channel, 1, AmcrestCamera.PTZ_CGI_TIMEOUT_MS));
|
|
26805
27215
|
},
|
|
26806
27216
|
getPosition: async () => {
|
|
26807
27217
|
return {
|
|
@@ -36728,3 +37138,4 @@ exports.AmcrestCamera = AmcrestCamera;
|
|
|
36728
37138
|
exports.AmcrestCgiClient = AmcrestCgiClient;
|
|
36729
37139
|
exports.AmcrestProviderAddon = AmcrestProviderAddon;
|
|
36730
37140
|
exports.amcrestCameraSchema = amcrestCameraSchema;
|
|
37141
|
+
exports.subscribeAmcrestEvents = subscribeAmcrestEvents;
|
package/dist/addon.mjs
CHANGED
|
@@ -19597,7 +19597,15 @@ var FrameworkPackageStatusSchema = object({
|
|
|
19597
19597
|
latestVersion: string().nullable(),
|
|
19598
19598
|
hasUpdate: boolean(),
|
|
19599
19599
|
/** Optional manifest description for the row tooltip. */
|
|
19600
|
-
description: string().optional()
|
|
19600
|
+
description: string().optional(),
|
|
19601
|
+
/**
|
|
19602
|
+
* Content build-id (md5 of the resolved `dist/` tree) of the code the hub
|
|
19603
|
+
* ACTUALLY loaded. Framework packages ship code changes without always
|
|
19604
|
+
* bumping `currentVersion`, so semver alone hides "same version, new code".
|
|
19605
|
+
* `null` when the dist can't be hashed (not installed / empty). The admin-UI
|
|
19606
|
+
* surfaces this so a stale-code hub is visible even at an unchanged version.
|
|
19607
|
+
*/
|
|
19608
|
+
buildId: string().nullable()
|
|
19601
19609
|
});
|
|
19602
19610
|
var LogStreamEntrySchema = object({
|
|
19603
19611
|
timestamp: string(),
|
|
@@ -26424,6 +26432,54 @@ var AmcrestCgiClient = class AmcrestCgiClient {
|
|
|
26424
26432
|
const body = await this.getText(`/cgi-bin/configManager.cgi?action=setConfig&${query}`);
|
|
26425
26433
|
if (!/\bOK\b/i.test(body)) throw new Error(`setConfig rejected: ${body.trim().slice(0, 200)}`);
|
|
26426
26434
|
}
|
|
26435
|
+
/**
|
|
26436
|
+
* Open the Dahua CGI event-manager attach stream — a long-lived
|
|
26437
|
+
* `multipart/x-mixed-replace` GET that pushes camera events
|
|
26438
|
+
* (`VideoMotion` Start/Stop, heartbeats, …) until the connection drops
|
|
26439
|
+
* or the caller aborts. Performs the same digest handshake as `send`
|
|
26440
|
+
* (absorbing the initial 401) but — critically — installs NO
|
|
26441
|
+
* per-request timeout: the long-poll must never self-cancel, so
|
|
26442
|
+
* teardown is driven solely by `signal.abort()`. `codes` is passed
|
|
26443
|
+
* through unescaped (Dahua requires the literal `[...]` brackets, same
|
|
26444
|
+
* as `setConfig`), e.g. `'[VideoMotion]'` or `'[All]'`.
|
|
26445
|
+
*/
|
|
26446
|
+
async openEventStream(codes, signal) {
|
|
26447
|
+
const path = `/cgi-bin/eventManager.cgi?action=attach&codes=${codes}`;
|
|
26448
|
+
const url = `${this.scheme}://${this.host}:${this.port}${path}`;
|
|
26449
|
+
const firstHeader = buildAuthHeader({
|
|
26450
|
+
state: this.digestState,
|
|
26451
|
+
method: "GET",
|
|
26452
|
+
uri: path,
|
|
26453
|
+
username: this.username,
|
|
26454
|
+
password: this.password
|
|
26455
|
+
});
|
|
26456
|
+
let res = await fetch(url, {
|
|
26457
|
+
method: "GET",
|
|
26458
|
+
headers: firstHeader ? { authorization: firstHeader } : {},
|
|
26459
|
+
signal
|
|
26460
|
+
});
|
|
26461
|
+
if (res.status === 401) {
|
|
26462
|
+
await res.body?.cancel().catch(() => {});
|
|
26463
|
+
const challenge = parseChallenge(res.headers.get("www-authenticate"));
|
|
26464
|
+
if (challenge) {
|
|
26465
|
+
this.digestState.challenge = challenge;
|
|
26466
|
+
this.digestState.nc = 0;
|
|
26467
|
+
const retryHeader = buildAuthHeader({
|
|
26468
|
+
state: this.digestState,
|
|
26469
|
+
method: "GET",
|
|
26470
|
+
uri: path,
|
|
26471
|
+
username: this.username,
|
|
26472
|
+
password: this.password
|
|
26473
|
+
});
|
|
26474
|
+
res = await fetch(url, {
|
|
26475
|
+
method: "GET",
|
|
26476
|
+
headers: retryHeader ? { authorization: retryHeader } : {},
|
|
26477
|
+
signal
|
|
26478
|
+
});
|
|
26479
|
+
}
|
|
26480
|
+
}
|
|
26481
|
+
return res;
|
|
26482
|
+
}
|
|
26427
26483
|
async getEncodeConfig() {
|
|
26428
26484
|
return this.getConfig("Encode");
|
|
26429
26485
|
}
|
|
@@ -26450,21 +26506,26 @@ var AmcrestCgiClient = class AmcrestCgiClient {
|
|
|
26450
26506
|
};
|
|
26451
26507
|
}
|
|
26452
26508
|
/** Begin a continuous PTZ move. `code` is a Dahua direction token
|
|
26453
|
-
* (`Left`/`Right`/`Up`/`Down`). `speed` maps to
|
|
26454
|
-
|
|
26455
|
-
|
|
26509
|
+
* (`Left`/`Right`/`Up`/`Down`/`ZoomTele`/`ZoomWide`). `speed` maps to
|
|
26510
|
+
* arg2 (1..8 typical). `timeoutMs` (optional) lets the caller pass a
|
|
26511
|
+
* short per-tick deadline so a slow/blocked camera round-trip fails
|
|
26512
|
+
* fast rather than hanging the whole cap call. The Dahua `start`
|
|
26513
|
+
* action is fire-and-return — the head keeps moving until a matching
|
|
26514
|
+
* `stop` is sent — so this must resolve quickly. */
|
|
26515
|
+
async ptzStart(channel, code, speed, timeoutMs) {
|
|
26516
|
+
await this.getText(`/cgi-bin/ptz.cgi?action=start&channel=${channel}&code=${code}&arg1=0&arg2=${speed}&arg3=0`, timeoutMs);
|
|
26456
26517
|
}
|
|
26457
|
-
async ptzStop(channel, code) {
|
|
26458
|
-
await this.getText(`/cgi-bin/ptz.cgi?action=stop&channel=${channel}&code=${code}&arg1=0&arg2=0&arg3=0
|
|
26518
|
+
async ptzStop(channel, code, timeoutMs) {
|
|
26519
|
+
await this.getText(`/cgi-bin/ptz.cgi?action=stop&channel=${channel}&code=${code}&arg1=0&arg2=0&arg3=0`, timeoutMs);
|
|
26459
26520
|
}
|
|
26460
|
-
async ptzGotoPreset(channel, presetId) {
|
|
26461
|
-
await this.getText(`/cgi-bin/ptz.cgi?action=start&channel=${channel}&code=GotoPreset&arg1=0&arg2=${presetId}&arg3=0
|
|
26521
|
+
async ptzGotoPreset(channel, presetId, timeoutMs) {
|
|
26522
|
+
await this.getText(`/cgi-bin/ptz.cgi?action=start&channel=${channel}&code=GotoPreset&arg1=0&arg2=${presetId}&arg3=0`, timeoutMs);
|
|
26462
26523
|
}
|
|
26463
|
-
async ptzSetPreset(channel, presetId) {
|
|
26464
|
-
await this.getText(`/cgi-bin/ptz.cgi?action=start&channel=${channel}&code=SetPreset&arg1=0&arg2=${presetId}&arg3=0
|
|
26524
|
+
async ptzSetPreset(channel, presetId, timeoutMs) {
|
|
26525
|
+
await this.getText(`/cgi-bin/ptz.cgi?action=start&channel=${channel}&code=SetPreset&arg1=0&arg2=${presetId}&arg3=0`, timeoutMs);
|
|
26465
26526
|
}
|
|
26466
|
-
async ptzClearPreset(channel, presetId) {
|
|
26467
|
-
await this.getText(`/cgi-bin/ptz.cgi?action=start&channel=${channel}&code=ClearPreset&arg1=0&arg2=${presetId}&arg3=0
|
|
26527
|
+
async ptzClearPreset(channel, presetId, timeoutMs) {
|
|
26528
|
+
await this.getText(`/cgi-bin/ptz.cgi?action=start&channel=${channel}&code=ClearPreset&arg1=0&arg2=${presetId}&arg3=0`, timeoutMs);
|
|
26468
26529
|
}
|
|
26469
26530
|
/** Read stored PTZ presets from the `PtzPreset` config block. */
|
|
26470
26531
|
async getPtzPresets(channel) {
|
|
@@ -26488,6 +26549,131 @@ var AmcrestCgiClient = class AmcrestCgiClient {
|
|
|
26488
26549
|
}));
|
|
26489
26550
|
}
|
|
26490
26551
|
};
|
|
26552
|
+
/**
|
|
26553
|
+
* Subscribe to a camera's Dahua CGI event-manager attach stream. Returns
|
|
26554
|
+
* an `AbortController` — `controller.abort()` tears the subscription
|
|
26555
|
+
* down. Reconnect logic is the caller's responsibility (the parser stays
|
|
26556
|
+
* simple; the device class owns lifecycle / backoff timing). Mirrors the
|
|
26557
|
+
* Hikvision provider's `subscribeAlarms` shape so both HTTP-stream
|
|
26558
|
+
* providers read the same way.
|
|
26559
|
+
*
|
|
26560
|
+
* Unlike Hikvision's XML alert stream, Dahua parts are `text/plain`
|
|
26561
|
+
* key=value blocks — `parseAmcrestEventBody` handles the projection.
|
|
26562
|
+
*/
|
|
26563
|
+
function subscribeAmcrestEvents(client, codes, handlers) {
|
|
26564
|
+
const controller = new AbortController();
|
|
26565
|
+
(async () => {
|
|
26566
|
+
try {
|
|
26567
|
+
const res = await client.openEventStream(codes, controller.signal);
|
|
26568
|
+
if (!res.ok || !res.body) {
|
|
26569
|
+
handlers.onError(/* @__PURE__ */ new Error(`event stream HTTP ${res.status} ${res.statusText}`));
|
|
26570
|
+
return;
|
|
26571
|
+
}
|
|
26572
|
+
const ct = res.headers.get("content-type") ?? "";
|
|
26573
|
+
const boundary = parseBoundary(ct);
|
|
26574
|
+
if (!boundary) {
|
|
26575
|
+
handlers.onError(/* @__PURE__ */ new Error(`event stream missing multipart boundary in Content-Type: "${ct}"`));
|
|
26576
|
+
return;
|
|
26577
|
+
}
|
|
26578
|
+
handlers.onConnected?.();
|
|
26579
|
+
await pumpEventStream(res.body, boundary, handlers);
|
|
26580
|
+
if (!controller.signal.aborted) handlers.onError(/* @__PURE__ */ new Error("event stream ended"));
|
|
26581
|
+
} catch (err) {
|
|
26582
|
+
if (controller.signal.aborted) return;
|
|
26583
|
+
handlers.onError(err);
|
|
26584
|
+
}
|
|
26585
|
+
})();
|
|
26586
|
+
return controller;
|
|
26587
|
+
}
|
|
26588
|
+
function parseBoundary(contentType) {
|
|
26589
|
+
const m = /boundary\s*=\s*"?([^";\s]+)"?/i.exec(contentType);
|
|
26590
|
+
return m ? m[1].trim() : null;
|
|
26591
|
+
}
|
|
26592
|
+
async function pumpEventStream(stream, boundary, handlers) {
|
|
26593
|
+
const reader = stream.getReader();
|
|
26594
|
+
const dashBoundary = `--${boundary}`;
|
|
26595
|
+
let bufferedBytes = new Uint8Array(0);
|
|
26596
|
+
try {
|
|
26597
|
+
while (true) {
|
|
26598
|
+
const { value, done } = await reader.read();
|
|
26599
|
+
if (done) break;
|
|
26600
|
+
if (!value || value.byteLength === 0) continue;
|
|
26601
|
+
bufferedBytes = concatBytes(bufferedBytes, value);
|
|
26602
|
+
const parts = splitOnBoundary(bufferedBytes, dashBoundary);
|
|
26603
|
+
bufferedBytes = parts.tail;
|
|
26604
|
+
for (const block of parts.blocks) {
|
|
26605
|
+
const parsed = parseMultipartBlock(block);
|
|
26606
|
+
if (!parsed) continue;
|
|
26607
|
+
const ev = parseAmcrestEventBody(parsed.bodyText);
|
|
26608
|
+
if (ev) handlers.onEvent(ev);
|
|
26609
|
+
}
|
|
26610
|
+
}
|
|
26611
|
+
} finally {
|
|
26612
|
+
try {
|
|
26613
|
+
reader.releaseLock();
|
|
26614
|
+
} catch {}
|
|
26615
|
+
}
|
|
26616
|
+
}
|
|
26617
|
+
function concatBytes(a, b) {
|
|
26618
|
+
const out = new Uint8Array(a.byteLength + b.byteLength);
|
|
26619
|
+
out.set(a, 0);
|
|
26620
|
+
out.set(b, a.byteLength);
|
|
26621
|
+
return out;
|
|
26622
|
+
}
|
|
26623
|
+
function splitOnBoundary(buf, dashBoundary) {
|
|
26624
|
+
const parts = Buffer.from(buf).toString("binary").split(dashBoundary);
|
|
26625
|
+
if (parts.length <= 1) return {
|
|
26626
|
+
blocks: [],
|
|
26627
|
+
tail: buf
|
|
26628
|
+
};
|
|
26629
|
+
const tail = Buffer.from(parts.pop(), "binary");
|
|
26630
|
+
parts.shift();
|
|
26631
|
+
return {
|
|
26632
|
+
blocks: parts.filter((p) => p.length > 0 && !p.startsWith("--")).map((p) => Buffer.from(p, "binary")),
|
|
26633
|
+
tail
|
|
26634
|
+
};
|
|
26635
|
+
}
|
|
26636
|
+
function parseMultipartBlock(block) {
|
|
26637
|
+
let start = 0;
|
|
26638
|
+
while (start < block.length && (block[start] === 13 || block[start] === 10)) start++;
|
|
26639
|
+
const headerEnd = findHeaderEnd(block, start);
|
|
26640
|
+
const bodyStart = headerEnd < 0 ? start : headerEnd + 4;
|
|
26641
|
+
const bodySlice = block.subarray(bodyStart);
|
|
26642
|
+
return { bodyText: Buffer.from(bodySlice).toString("utf8").replace(/\r?\n--$/m, "").trim() };
|
|
26643
|
+
}
|
|
26644
|
+
function findHeaderEnd(buf, from) {
|
|
26645
|
+
for (let i = from; i + 3 < buf.length; i++) if (buf[i] === 13 && buf[i + 1] === 10 && buf[i + 2] === 13 && buf[i + 3] === 10) return i;
|
|
26646
|
+
return -1;
|
|
26647
|
+
}
|
|
26648
|
+
/**
|
|
26649
|
+
* Parse a Dahua event block body. The salient line is a `;`-delimited
|
|
26650
|
+
* key=value list, e.g. `Code=VideoMotion;action=Start;index=0`. A
|
|
26651
|
+
* trailing multi-line `data={…}` JSON blob (present on some firmwares) is
|
|
26652
|
+
* ignored — motion needs only code/action/index. Returns null when no
|
|
26653
|
+
* `Code=` line is present (e.g. an empty heartbeat frame).
|
|
26654
|
+
*/
|
|
26655
|
+
function parseAmcrestEventBody(body) {
|
|
26656
|
+
const codeLine = body.split(/\r?\n/).find((l) => l.includes("Code=")) ?? "";
|
|
26657
|
+
if (!codeLine) return null;
|
|
26658
|
+
const fields = /* @__PURE__ */ new Map();
|
|
26659
|
+
for (const pair of codeLine.split(";")) {
|
|
26660
|
+
const eq = pair.indexOf("=");
|
|
26661
|
+
if (eq < 0) continue;
|
|
26662
|
+
fields.set(pair.slice(0, eq).trim().toLowerCase(), pair.slice(eq + 1).trim());
|
|
26663
|
+
}
|
|
26664
|
+
const code = fields.get("code");
|
|
26665
|
+
const action = fields.get("action");
|
|
26666
|
+
if (!code || !action) return null;
|
|
26667
|
+
const indexRaw = fields.get("index");
|
|
26668
|
+
const index = indexRaw !== void 0 && /^\d+$/.test(indexRaw) ? Number.parseInt(indexRaw, 10) : null;
|
|
26669
|
+
return {
|
|
26670
|
+
observedAt: Date.now(),
|
|
26671
|
+
code,
|
|
26672
|
+
action,
|
|
26673
|
+
index,
|
|
26674
|
+
raw: body
|
|
26675
|
+
};
|
|
26676
|
+
}
|
|
26491
26677
|
//#endregion
|
|
26492
26678
|
//#region src/amcrest-camera.ts
|
|
26493
26679
|
/**
|
|
@@ -26539,11 +26725,34 @@ var AmcrestCamera = class AmcrestCamera extends BaseDevice {
|
|
|
26539
26725
|
this.registerPtzCap();
|
|
26540
26726
|
this.registerStreamParamsCap();
|
|
26541
26727
|
this.registerMotionZonesCap();
|
|
26728
|
+
this.registerMotionCap();
|
|
26542
26729
|
this.registerPrivacyMaskCap();
|
|
26543
26730
|
this.registerOsdCap();
|
|
26544
26731
|
this.registerDayNightCap();
|
|
26545
26732
|
this.registerImageSettingsCap();
|
|
26546
26733
|
}
|
|
26734
|
+
/** Dahua CGI `eventManager` attach codes — VideoMotion is the base
|
|
26735
|
+
* onboard-motion signal (Start/Stop). Smart sub-codes
|
|
26736
|
+
* (SmartMotionHuman/Vehicle) are camera-AI detections handled by a
|
|
26737
|
+
* separate cap and intentionally out of scope here. */
|
|
26738
|
+
static MOTION_EVENT_CODES = "[VideoMotion]";
|
|
26739
|
+
/** Re-emit interval (ms) for a sustained motion window. Dahua fires a
|
|
26740
|
+
* single `VideoMotion action=Start` for the whole window (no per-tick
|
|
26741
|
+
* repeats), but the pipeline-runner re-arms its `motionCooldownMs` on
|
|
26742
|
+
* every `detected:true`. Without a heartbeat a >cooldown motion window
|
|
26743
|
+
* would lapse the runner back to `watching`; a bounded ~0.1 Hz re-emit
|
|
26744
|
+
* holds it `active` until the `Stop`. */
|
|
26745
|
+
static MOTION_KEEPALIVE_MS = 1e4;
|
|
26746
|
+
/** Active event-stream subscription; aborted on dispose / conn change. */
|
|
26747
|
+
eventStreamController = null;
|
|
26748
|
+
/** Reconnect attempt counter — drives exponential backoff. */
|
|
26749
|
+
eventStreamReconnectAttempts = 0;
|
|
26750
|
+
/** Pending reconnect timer. */
|
|
26751
|
+
eventStreamReconnectTimer = null;
|
|
26752
|
+
/** True between a `VideoMotion Start` and its `Stop`. */
|
|
26753
|
+
motionActive = false;
|
|
26754
|
+
/** Keepalive re-emit timer for a sustained motion window. */
|
|
26755
|
+
motionKeepaliveTimer = null;
|
|
26547
26756
|
/** Single-flight guard for the `stream-params` camera refresh. */
|
|
26548
26757
|
streamParamsRefreshInFlight = null;
|
|
26549
26758
|
/** Single-flight guard for the `motion-zones` camera refresh. */
|
|
@@ -26574,7 +26783,155 @@ var AmcrestCamera = class AmcrestCamera extends BaseDevice {
|
|
|
26574
26783
|
* (possibly just-edited) credentials/host. */
|
|
26575
26784
|
async applySettingsPatch(patch) {
|
|
26576
26785
|
await super.applySettingsPatch(patch);
|
|
26577
|
-
if ("host" in patch || "port" in patch || "https" in patch || "username" in patch || "password" in patch)
|
|
26786
|
+
if ("host" in patch || "port" in patch || "https" in patch || "username" in patch || "password" in patch) {
|
|
26787
|
+
this.clearPtzAutoStop();
|
|
26788
|
+
this.client = null;
|
|
26789
|
+
const hadStream = this.eventStreamController !== null || this.eventStreamReconnectTimer !== null;
|
|
26790
|
+
this.teardownEventSubscription();
|
|
26791
|
+
if (hadStream) this.ensureEventSubscription();
|
|
26792
|
+
}
|
|
26793
|
+
}
|
|
26794
|
+
/** Register the native `motion` cap. Its mere presence (a `kind:'native'`
|
|
26795
|
+
* `motion` binding) is what the orchestrator reads to default a
|
|
26796
|
+
* freshly-adopted camera to `motionSources:['onboard']`. */
|
|
26797
|
+
registerMotionCap() {
|
|
26798
|
+
this.ctx.registerNativeCap(motionCapability, { isDetected: async ({ deviceId }) => {
|
|
26799
|
+
if (deviceId !== this.id) throw new Error(`AmcrestCamera: motion deviceId mismatch, expected ${this.id}, got ${deviceId}`);
|
|
26800
|
+
return this.getCapSlice(motionCapability)?.detected ?? false;
|
|
26801
|
+
} });
|
|
26802
|
+
this.setCapSlice(motionCapability, {
|
|
26803
|
+
detected: false,
|
|
26804
|
+
lastDetectedAt: null,
|
|
26805
|
+
autoClearAfterMs: null
|
|
26806
|
+
});
|
|
26807
|
+
this.ctx.logger.info("amcrest: motion cap registered", { tags: { deviceId: this.id } });
|
|
26808
|
+
}
|
|
26809
|
+
/** Reusable event-source identity for every emit on this device. */
|
|
26810
|
+
eventSource() {
|
|
26811
|
+
return {
|
|
26812
|
+
type: "device",
|
|
26813
|
+
id: this.id,
|
|
26814
|
+
addonId: AMCREST_ADDON_ID,
|
|
26815
|
+
deviceId: this.id
|
|
26816
|
+
};
|
|
26817
|
+
}
|
|
26818
|
+
/** Emit an onboard motion transition on the bus. The runner subscribes
|
|
26819
|
+
* and drives its phase machine via `reportMotion`. */
|
|
26820
|
+
emitMotion(detected, timestamp) {
|
|
26821
|
+
this.ctx.eventBus.emit(createEvent(EventCategory.MotionOnMotionChanged, this.eventSource(), {
|
|
26822
|
+
deviceId: this.id,
|
|
26823
|
+
detected,
|
|
26824
|
+
timestamp,
|
|
26825
|
+
source: "onboard"
|
|
26826
|
+
}));
|
|
26827
|
+
}
|
|
26828
|
+
/** Idempotently open the eventManager attach stream. */
|
|
26829
|
+
ensureEventSubscription() {
|
|
26830
|
+
if (this.eventStreamController) return;
|
|
26831
|
+
const client = this.ensureClient();
|
|
26832
|
+
this.eventStreamController = subscribeAmcrestEvents(client, AmcrestCamera.MOTION_EVENT_CODES, {
|
|
26833
|
+
onConnected: () => {
|
|
26834
|
+
this.eventStreamReconnectAttempts = 0;
|
|
26835
|
+
if (this.motionActive) this.onMotionStop(Date.now());
|
|
26836
|
+
this.ctx.logger.info("amcrest: event stream connected", { tags: { deviceId: this.id } });
|
|
26837
|
+
},
|
|
26838
|
+
onEvent: (ev) => this.handleAmcrestEvent(ev),
|
|
26839
|
+
onError: (err) => {
|
|
26840
|
+
this.ctx.logger.warn("amcrest: event stream error — will reconnect", {
|
|
26841
|
+
tags: { deviceId: this.id },
|
|
26842
|
+
meta: { error: err instanceof Error ? err.message : String(err) }
|
|
26843
|
+
});
|
|
26844
|
+
this.eventStreamController = null;
|
|
26845
|
+
this.scheduleEventReconnect();
|
|
26846
|
+
}
|
|
26847
|
+
});
|
|
26848
|
+
}
|
|
26849
|
+
/** Single pending reconnect timer with capped exponential backoff
|
|
26850
|
+
* (1s → 60s) — same shape as the Hikvision alarm stream. */
|
|
26851
|
+
scheduleEventReconnect() {
|
|
26852
|
+
if (this.eventStreamReconnectTimer) return;
|
|
26853
|
+
const attempts = ++this.eventStreamReconnectAttempts;
|
|
26854
|
+
const delayMs = Math.min(6e4, 1e3 * 2 ** Math.min(attempts, 6));
|
|
26855
|
+
this.eventStreamReconnectTimer = setTimeout(() => {
|
|
26856
|
+
this.eventStreamReconnectTimer = null;
|
|
26857
|
+
this.ensureEventSubscription();
|
|
26858
|
+
}, delayMs);
|
|
26859
|
+
}
|
|
26860
|
+
handleAmcrestEvent(ev) {
|
|
26861
|
+
if (ev.code !== "VideoMotion") {
|
|
26862
|
+
this.ctx.logger.debug("amcrest: unhandled event", {
|
|
26863
|
+
tags: { deviceId: this.id },
|
|
26864
|
+
meta: {
|
|
26865
|
+
code: ev.code,
|
|
26866
|
+
action: ev.action
|
|
26867
|
+
}
|
|
26868
|
+
});
|
|
26869
|
+
return;
|
|
26870
|
+
}
|
|
26871
|
+
const action = ev.action.toLowerCase();
|
|
26872
|
+
if (action === "start" || action === "pulse") this.onMotionStart(ev.observedAt);
|
|
26873
|
+
else if (action === "stop") this.onMotionStop(ev.observedAt);
|
|
26874
|
+
}
|
|
26875
|
+
onMotionStart(now) {
|
|
26876
|
+
const wasActive = this.motionActive;
|
|
26877
|
+
this.motionActive = true;
|
|
26878
|
+
this.emitMotion(true, now);
|
|
26879
|
+
if (!wasActive) {
|
|
26880
|
+
this.setCapSlice(motionCapability, {
|
|
26881
|
+
detected: true,
|
|
26882
|
+
lastDetectedAt: now,
|
|
26883
|
+
autoClearAfterMs: null
|
|
26884
|
+
});
|
|
26885
|
+
this.ctx.logger.info("amcrest: onboard motion started", { tags: { deviceId: this.id } });
|
|
26886
|
+
}
|
|
26887
|
+
if (this.motionKeepaliveTimer === null) this.motionKeepaliveTimer = setInterval(() => {
|
|
26888
|
+
this.emitMotion(true, Date.now());
|
|
26889
|
+
}, AmcrestCamera.MOTION_KEEPALIVE_MS);
|
|
26890
|
+
}
|
|
26891
|
+
onMotionStop(now) {
|
|
26892
|
+
this.clearMotionKeepalive();
|
|
26893
|
+
if (!this.motionActive) return;
|
|
26894
|
+
this.motionActive = false;
|
|
26895
|
+
this.emitMotion(false, now);
|
|
26896
|
+
const lastDetectedAt = this.getCapSlice(motionCapability)?.lastDetectedAt ?? now;
|
|
26897
|
+
this.setCapSlice(motionCapability, {
|
|
26898
|
+
detected: false,
|
|
26899
|
+
lastDetectedAt,
|
|
26900
|
+
autoClearAfterMs: null
|
|
26901
|
+
});
|
|
26902
|
+
this.ctx.logger.info("amcrest: onboard motion stopped", { tags: { deviceId: this.id } });
|
|
26903
|
+
}
|
|
26904
|
+
clearMotionKeepalive() {
|
|
26905
|
+
if (this.motionKeepaliveTimer !== null) {
|
|
26906
|
+
clearInterval(this.motionKeepaliveTimer);
|
|
26907
|
+
this.motionKeepaliveTimer = null;
|
|
26908
|
+
}
|
|
26909
|
+
}
|
|
26910
|
+
/** Abort the stream + clear all motion timers. Idempotent. */
|
|
26911
|
+
teardownEventSubscription() {
|
|
26912
|
+
if (this.eventStreamReconnectTimer) {
|
|
26913
|
+
clearTimeout(this.eventStreamReconnectTimer);
|
|
26914
|
+
this.eventStreamReconnectTimer = null;
|
|
26915
|
+
}
|
|
26916
|
+
if (this.eventStreamController) {
|
|
26917
|
+
try {
|
|
26918
|
+
this.eventStreamController.abort();
|
|
26919
|
+
} catch {}
|
|
26920
|
+
this.eventStreamController = null;
|
|
26921
|
+
}
|
|
26922
|
+
this.clearMotionKeepalive();
|
|
26923
|
+
this.motionActive = false;
|
|
26924
|
+
}
|
|
26925
|
+
/** Phase 5 — device is live: open the onboard-motion event stream. */
|
|
26926
|
+
async onActivate() {
|
|
26927
|
+
this.ensureEventSubscription();
|
|
26928
|
+
}
|
|
26929
|
+
/** Teardown — stop the stream + motion timers, drop the client. */
|
|
26930
|
+
async removeDevice() {
|
|
26931
|
+
this.ctx.logger.info("Removing Amcrest camera", { tags: { deviceId: this.id } });
|
|
26932
|
+
this.teardownEventSubscription();
|
|
26933
|
+
this.clearPtzAutoStop();
|
|
26934
|
+
this.client = null;
|
|
26578
26935
|
}
|
|
26579
26936
|
registerStreamCatalogProvider() {
|
|
26580
26937
|
this.ctx.registerNativeCap(streamCatalogCapability, { getCatalog: async ({ deviceId }) => {
|
|
@@ -26681,16 +27038,41 @@ var AmcrestCamera = class AmcrestCamera extends BaseDevice {
|
|
|
26681
27038
|
invalidateCache: async () => {}
|
|
26682
27039
|
});
|
|
26683
27040
|
}
|
|
27041
|
+
/** Duration (ms) a one-shot `move` pulse runs before its safety
|
|
27042
|
+
* auto-stop fires. The `usePTZ` UI hook already issues its own paired
|
|
27043
|
+
* `stop` after a client-side pulse window; this timer is a backstop so
|
|
27044
|
+
* a lone `move` (no following `stop`) can't leave the head gliding. */
|
|
27045
|
+
static PTZ_PULSE_MS = 350;
|
|
27046
|
+
/** Short per-call deadline (ms) for the PTZ start/stop CGI so a slow or
|
|
27047
|
+
* blocked camera round-trip fails fast instead of exceeding the
|
|
27048
|
+
* hub-local-uds transport's abort window — the failure mode that
|
|
27049
|
+
* surfaced live as
|
|
27050
|
+
* `ptz.move ... hub-local-uds=This operation was aborted`. */
|
|
27051
|
+
static PTZ_CGI_TIMEOUT_MS = 2500;
|
|
26684
27052
|
/** Dahua `ptz.cgi` direction/zoom codes currently started — tracked so
|
|
26685
27053
|
* `stop()` / the next `continuousMove()` can cleanly halt exactly the
|
|
26686
27054
|
* codes that are actually moving instead of guessing. */
|
|
26687
27055
|
activePtzCodes = /* @__PURE__ */ new Set();
|
|
27056
|
+
/** Pending safety auto-stop for the last one-shot `move` pulse.
|
|
27057
|
+
* Cleared by any subsequent move/continuousMove/stop so pulses and
|
|
27058
|
+
* gestures never race a stale timer. */
|
|
27059
|
+
ptzAutoStopTimer = null;
|
|
27060
|
+
/** Cancel the pending one-shot `move` auto-stop, if any. */
|
|
27061
|
+
clearPtzAutoStop() {
|
|
27062
|
+
if (this.ptzAutoStopTimer !== null) {
|
|
27063
|
+
clearTimeout(this.ptzAutoStopTimer);
|
|
27064
|
+
this.ptzAutoStopTimer = null;
|
|
27065
|
+
}
|
|
27066
|
+
}
|
|
26688
27067
|
/** Resolve a cap `PtzMoveCommand` into 0-2 Dahua CGI move commands
|
|
26689
27068
|
* (one pan/tilt code — possibly diagonal — plus one zoom code).
|
|
26690
|
-
* `pan`/`tilt`/`zoom` are normalized -1..1
|
|
26691
|
-
*
|
|
26692
|
-
|
|
26693
|
-
|
|
27069
|
+
* `pan`/`tilt`/`zoom` are normalized -1..1. Speed maps to Dahua's 1-8
|
|
27070
|
+
* scale: the explicit cap-level `speed` (0..1, from the UI speed
|
|
27071
|
+
* slider) wins when supplied; otherwise the vector magnitude is used. */
|
|
27072
|
+
resolvePtzCodes(pan, tilt, zoom, speed) {
|
|
27073
|
+
const speedFor = (magnitude) => {
|
|
27074
|
+
return Math.max(1, Math.min(8, Math.round((speed !== void 0 ? Math.abs(speed) : Math.abs(magnitude)) * 8)));
|
|
27075
|
+
};
|
|
26694
27076
|
const out = [];
|
|
26695
27077
|
const p = pan ?? 0;
|
|
26696
27078
|
const t = tilt ?? 0;
|
|
@@ -26716,7 +27098,7 @@ var AmcrestCamera = class AmcrestCamera extends BaseDevice {
|
|
|
26716
27098
|
const channel = this.channel;
|
|
26717
27099
|
const codes = [...this.activePtzCodes];
|
|
26718
27100
|
this.activePtzCodes.clear();
|
|
26719
|
-
await Promise.all(codes.map((code) => client.ptzStop(channel, code).catch((err) => {
|
|
27101
|
+
await Promise.all(codes.map((code) => client.ptzStop(channel, code, AmcrestCamera.PTZ_CGI_TIMEOUT_MS).catch((err) => {
|
|
26720
27102
|
this.ctx.logger.debug("amcrest: ptzStop failed (continuing)", {
|
|
26721
27103
|
tags: { deviceId: this.id },
|
|
26722
27104
|
meta: {
|
|
@@ -26729,31 +27111,57 @@ var AmcrestCamera = class AmcrestCamera extends BaseDevice {
|
|
|
26729
27111
|
async startPtzCodes(codes) {
|
|
26730
27112
|
const client = this.ensureClient();
|
|
26731
27113
|
const channel = this.channel;
|
|
26732
|
-
|
|
27114
|
+
await Promise.all(codes.map(({ code, speed }) => {
|
|
26733
27115
|
this.activePtzCodes.add(code);
|
|
26734
|
-
|
|
26735
|
-
}
|
|
27116
|
+
return client.ptzStart(channel, code, speed, AmcrestCamera.PTZ_CGI_TIMEOUT_MS);
|
|
27117
|
+
}));
|
|
27118
|
+
}
|
|
27119
|
+
/** Serialized tail of PTZ CGI operations for this camera. */
|
|
27120
|
+
ptzChain = Promise.resolve();
|
|
27121
|
+
/** Serialize a PTZ CGI op onto the per-camera chain and return AT ONCE.
|
|
27122
|
+
* The UI models a short tap as `move`+`stop` and a long press as
|
|
27123
|
+
* `continuousMove`+`stop`. Two problems the chain solves together:
|
|
27124
|
+
* (1) awaiting the camera round-trip (digest handshake + CGI) in the cap
|
|
27125
|
+
* call makes the hub-local-uds transport abort it → `This operation was
|
|
27126
|
+
* aborted`, head never moves; (2) issuing start/stop as independent
|
|
27127
|
+
* fire-and-forget fetches RACES them — a `stop` fetch can land before its
|
|
27128
|
+
* `start`, leaving the head gliding or never moving (why tap "only worked
|
|
27129
|
+
* on long press"). Chaining runs each op to completion IN ORDER (start
|
|
27130
|
+
* before its paired stop) while the cap returns immediately. */
|
|
27131
|
+
enqueuePtz(label, op) {
|
|
27132
|
+
this.ptzChain = this.ptzChain.then(op).catch((err) => {
|
|
27133
|
+
this.ctx.logger.debug(`amcrest: ptz ${label} failed`, {
|
|
27134
|
+
tags: { deviceId: this.id },
|
|
27135
|
+
meta: { error: err instanceof Error ? err.message : String(err) }
|
|
27136
|
+
});
|
|
27137
|
+
});
|
|
26736
27138
|
}
|
|
26737
27139
|
registerPtzCap() {
|
|
26738
27140
|
this.ctx.registerNativeCap(ptzCapability, {
|
|
26739
|
-
move: async ({ deviceId, pan, tilt, zoom }) => {
|
|
27141
|
+
move: async ({ deviceId, pan, tilt, zoom, speed }) => {
|
|
26740
27142
|
if (deviceId !== this.id) return;
|
|
26741
|
-
|
|
26742
|
-
const codes = this.resolvePtzCodes(pan, tilt, zoom);
|
|
27143
|
+
this.clearPtzAutoStop();
|
|
27144
|
+
const codes = this.resolvePtzCodes(pan, tilt, zoom, speed);
|
|
26743
27145
|
if (codes.length === 0) return;
|
|
26744
|
-
|
|
26745
|
-
|
|
26746
|
-
|
|
27146
|
+
this.enqueuePtz("move", () => this.startPtzCodes(codes));
|
|
27147
|
+
this.ptzAutoStopTimer = setTimeout(() => {
|
|
27148
|
+
this.ptzAutoStopTimer = null;
|
|
27149
|
+
this.enqueuePtz("auto-stop", () => this.stopActivePtz());
|
|
27150
|
+
}, AmcrestCamera.PTZ_PULSE_MS);
|
|
26747
27151
|
},
|
|
26748
|
-
continuousMove: async ({ deviceId, pan, tilt, zoom }) => {
|
|
27152
|
+
continuousMove: async ({ deviceId, pan, tilt, zoom, speed }) => {
|
|
26749
27153
|
if (deviceId !== this.id) return;
|
|
26750
|
-
|
|
26751
|
-
const codes = this.resolvePtzCodes(pan, tilt, zoom);
|
|
26752
|
-
|
|
27154
|
+
this.clearPtzAutoStop();
|
|
27155
|
+
const codes = this.resolvePtzCodes(pan, tilt, zoom, speed);
|
|
27156
|
+
this.enqueuePtz("continuousMove", async () => {
|
|
27157
|
+
await this.stopActivePtz();
|
|
27158
|
+
if (codes.length > 0) await this.startPtzCodes(codes);
|
|
27159
|
+
});
|
|
26753
27160
|
},
|
|
26754
27161
|
stop: async ({ deviceId }) => {
|
|
26755
27162
|
if (deviceId !== this.id) return;
|
|
26756
|
-
|
|
27163
|
+
this.clearPtzAutoStop();
|
|
27164
|
+
this.enqueuePtz("stop", () => this.stopActivePtz());
|
|
26757
27165
|
},
|
|
26758
27166
|
getPresets: async ({ deviceId }) => {
|
|
26759
27167
|
if (deviceId !== this.id) return [];
|
|
@@ -26765,8 +27173,9 @@ var AmcrestCamera = class AmcrestCamera extends BaseDevice {
|
|
|
26765
27173
|
goToPreset: async ({ deviceId, presetId }) => {
|
|
26766
27174
|
if (deviceId !== this.id) return;
|
|
26767
27175
|
const id = Number.parseInt(presetId, 10);
|
|
26768
|
-
if (!Number.isFinite(id)) return;
|
|
26769
|
-
|
|
27176
|
+
if (!Number.isFinite(id) || id < 1) return;
|
|
27177
|
+
const client = this.ensureClient();
|
|
27178
|
+
this.enqueuePtz("goToPreset", () => client.ptzGotoPreset(this.channel, id, AmcrestCamera.PTZ_CGI_TIMEOUT_MS));
|
|
26770
27179
|
},
|
|
26771
27180
|
savePreset: async ({ deviceId, presetId, name }) => {
|
|
26772
27181
|
if (deviceId !== this.id) return;
|
|
@@ -26802,7 +27211,8 @@ var AmcrestCamera = class AmcrestCamera extends BaseDevice {
|
|
|
26802
27211
|
},
|
|
26803
27212
|
goHome: async ({ deviceId }) => {
|
|
26804
27213
|
if (deviceId !== this.id) return;
|
|
26805
|
-
|
|
27214
|
+
const client = this.ensureClient();
|
|
27215
|
+
this.enqueuePtz("goHome", () => client.ptzGotoPreset(this.channel, 1, AmcrestCamera.PTZ_CGI_TIMEOUT_MS));
|
|
26806
27216
|
},
|
|
26807
27217
|
getPosition: async () => {
|
|
26808
27218
|
return {
|
|
@@ -36724,4 +37134,4 @@ var AmcrestProviderAddon = class extends BaseDeviceProvider {
|
|
|
36724
37134
|
}
|
|
36725
37135
|
};
|
|
36726
37136
|
//#endregion
|
|
36727
|
-
export { AmcrestProviderAddon, amcrestCameraSchema as i, AmcrestCgiClient as n,
|
|
37137
|
+
export { AmcrestProviderAddon, amcrestCameraSchema as a, AMCREST_ADDON_ID as i, AmcrestCgiClient as n, subscribeAmcrestEvents as r, AmcrestCamera as t };
|
package/dist/index.js
CHANGED
|
@@ -5,3 +5,4 @@ exports.AmcrestCamera = require_addon.AmcrestCamera;
|
|
|
5
5
|
exports.AmcrestCgiClient = require_addon.AmcrestCgiClient;
|
|
6
6
|
exports.AmcrestProviderAddon = require_addon.AmcrestProviderAddon;
|
|
7
7
|
exports.amcrestCameraSchema = require_addon.amcrestCameraSchema;
|
|
8
|
+
exports.subscribeAmcrestEvents = require_addon.subscribeAmcrestEvents;
|
package/dist/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { AmcrestProviderAddon,
|
|
2
|
-
export { AMCREST_ADDON_ID, AmcrestCamera, AmcrestCgiClient, AmcrestProviderAddon, amcrestCameraSchema };
|
|
1
|
+
import { AmcrestProviderAddon, a as amcrestCameraSchema, i as AMCREST_ADDON_ID, n as AmcrestCgiClient, r as subscribeAmcrestEvents, t as AmcrestCamera } from "./addon.mjs";
|
|
2
|
+
export { AMCREST_ADDON_ID, AmcrestCamera, AmcrestCgiClient, AmcrestProviderAddon, amcrestCameraSchema, subscribeAmcrestEvents };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/addon-provider-amcrest",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Amcrest/Dahua camera device provider addon for CamStack — Dahua CGI over HTTP(S) with digest auth (snapshot, RTSP catalog, PTZ, image/day-night config)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"camstack",
|