@camstack/addon-provider-amcrest 0.1.5 → 0.1.6

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.
Files changed (3) hide show
  1. package/dist/addon.js +115 -18
  2. package/dist/addon.mjs +115 -18
  3. package/package.json +1 -1
package/dist/addon.js CHANGED
@@ -6931,10 +6931,18 @@ var ModelVariantGroupSchema = object({
6931
6931
  precision: _enum(["fp32", "int8"]).optional(),
6932
6932
  /**
6933
6933
  * Speed-optimization axis. Omit ⇒ the standard build. `fast` marks a
6934
- * latency-optimized export (e.g. ReLU-activation / reduced-input variant)
6935
- * — the slot the future performance variants plug into.
6934
+ * latency-optimized export (e.g. ReLU-activation variant) the slot the
6935
+ * future performance variants plug into.
6936
6936
  */
6937
- optimization: _enum(["standard", "fast"]).optional()
6937
+ optimization: _enum(["standard", "fast"]).optional(),
6938
+ /**
6939
+ * Input-resolution axis (square input side, px). Omit ⇒ the family's native
6940
+ * resolution (640 for yolo26). Reduced-input builds (320 / 256) are a big,
6941
+ * cheap latency lever — especially on Apple ANE and the Intel N100 — at a
6942
+ * small-object accuracy cost. Mirrors the model's `inputSize` but lifted onto
6943
+ * the group so the selector can offer it as a variant axis.
6944
+ */
6945
+ resolution: number().int().positive().optional()
6938
6946
  });
6939
6947
  var ModelCatalogEntrySchema = object({
6940
6948
  id: string(),
@@ -8587,6 +8595,72 @@ function createRuntimeStateBridge(params) {
8587
8595
  getStatus
8588
8596
  };
8589
8597
  }
8598
+ /** Reject after `ms`; always clears its own timer. */
8599
+ async function withTimeout(promise, ms, label) {
8600
+ let timer;
8601
+ const timeout = new Promise((_resolve, reject) => {
8602
+ timer = setTimeout(() => reject(/* @__PURE__ */ new Error(`${label} timed out after ${String(ms)}ms`)), ms);
8603
+ });
8604
+ try {
8605
+ return await Promise.race([promise, timeout]);
8606
+ } finally {
8607
+ if (timer !== void 0) clearTimeout(timer);
8608
+ }
8609
+ }
8610
+ /**
8611
+ * Start the reachability poll loop. Returns a handle whose `stop()` clears the
8612
+ * timer and prevents any further ticks. Start on device activation, stop on
8613
+ * device teardown (`removeDevice`) so no timer leaks.
8614
+ */
8615
+ function startReachabilityPoll(options) {
8616
+ const intervalMs = options.intervalMs ?? 3e4;
8617
+ const failuresToOffline = options.failuresToOffline ?? 3;
8618
+ const probeTimeoutMs = options.probeTimeoutMs ?? 1e4;
8619
+ const runImmediately = options.runImmediately ?? true;
8620
+ let stopped = false;
8621
+ let running = false;
8622
+ let consecutiveFailures = 0;
8623
+ let timer;
8624
+ const tick = async () => {
8625
+ if (stopped) return;
8626
+ if (running) return;
8627
+ if (options.isEnabled && !options.isEnabled()) return;
8628
+ running = true;
8629
+ try {
8630
+ const reachable = await withTimeout(Promise.resolve().then(options.probe), probeTimeoutMs, "reachability probe");
8631
+ if (stopped) return;
8632
+ if (reachable) {
8633
+ consecutiveFailures = 0;
8634
+ options.setOnline(true);
8635
+ } else registerFailure("probe resolved unreachable");
8636
+ } catch (error) {
8637
+ if (stopped) return;
8638
+ registerFailure(error instanceof Error ? error.message : "probe threw");
8639
+ } finally {
8640
+ running = false;
8641
+ }
8642
+ };
8643
+ const registerFailure = (reason) => {
8644
+ consecutiveFailures += 1;
8645
+ options.logger?.debug("reachability probe failed", {
8646
+ reason,
8647
+ consecutiveFailures,
8648
+ failuresToOffline
8649
+ });
8650
+ if (consecutiveFailures >= failuresToOffline) options.setOnline(false);
8651
+ };
8652
+ timer = setInterval(() => {
8653
+ tick();
8654
+ }, intervalMs);
8655
+ if (typeof timer === "object" && timer !== null && "unref" in timer) timer.unref();
8656
+ if (runImmediately) tick();
8657
+ return { stop: () => {
8658
+ if (stopped) return;
8659
+ stopped = true;
8660
+ if (timer !== void 0) clearInterval(timer);
8661
+ timer = void 0;
8662
+ } };
8663
+ }
8590
8664
  /**
8591
8665
  * Generic device-level status snapshot. Auto-registered by `BaseDevice`
8592
8666
  * for every device, regardless of provider — the kernel needs a uniform
@@ -11900,6 +11974,12 @@ method(_void(), array(PipelineEngineChoiceSchema)), method(_void(), PipelineEngi
11900
11974
  })) }), object({ success: literal(true) }), {
11901
11975
  kind: "mutation",
11902
11976
  auth: "admin"
11977
+ }), method(object({ nodeId: string() }), object({
11978
+ success: literal(true),
11979
+ regeneratedModelId: string().nullable()
11980
+ }), {
11981
+ kind: "mutation",
11982
+ auth: "admin"
11903
11983
  }), method(_void(), PipelineSchemaSchema), method(_void(), array(PipelineDefaultStepSchema).readonly().nullable()), method(_void(), PipelineConfigBridge), method(_void(), ConfigUISchemaBridge), method(object({ steps: array(PipelineStepInputSchema) }), PipelineValidationResultSchema), method(_void(), array(PipelineTemplateSchema$1).readonly()), method(object({
11904
11984
  name: string(),
11905
11985
  steps: array(PipelineTemplateStepSchema).readonly(),
@@ -24504,6 +24584,12 @@ Object.freeze({
24504
24584
  addonId: null,
24505
24585
  access: "create"
24506
24586
  },
24587
+ "pipelineExecutor.resetToDefault": {
24588
+ capName: "pipeline-executor",
24589
+ capScope: "system",
24590
+ addonId: null,
24591
+ access: "delete"
24592
+ },
24507
24593
  "pipelineExecutor.runAudioTest": {
24508
24594
  capName: "pipeline-executor",
24509
24595
  capScope: "system",
@@ -26725,6 +26811,10 @@ var AmcrestCamera = class AmcrestCamera extends BaseDevice {
26725
26811
  motionActive = false;
26726
26812
  /** Keepalive re-emit timer for a sustained motion window. */
26727
26813
  motionKeepaliveTimer = null;
26814
+ /** Control-plane reachability poll — drives `device.online` from Dahua CGI
26815
+ * `getDeviceInfo` liveness, decoupled from stream-broker video health.
26816
+ * Started in `onActivate`, stopped in `removeDevice`. */
26817
+ reachabilityPoll = null;
26728
26818
  /** Single-flight guard for the `stream-params` camera refresh. */
26729
26819
  streamParamsRefreshInFlight = null;
26730
26820
  /** Single-flight guard for the `motion-zones` camera refresh. */
@@ -26897,12 +26987,33 @@ var AmcrestCamera = class AmcrestCamera extends BaseDevice {
26897
26987
  /** Phase 5 — device is live: open the onboard-motion event stream. */
26898
26988
  async onActivate() {
26899
26989
  this.ensureEventSubscription();
26990
+ this.startReachabilityPolling();
26991
+ }
26992
+ /** Start the control-plane reachability poll: a Dahua CGI `getDeviceInfo`
26993
+ * round-trip every 30s drives `device.online`, with hysteresis. Replaces
26994
+ * the old stream-health→online mirror so an on-demand (idle) but reachable
26995
+ * camera still reports ONLINE. Idempotent. */
26996
+ startReachabilityPolling() {
26997
+ if (this.reachabilityPoll) return;
26998
+ this.reachabilityPoll = startReachabilityPoll({
26999
+ probe: async () => {
27000
+ await this.ensureClient().getDeviceInfo();
27001
+ return true;
27002
+ },
27003
+ setOnline: (online) => {
27004
+ this.markOnline(online);
27005
+ },
27006
+ isEnabled: () => !this.disabled,
27007
+ logger: this.ctx.logger
27008
+ });
26900
27009
  }
26901
27010
  /** Teardown — stop the stream + motion timers, drop the client. */
26902
27011
  async removeDevice() {
26903
27012
  this.ctx.logger.info("Removing Amcrest camera", { tags: { deviceId: this.id } });
26904
27013
  this.teardownEventSubscription();
26905
27014
  this.clearPtzAutoStop();
27015
+ this.reachabilityPoll?.stop();
27016
+ this.reachabilityPoll = null;
26906
27017
  this.client = null;
26907
27018
  }
26908
27019
  registerStreamCatalogProvider() {
@@ -36923,21 +37034,7 @@ var AmcrestProviderAddon = class extends BaseDeviceProvider {
36923
37034
  throw new Error(`Amcrest: probe on ${host || "(unknown host)"} resolved neither mac nor host address — cannot persist a stable row key. Verify network reachability + credentials, then retry.`);
36924
37035
  }
36925
37036
  async onInitialize() {
36926
- const regs = await super.onInitialize();
36927
- this.subscribe({ category: EventCategory.DeviceStateChanged }, (event) => {
36928
- const data = event.data;
36929
- if (data.capName !== "camera-streams") return;
36930
- const deviceId = data.deviceId;
36931
- if (typeof deviceId !== "number") return;
36932
- const registry = this.ctx.kernel.deviceRegistry;
36933
- if (!registry) return;
36934
- if (registry.getAddonId(deviceId) !== this.addonId) return;
36935
- const device = registry.getById(deviceId);
36936
- if (!device) return;
36937
- const online = data.slice?.online === true;
36938
- if (device.online !== online) device.online = online;
36939
- });
36940
- return regs;
37037
+ return await super.onInitialize();
36941
37038
  }
36942
37039
  async supportsDiscovery() {
36943
37040
  return true;
package/dist/addon.mjs CHANGED
@@ -6932,10 +6932,18 @@ var ModelVariantGroupSchema = object({
6932
6932
  precision: _enum(["fp32", "int8"]).optional(),
6933
6933
  /**
6934
6934
  * Speed-optimization axis. Omit ⇒ the standard build. `fast` marks a
6935
- * latency-optimized export (e.g. ReLU-activation / reduced-input variant)
6936
- * — the slot the future performance variants plug into.
6935
+ * latency-optimized export (e.g. ReLU-activation variant) the slot the
6936
+ * future performance variants plug into.
6937
6937
  */
6938
- optimization: _enum(["standard", "fast"]).optional()
6938
+ optimization: _enum(["standard", "fast"]).optional(),
6939
+ /**
6940
+ * Input-resolution axis (square input side, px). Omit ⇒ the family's native
6941
+ * resolution (640 for yolo26). Reduced-input builds (320 / 256) are a big,
6942
+ * cheap latency lever — especially on Apple ANE and the Intel N100 — at a
6943
+ * small-object accuracy cost. Mirrors the model's `inputSize` but lifted onto
6944
+ * the group so the selector can offer it as a variant axis.
6945
+ */
6946
+ resolution: number().int().positive().optional()
6939
6947
  });
6940
6948
  var ModelCatalogEntrySchema = object({
6941
6949
  id: string(),
@@ -8588,6 +8596,72 @@ function createRuntimeStateBridge(params) {
8588
8596
  getStatus
8589
8597
  };
8590
8598
  }
8599
+ /** Reject after `ms`; always clears its own timer. */
8600
+ async function withTimeout(promise, ms, label) {
8601
+ let timer;
8602
+ const timeout = new Promise((_resolve, reject) => {
8603
+ timer = setTimeout(() => reject(/* @__PURE__ */ new Error(`${label} timed out after ${String(ms)}ms`)), ms);
8604
+ });
8605
+ try {
8606
+ return await Promise.race([promise, timeout]);
8607
+ } finally {
8608
+ if (timer !== void 0) clearTimeout(timer);
8609
+ }
8610
+ }
8611
+ /**
8612
+ * Start the reachability poll loop. Returns a handle whose `stop()` clears the
8613
+ * timer and prevents any further ticks. Start on device activation, stop on
8614
+ * device teardown (`removeDevice`) so no timer leaks.
8615
+ */
8616
+ function startReachabilityPoll(options) {
8617
+ const intervalMs = options.intervalMs ?? 3e4;
8618
+ const failuresToOffline = options.failuresToOffline ?? 3;
8619
+ const probeTimeoutMs = options.probeTimeoutMs ?? 1e4;
8620
+ const runImmediately = options.runImmediately ?? true;
8621
+ let stopped = false;
8622
+ let running = false;
8623
+ let consecutiveFailures = 0;
8624
+ let timer;
8625
+ const tick = async () => {
8626
+ if (stopped) return;
8627
+ if (running) return;
8628
+ if (options.isEnabled && !options.isEnabled()) return;
8629
+ running = true;
8630
+ try {
8631
+ const reachable = await withTimeout(Promise.resolve().then(options.probe), probeTimeoutMs, "reachability probe");
8632
+ if (stopped) return;
8633
+ if (reachable) {
8634
+ consecutiveFailures = 0;
8635
+ options.setOnline(true);
8636
+ } else registerFailure("probe resolved unreachable");
8637
+ } catch (error) {
8638
+ if (stopped) return;
8639
+ registerFailure(error instanceof Error ? error.message : "probe threw");
8640
+ } finally {
8641
+ running = false;
8642
+ }
8643
+ };
8644
+ const registerFailure = (reason) => {
8645
+ consecutiveFailures += 1;
8646
+ options.logger?.debug("reachability probe failed", {
8647
+ reason,
8648
+ consecutiveFailures,
8649
+ failuresToOffline
8650
+ });
8651
+ if (consecutiveFailures >= failuresToOffline) options.setOnline(false);
8652
+ };
8653
+ timer = setInterval(() => {
8654
+ tick();
8655
+ }, intervalMs);
8656
+ if (typeof timer === "object" && timer !== null && "unref" in timer) timer.unref();
8657
+ if (runImmediately) tick();
8658
+ return { stop: () => {
8659
+ if (stopped) return;
8660
+ stopped = true;
8661
+ if (timer !== void 0) clearInterval(timer);
8662
+ timer = void 0;
8663
+ } };
8664
+ }
8591
8665
  /**
8592
8666
  * Generic device-level status snapshot. Auto-registered by `BaseDevice`
8593
8667
  * for every device, regardless of provider — the kernel needs a uniform
@@ -11901,6 +11975,12 @@ method(_void(), array(PipelineEngineChoiceSchema)), method(_void(), PipelineEngi
11901
11975
  })) }), object({ success: literal(true) }), {
11902
11976
  kind: "mutation",
11903
11977
  auth: "admin"
11978
+ }), method(object({ nodeId: string() }), object({
11979
+ success: literal(true),
11980
+ regeneratedModelId: string().nullable()
11981
+ }), {
11982
+ kind: "mutation",
11983
+ auth: "admin"
11904
11984
  }), method(_void(), PipelineSchemaSchema), method(_void(), array(PipelineDefaultStepSchema).readonly().nullable()), method(_void(), PipelineConfigBridge), method(_void(), ConfigUISchemaBridge), method(object({ steps: array(PipelineStepInputSchema) }), PipelineValidationResultSchema), method(_void(), array(PipelineTemplateSchema$1).readonly()), method(object({
11905
11985
  name: string(),
11906
11986
  steps: array(PipelineTemplateStepSchema).readonly(),
@@ -24505,6 +24585,12 @@ Object.freeze({
24505
24585
  addonId: null,
24506
24586
  access: "create"
24507
24587
  },
24588
+ "pipelineExecutor.resetToDefault": {
24589
+ capName: "pipeline-executor",
24590
+ capScope: "system",
24591
+ addonId: null,
24592
+ access: "delete"
24593
+ },
24508
24594
  "pipelineExecutor.runAudioTest": {
24509
24595
  capName: "pipeline-executor",
24510
24596
  capScope: "system",
@@ -26726,6 +26812,10 @@ var AmcrestCamera = class AmcrestCamera extends BaseDevice {
26726
26812
  motionActive = false;
26727
26813
  /** Keepalive re-emit timer for a sustained motion window. */
26728
26814
  motionKeepaliveTimer = null;
26815
+ /** Control-plane reachability poll — drives `device.online` from Dahua CGI
26816
+ * `getDeviceInfo` liveness, decoupled from stream-broker video health.
26817
+ * Started in `onActivate`, stopped in `removeDevice`. */
26818
+ reachabilityPoll = null;
26729
26819
  /** Single-flight guard for the `stream-params` camera refresh. */
26730
26820
  streamParamsRefreshInFlight = null;
26731
26821
  /** Single-flight guard for the `motion-zones` camera refresh. */
@@ -26898,12 +26988,33 @@ var AmcrestCamera = class AmcrestCamera extends BaseDevice {
26898
26988
  /** Phase 5 — device is live: open the onboard-motion event stream. */
26899
26989
  async onActivate() {
26900
26990
  this.ensureEventSubscription();
26991
+ this.startReachabilityPolling();
26992
+ }
26993
+ /** Start the control-plane reachability poll: a Dahua CGI `getDeviceInfo`
26994
+ * round-trip every 30s drives `device.online`, with hysteresis. Replaces
26995
+ * the old stream-health→online mirror so an on-demand (idle) but reachable
26996
+ * camera still reports ONLINE. Idempotent. */
26997
+ startReachabilityPolling() {
26998
+ if (this.reachabilityPoll) return;
26999
+ this.reachabilityPoll = startReachabilityPoll({
27000
+ probe: async () => {
27001
+ await this.ensureClient().getDeviceInfo();
27002
+ return true;
27003
+ },
27004
+ setOnline: (online) => {
27005
+ this.markOnline(online);
27006
+ },
27007
+ isEnabled: () => !this.disabled,
27008
+ logger: this.ctx.logger
27009
+ });
26901
27010
  }
26902
27011
  /** Teardown — stop the stream + motion timers, drop the client. */
26903
27012
  async removeDevice() {
26904
27013
  this.ctx.logger.info("Removing Amcrest camera", { tags: { deviceId: this.id } });
26905
27014
  this.teardownEventSubscription();
26906
27015
  this.clearPtzAutoStop();
27016
+ this.reachabilityPoll?.stop();
27017
+ this.reachabilityPoll = null;
26907
27018
  this.client = null;
26908
27019
  }
26909
27020
  registerStreamCatalogProvider() {
@@ -36924,21 +37035,7 @@ var AmcrestProviderAddon = class extends BaseDeviceProvider {
36924
37035
  throw new Error(`Amcrest: probe on ${host || "(unknown host)"} resolved neither mac nor host address — cannot persist a stable row key. Verify network reachability + credentials, then retry.`);
36925
37036
  }
36926
37037
  async onInitialize() {
36927
- const regs = await super.onInitialize();
36928
- this.subscribe({ category: EventCategory.DeviceStateChanged }, (event) => {
36929
- const data = event.data;
36930
- if (data.capName !== "camera-streams") return;
36931
- const deviceId = data.deviceId;
36932
- if (typeof deviceId !== "number") return;
36933
- const registry = this.ctx.kernel.deviceRegistry;
36934
- if (!registry) return;
36935
- if (registry.getAddonId(deviceId) !== this.addonId) return;
36936
- const device = registry.getById(deviceId);
36937
- if (!device) return;
36938
- const online = data.slice?.online === true;
36939
- if (device.online !== online) device.online = online;
36940
- });
36941
- return regs;
37038
+ return await super.onInitialize();
36942
37039
  }
36943
37040
  async supportsDiscovery() {
36944
37041
  return true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-amcrest",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
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",