@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 CHANGED
@@ -1,6 +1,3 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
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 arg2 (1..8 typical). */
26453
- async ptzStart(channel, code, speed) {
26454
- await this.getText(`/cgi-bin/ptz.cgi?action=start&channel=${channel}&code=${code}&arg1=0&arg2=${speed}&arg3=0`);
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) this.client = null;
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; magnitude maps to Dahua's
26690
- * 1-8 speed scale. */
26691
- resolvePtzCodes(pan, tilt, zoom) {
26692
- const speedFor = (magnitude) => Math.max(1, Math.min(8, Math.round(Math.abs(magnitude) * 8)));
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
- for (const { code, speed } of codes) {
27113
+ await Promise.all(codes.map(({ code, speed }) => {
26732
27114
  this.activePtzCodes.add(code);
26733
- await client.ptzStart(channel, code, speed);
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
- await this.stopActivePtz();
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
- await this.startPtzCodes(codes);
26744
- await new Promise((resolve) => setTimeout(resolve, 500));
26745
- await this.stopActivePtz();
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
- await this.stopActivePtz();
26750
- const codes = this.resolvePtzCodes(pan, tilt, zoom);
26751
- if (codes.length > 0) await this.startPtzCodes(codes);
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
- await this.stopActivePtz();
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
- await this.ensureClient().ptzGotoPreset(this.channel, id);
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
- await this.ensureClient().ptzGotoPreset(this.channel, 1);
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 arg2 (1..8 typical). */
26454
- async ptzStart(channel, code, speed) {
26455
- await this.getText(`/cgi-bin/ptz.cgi?action=start&channel=${channel}&code=${code}&arg1=0&arg2=${speed}&arg3=0`);
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) this.client = null;
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; magnitude maps to Dahua's
26691
- * 1-8 speed scale. */
26692
- resolvePtzCodes(pan, tilt, zoom) {
26693
- const speedFor = (magnitude) => Math.max(1, Math.min(8, Math.round(Math.abs(magnitude) * 8)));
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
- for (const { code, speed } of codes) {
27114
+ await Promise.all(codes.map(({ code, speed }) => {
26733
27115
  this.activePtzCodes.add(code);
26734
- await client.ptzStart(channel, code, speed);
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
- await this.stopActivePtz();
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
- await this.startPtzCodes(codes);
26745
- await new Promise((resolve) => setTimeout(resolve, 500));
26746
- await this.stopActivePtz();
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
- await this.stopActivePtz();
26751
- const codes = this.resolvePtzCodes(pan, tilt, zoom);
26752
- if (codes.length > 0) await this.startPtzCodes(codes);
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
- await this.stopActivePtz();
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
- await this.ensureClient().ptzGotoPreset(this.channel, id);
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
- await this.ensureClient().ptzGotoPreset(this.channel, 1);
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, AMCREST_ADDON_ID as r, AmcrestCamera as t };
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, i as amcrestCameraSchema, n as AmcrestCgiClient, r as AMCREST_ADDON_ID, t as AmcrestCamera } from "./addon.mjs";
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.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",