@camstack/addon-provider-ecowitt 0.2.24 → 0.2.26

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 +258 -17
  2. package/dist/addon.mjs +258 -17
  3. package/package.json +1 -1
package/dist/addon.js CHANGED
@@ -2,7 +2,7 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  let http = require("http");
3
3
  let events = require("events");
4
4
  let dgram = require("dgram");
5
- //#region ../types/dist/event-category-XfKNtfCc.mjs
5
+ //#region ../types/dist/event-category-CIa_iT6b.mjs
6
6
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
7
7
  EventCategory["SystemBoot"] = "system.boot";
8
8
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -18,6 +18,15 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
18
18
  */
19
19
  EventCategory["SystemRestartCompleted"] = "system.restart-completed";
20
20
  /**
21
+ * The hub reissued its own TLS certificate at boot (`ensureTlsCert`).
22
+ * Emitted only when the material on disk actually changed, so an
23
+ * operator who trusted the old certificate by hand is told rather than
24
+ * discovering it as a browser error. Payload `TlsCertChangedPayload`.
25
+ *
26
+ * Rule: docs/decisions/adr-0227-*.md
27
+ */
28
+ EventCategory["SystemTlsCertChanged"] = "system.tls-cert-changed";
29
+ /**
21
30
  * A newer addon or server-root package version was found by the
22
31
  * authoritative registry check. Emitted once when any observed
23
32
  * `latestVersion` changes (or a package/node first appears behind);
@@ -8014,6 +8023,15 @@ var LabelDefinitionSchema = object({
8014
8023
  description: string().optional(),
8015
8024
  icon: string().optional()
8016
8025
  });
8026
+ var ClassMapDefinitionSchema = object({
8027
+ mapping: record(string(), _enum([
8028
+ "person",
8029
+ "vehicle",
8030
+ "animal",
8031
+ "package"
8032
+ ])),
8033
+ preserveOriginal: boolean()
8034
+ });
8017
8035
  var MODEL_FORMATS = [
8018
8036
  "onnx",
8019
8037
  "coreml",
@@ -8192,7 +8210,13 @@ var ModelCatalogEntrySchema = object({
8192
8210
  * `id` stays the source of truth for resolution/download/persistence; grouping
8193
8211
  * is a presentation overlay resolved back to an `id`.
8194
8212
  */
8195
- group: ModelVariantGroupSchema.optional()
8213
+ group: ModelVariantGroupSchema.optional(),
8214
+ /**
8215
+ * Per-MODEL class map override. Absent ⇒ the step's `StepDefinition.classMap`
8216
+ * applies (Frigate / COCO public catalog). Set on a custom model whose raw
8217
+ * labels already ARE the CamStack macros (Scrypted identity map).
8218
+ */
8219
+ classMap: ClassMapDefinitionSchema.optional()
8196
8220
  });
8197
8221
  var ConvertTargetSchema = discriminatedUnion("format", [object({
8198
8222
  format: literal("openvino"),
@@ -8221,7 +8245,8 @@ var ModelConvertMetadataSchema = object({
8221
8245
  "ocr",
8222
8246
  "segmentation"
8223
8247
  ]),
8224
- faceAlignment: boolean().optional()
8248
+ faceAlignment: boolean().optional(),
8249
+ classMap: ClassMapDefinitionSchema.optional()
8225
8250
  });
8226
8251
  var ConvertResultSchema = object({
8227
8252
  entry: ModelCatalogEntrySchema,
@@ -17514,6 +17539,33 @@ var NativeCropRefSchema = object({
17514
17539
  h: number()
17515
17540
  })
17516
17541
  });
17542
+ object({
17543
+ crop: object({
17544
+ left: number(),
17545
+ top: number(),
17546
+ width: number().positive(),
17547
+ height: number().positive()
17548
+ }).optional(),
17549
+ content: object({
17550
+ width: number().int().positive(),
17551
+ height: number().int().positive()
17552
+ }),
17553
+ fit: _enum(["stretch", "contain"]),
17554
+ format: _enum([
17555
+ "rgb",
17556
+ "gray",
17557
+ "jpeg"
17558
+ ])
17559
+ });
17560
+ var FrameRefSchema = object({
17561
+ registryId: string().min(1),
17562
+ id: string().min(1),
17563
+ width: number().int().positive(),
17564
+ height: number().int().positive(),
17565
+ format: _enum(["rgb", "gray"]),
17566
+ timestamp: number(),
17567
+ capturedAt: number().optional()
17568
+ });
17517
17569
  var ModelFormatSchema$1 = _enum([
17518
17570
  "onnx",
17519
17571
  "coreml",
@@ -17758,6 +17810,12 @@ method(_void(), array(PipelineEngineChoiceSchema)), method(_void(), PipelineEngi
17758
17810
  steps: array(PipelineStepInputSchema).min(1),
17759
17811
  frame: FrameInputSchema.optional(),
17760
17812
  /**
17813
+ * Process-local lazy frame. Valid only when caller and provider resolve
17814
+ * in the same execution-group process; split/cross-node callers use
17815
+ * `frame`/`image` inline compatibility instead.
17816
+ */
17817
+ frameRef: FrameRefSchema.optional(),
17818
+ /**
17761
17819
  * CB5 shm passthrough — a `FrameHandle` naming the same ring slot
17762
17820
  * the decoded pixels live in. One more member of the one-of
17763
17821
  * frame/frameHandle/image/imageBase64/referenceImage group.
@@ -18053,7 +18111,10 @@ var NativeCropResultSchema = object({
18053
18111
  * Which source served this crop, so a quality-sensitive consumer (the native
18054
18112
  * `keyFrame`) can reject a degraded fallback:
18055
18113
  * - `native` — cut from the decode worker's retained NATIVE surface (the
18056
- * quality path).
18114
+ * quality path). A subject-tile serve is also native-resolution and stays
18115
+ * `native` here: the public enum cannot name `tile` without a breaking cap
18116
+ * change. Runner telemetry distinguishes lease vs tile via `source` on the
18117
+ * internal crop result (`nativeHits` vs `tileHits`).
18057
18118
  * - `ram-fullframe` — the native surface MISSED but the request was full-frame,
18058
18119
  * so the ≤640 RAM `RetainedFrameStore` served it (honest lower-res; legit for
18059
18120
  * the blank-frame guard / 640-snapshot resolve, NOT for the clean keyFrame).
@@ -18544,12 +18605,41 @@ var RunnerLocalLoadSchema = object({
18544
18605
  * legacy `OrchestratorMetricsSchema` shape so existing dashboards keep
18545
18606
  * working unchanged when they switch to reading from the runner cap.
18546
18607
  */
18608
+ var FrameLazyCountersSchema = object({
18609
+ framesDecoded: number(),
18610
+ framesAdmitted: number(),
18611
+ framesDroppedPixelFree: number(),
18612
+ viewsMaterialized: number(),
18613
+ viewsSkipped: number(),
18614
+ workerToRunnerBytes: number(),
18615
+ runnerToPoolRawBytes: number(),
18616
+ runnerToPoolJpegBytes: number(),
18617
+ onDemandFullFrameRequests: number(),
18618
+ onDemandCropRequests: number(),
18619
+ nativeHits: number(),
18620
+ nativeMisses: number(),
18621
+ tileHits: number(),
18622
+ tileMisses: number(),
18623
+ fallbackHits: number(),
18624
+ fallbackMisses: number(),
18625
+ retainedWritesAvoided: number(),
18626
+ residentRefs: number(),
18627
+ residentBytes: number(),
18628
+ releases: number(),
18629
+ evictions: number(),
18630
+ staleMisses: number()
18631
+ });
18632
+ var FrameLazyMetricsSchema = object({
18633
+ node: FrameLazyCountersSchema,
18634
+ cameras: array(FrameLazyCountersSchema.extend({ deviceId: number() }))
18635
+ });
18547
18636
  var RunnerLocalMetricsSchema = object({
18548
18637
  nodeId: string(),
18549
18638
  activeCameras: number(),
18550
18639
  throttledCameras: number(),
18551
18640
  avgInferenceTimeMs: number(),
18552
- queueDepth: number()
18641
+ queueDepth: number(),
18642
+ frameLazy: FrameLazyMetricsSchema.optional()
18553
18643
  });
18554
18644
  method(RunnerCameraConfigSchema, object({ success: literal(true) }), { kind: "mutation" }), method(object({ deviceId: number() }), object({ success: literal(true) }), { kind: "mutation" }), method(ReportMotionInputSchema, object({ success: literal(true) }), { kind: "mutation" }), method(_void(), RunnerLocalLoadSchema), method(_void(), RunnerLocalMetricsSchema), method(object({ deviceId: number() }), CameraMetricsSchema.nullable()), method(_void(), array(CameraMetricsWithDeviceIdSchema).readonly()), method(_void(), array(number()).readonly()), method(object({
18555
18645
  handle: FrameHandleSchema,
@@ -19849,6 +19939,9 @@ method(_void(), ProviderInfoSchema), method(object({ config: record(string(), un
19849
19939
  location: StorageLocationSchema,
19850
19940
  relativePath: string()
19851
19941
  }), _void(), { kind: "mutation" }), method(object({ location: StorageLocationSchema }), number().nullable()), method(BeginUploadInputSchema, BeginUploadResultSchema, { kind: "mutation" }), method(WriteChunkInputSchema, _void(), { kind: "mutation" }), method(FinalizeUploadInputSchema, _void(), { kind: "mutation" }), method(AbortUploadInputSchema, _void(), { kind: "mutation" }), method(BeginDownloadInputSchema, BeginDownloadResultSchema, { kind: "mutation" }), method(ReadChunkInputSchema, _instanceof(Uint8Array)), method(EndDownloadInputSchema, _void(), { kind: "mutation" });
19942
+ /** Profile-exported FormBuilder schema. Shape is ConfigUISchema at the UI. */
19943
+ var ProfileSettingsSchemaBridge = unknown().nullable();
19944
+ var ProfileSettingsBagSchema = record(string(), unknown());
19852
19945
  /**
19853
19946
  * A live terminal session hosted by the provider addon. Output and input do
19854
19947
  * NOT flow through the capability — they use the addon data plane
@@ -19878,7 +19971,14 @@ var TerminalSessionInfoSchema = object({
19878
19971
  var TerminalProfileInfoSchema = object({
19879
19972
  profileId: string(),
19880
19973
  label: string(),
19881
- description: string().optional()
19974
+ description: string().optional(),
19975
+ /** Spawn defaults the instance form copies on create. */
19976
+ executable: string().optional(),
19977
+ args: array(string()).readonly().optional(),
19978
+ cwd: string().optional(),
19979
+ environment: array(string()).readonly().optional(),
19980
+ /** ConfigUISchema for instance knobs, or null when the profile has none. */
19981
+ settingsSchema: ProfileSettingsSchemaBridge.optional()
19882
19982
  });
19883
19983
  /**
19884
19984
  * A durable operator-created Terminal instance. Profiles are templates; only
@@ -19891,7 +19991,12 @@ var TerminalInstanceInfoSchema = object({
19891
19991
  profileId: string(),
19892
19992
  profileLabel: string(),
19893
19993
  name: string(),
19894
- enabled: boolean()
19994
+ enabled: boolean(),
19995
+ executable: string(),
19996
+ args: array(string()).readonly(),
19997
+ cwd: string(),
19998
+ environment: array(string()).readonly(),
19999
+ profileSettings: ProfileSettingsBagSchema
19895
20000
  });
19896
20001
  var TerminalLegacyCameraSchema = object({
19897
20002
  stableId: string(),
@@ -19921,7 +20026,23 @@ var TerminalOutputBatchSchema = object({
19921
20026
  method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }), method(_void(), array(TerminalInstanceInfoSchema).readonly(), { auth: "admin" }), method(object({
19922
20027
  targetNodeId: string().min(1),
19923
20028
  profileId: string().min(1),
19924
- name: string().trim().min(1).max(160).optional()
20029
+ name: string().trim().min(1).max(160).optional(),
20030
+ executable: string().max(1024).optional(),
20031
+ args: array(string().max(2048)).max(64).optional(),
20032
+ cwd: string().max(1024).optional(),
20033
+ environment: array(string().max(4096)).max(64).optional(),
20034
+ profileSettings: ProfileSettingsBagSchema.optional()
20035
+ }), TerminalInstanceInfoSchema, {
20036
+ kind: "mutation",
20037
+ auth: "admin"
20038
+ }), method(object({
20039
+ instanceId: string().min(1),
20040
+ name: string().trim().min(1).max(160).optional(),
20041
+ executable: string().max(1024).optional(),
20042
+ args: array(string().max(2048)).max(64).optional(),
20043
+ cwd: string().max(1024).optional(),
20044
+ environment: array(string().max(4096)).max(64).optional(),
20045
+ profileSettings: ProfileSettingsBagSchema.optional()
19925
20046
  }), TerminalInstanceInfoSchema, {
19926
20047
  kind: "mutation",
19927
20048
  auth: "admin"
@@ -19943,7 +20064,11 @@ method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }),
19943
20064
  }), method(_void(), array(TerminalSessionInfoSchema).readonly(), { auth: "admin" }), method(object({
19944
20065
  profileId: string(),
19945
20066
  cols: number().int().positive(),
19946
- rows: number().int().positive()
20067
+ rows: number().int().positive(),
20068
+ executable: string().max(1024).optional(),
20069
+ args: array(string().max(2048)).max(64).optional(),
20070
+ cwd: string().max(1024).optional(),
20071
+ environment: array(string().max(4096)).max(64).optional()
19947
20072
  }), TerminalSessionInfoSchema, {
19948
20073
  kind: "mutation",
19949
20074
  auth: "admin"
@@ -23818,10 +23943,10 @@ var lawnMowerControlCapability = {
23818
23943
  *
23819
23944
  * • The SDK (mobile / web client) consumes `getConnectionEndpoints()`
23820
23945
  * to receive an ordered list of candidate base URLs it should race
23821
- * on connect — LAN IPv4 first (lowest latency when on same network),
23822
- * then public hostname (if a tunnel is up), then IPv6. The SDK can
23823
- * race them with short timeouts and stick with the winner for the
23824
- * session.
23946
+ * on connect — LAN IPv4 and stable LAN IPv6 first (lowest latency
23947
+ * when on the same network), then public hostname (if a tunnel is
23948
+ * up). The SDK can race them with short timeouts and stick with the
23949
+ * winner for the session.
23825
23950
  *
23826
23951
  * Why hub-only: agents are not directly addressable by the operator's
23827
23952
  * clients — they reverse-connect to the hub. Exposing their interfaces
@@ -23976,6 +24101,17 @@ var NotificationEndpointSchema = object({
23976
24101
  /** What the ranking currently resolves to (null when nothing is reachable). */
23977
24102
  resolved: string().nullable()
23978
24103
  });
24104
+ /**
24105
+ * The URLs the SDK / viewer should race for API access. `baseUrls` empty =
24106
+ * AUTO (every LAN IPv4 + the public tunnel). `resolved` is what that choice
24107
+ * currently expands to, so the UI can show the effective set either way.
24108
+ */
24109
+ var ViewerEndpointsSchema = object({
24110
+ /** The operator's explicit race set, or empty for AUTO. */
24111
+ baseUrls: array(string()).readonly(),
24112
+ /** What the ranking currently resolves to (may be empty if nothing is up). */
24113
+ resolved: array(string()).readonly()
24114
+ });
23979
24115
  var AllowedAddressesSchema = object({
23980
24116
  /**
23981
24117
  * Allowlist of interface addresses operators have explicitly opted
@@ -23984,6 +24120,20 @@ var AllowedAddressesSchema = object({
23984
24120
  * Network Addresses admin page and persisted by the addon.
23985
24121
  */
23986
24122
  addresses: array(string()).readonly() });
24123
+ var TlsStatusSchema = object({
24124
+ mode: _enum([
24125
+ "generated",
24126
+ "uploaded",
24127
+ "disabled"
24128
+ ]),
24129
+ leafFingerprintSha256: string().nullable(),
24130
+ caFingerprintSha256: string().nullable(),
24131
+ validTo: string().nullable(),
24132
+ sans: array(string()),
24133
+ caCertPem: string().nullable(),
24134
+ reissueError: string().nullable(),
24135
+ restartRequired: boolean()
24136
+ });
23987
24137
  method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(object({
23988
24138
  /**
23989
24139
  * LEGACY HINT — do not send from new code. Kept optional so clients
@@ -23993,17 +24143,31 @@ method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(obje
23993
24143
  */
23994
24144
  port: number().int().min(1).max(65535).optional(),
23995
24145
  /** Include `http(s)://127.0.0.1:<port>` as the lowest-priority
23996
- * candidate. Default `true`. */
24146
+ * candidate. Default `false` — loopback is not a client route. */
23997
24147
  includeLoopback: boolean().optional(),
23998
- /** Skip IPv6 entries. Some legacy clients can't parse them.
23999
- * Default `false`. */
24148
+ /** Skip IPv6 entries. Default `false` the palette includes stable
24149
+ * LAN IPv6 (ICE/WebRTC uses dual-stack regardless). Pass `true` to
24150
+ * hide them. The viewer HTTP/WS race is `getViewerEndpoints`. */
24000
24151
  ipv4Only: boolean().optional(),
24001
24152
  /** Scheme to emit for LAN/loopback URLs. Default `'http'`.
24002
24153
  * Pass `'https'` when the caller is itself loaded over HTTPS
24003
24154
  * to avoid mixed-content blocks in the browser. The public
24004
24155
  * tunnel always emits `https://` regardless. */
24005
24156
  scheme: _enum(["http", "https"]).optional()
24006
- }), GetConnectionEndpointsResultSchema), method(_void(), NotificationEndpointSchema), method(object({ baseUrl: string().nullable() }), NotificationEndpointSchema, { kind: "mutation" }), method(_void(), AllowedAddressesSchema), method(AllowedAddressesSchema, object({ success: literal(true) }), { kind: "mutation" }), method(_void(), AllowedAddressesSchema, { kind: "mutation" });
24157
+ }), GetConnectionEndpointsResultSchema), method(_void(), NotificationEndpointSchema), method(object({ baseUrl: string().nullable() }), NotificationEndpointSchema, { kind: "mutation" }), method(_void(), ViewerEndpointsSchema), method(object({ baseUrls: array(string()).readonly() }), ViewerEndpointsSchema, { kind: "mutation" }), method(_void(), AllowedAddressesSchema), method(AllowedAddressesSchema, object({ success: literal(true) }), { kind: "mutation" }), method(_void(), AllowedAddressesSchema, { kind: "mutation" }), method(_void(), TlsStatusSchema), method(object({ reason: string().optional() }), TlsStatusSchema, {
24158
+ kind: "mutation",
24159
+ auth: "admin"
24160
+ }), method(object({
24161
+ certPem: string().min(1),
24162
+ keyPem: string().min(1),
24163
+ caPem: string().optional()
24164
+ }), TlsStatusSchema, {
24165
+ kind: "mutation",
24166
+ auth: "admin"
24167
+ }), method(_void(), object({ pem: string() })), method(_void(), TlsStatusSchema, {
24168
+ kind: "mutation",
24169
+ auth: "admin"
24170
+ });
24007
24171
  var LockControlStatusSchema = object({
24008
24172
  /** Lifecycle state of the lock. `jammed` means the motor reported
24009
24173
  * failure to reach the target — operator intervention required. */
@@ -32030,6 +32194,12 @@ Object.freeze({
32030
32194
  addonId: null,
32031
32195
  access: "create"
32032
32196
  },
32197
+ "localNetwork.downloadCa": {
32198
+ capName: "local-network",
32199
+ capScope: "system",
32200
+ addonId: null,
32201
+ access: "view"
32202
+ },
32033
32203
  "localNetwork.getAllowedAddresses": {
32034
32204
  capName: "local-network",
32035
32205
  capScope: "system",
@@ -32054,18 +32224,42 @@ Object.freeze({
32054
32224
  addonId: null,
32055
32225
  access: "view"
32056
32226
  },
32227
+ "localNetwork.getTlsStatus": {
32228
+ capName: "local-network",
32229
+ capScope: "system",
32230
+ addonId: null,
32231
+ access: "view"
32232
+ },
32233
+ "localNetwork.getViewerEndpoints": {
32234
+ capName: "local-network",
32235
+ capScope: "system",
32236
+ addonId: null,
32237
+ access: "view"
32238
+ },
32057
32239
  "localNetwork.list": {
32058
32240
  capName: "local-network",
32059
32241
  capScope: "system",
32060
32242
  addonId: null,
32061
32243
  access: "view"
32062
32244
  },
32245
+ "localNetwork.regenerateCertificate": {
32246
+ capName: "local-network",
32247
+ capScope: "system",
32248
+ addonId: null,
32249
+ access: "create"
32250
+ },
32063
32251
  "localNetwork.resetAllowlistToBestMatch": {
32064
32252
  capName: "local-network",
32065
32253
  capScope: "system",
32066
32254
  addonId: null,
32067
32255
  access: "delete"
32068
32256
  },
32257
+ "localNetwork.revertToGeneratedCertificate": {
32258
+ capName: "local-network",
32259
+ capScope: "system",
32260
+ addonId: null,
32261
+ access: "create"
32262
+ },
32069
32263
  "localNetwork.setAllowedAddresses": {
32070
32264
  capName: "local-network",
32071
32265
  capScope: "system",
@@ -32078,6 +32272,18 @@ Object.freeze({
32078
32272
  addonId: null,
32079
32273
  access: "create"
32080
32274
  },
32275
+ "localNetwork.setViewerEndpoints": {
32276
+ capName: "local-network",
32277
+ capScope: "system",
32278
+ addonId: null,
32279
+ access: "create"
32280
+ },
32281
+ "localNetwork.uploadCertificate": {
32282
+ capName: "local-network",
32283
+ capScope: "system",
32284
+ addonId: null,
32285
+ access: "create"
32286
+ },
32081
32287
  "lockControl.lock": {
32082
32288
  capName: "lock-control",
32083
32289
  capScope: "device",
@@ -34958,6 +35164,12 @@ Object.freeze({
34958
35164
  addonId: null,
34959
35165
  access: "create"
34960
35166
  },
35167
+ "terminalSession.updateInstance": {
35168
+ capName: "terminal-session",
35169
+ capScope: "system",
35170
+ addonId: null,
35171
+ access: "create"
35172
+ },
34961
35173
  "terminalSession.writeInput": {
34962
35174
  capName: "terminal-session",
34963
35175
  capScope: "system",
@@ -37445,6 +37657,35 @@ Object.freeze(Object.fromEntries([{
37445
37657
  }]
37446
37658
  }].map((s) => [s.stepId, s.defaultModelId])));
37447
37659
  string().min(1);
37660
+ var CLUSTER_STEP_SETTING_FIELDS = [{
37661
+ stepId: "face-embedding",
37662
+ key: "minLandmarkFaceSize",
37663
+ label: "Min face size for recognition (detection px)",
37664
+ description: "Refuse to embed a face smaller than this IN THE DETECTION FRAME. Applies to every node — the enrolled gallery is one index, and two admission floors would fill it from two policies. 0 disables the gate.",
37665
+ type: "slider",
37666
+ min: 0,
37667
+ max: 64,
37668
+ step: 2,
37669
+ default: 24
37670
+ }];
37671
+ function clusterStepSettingKey(stepId, fieldKey) {
37672
+ return `clusterStepSetting:${stepId}:${fieldKey}`;
37673
+ }
37674
+ var ClusterSettingNumberSchema = number().finite();
37675
+ function readClusterStepSettings(config) {
37676
+ const out = {};
37677
+ for (const field of CLUSTER_STEP_SETTING_FIELDS) {
37678
+ const parsed = ClusterSettingNumberSchema.safeParse(config[clusterStepSettingKey(field.stepId, field.key)]);
37679
+ const value = parsed.success ? parsed.data : field.default;
37680
+ const existing = out[field.stepId] ?? {};
37681
+ out[field.stepId] = {
37682
+ ...existing,
37683
+ [field.key]: value
37684
+ };
37685
+ }
37686
+ return out;
37687
+ }
37688
+ readClusterStepSettings({});
37448
37689
  object({
37449
37690
  /**
37450
37691
  * Fraction of the box's own size added on EACH side before cutting.
package/dist/addon.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { createServer } from "http";
2
2
  import { EventEmitter } from "events";
3
3
  import { createSocket } from "dgram";
4
- //#region ../types/dist/event-category-XfKNtfCc.mjs
4
+ //#region ../types/dist/event-category-CIa_iT6b.mjs
5
5
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
6
6
  EventCategory["SystemBoot"] = "system.boot";
7
7
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -17,6 +17,15 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
17
17
  */
18
18
  EventCategory["SystemRestartCompleted"] = "system.restart-completed";
19
19
  /**
20
+ * The hub reissued its own TLS certificate at boot (`ensureTlsCert`).
21
+ * Emitted only when the material on disk actually changed, so an
22
+ * operator who trusted the old certificate by hand is told rather than
23
+ * discovering it as a browser error. Payload `TlsCertChangedPayload`.
24
+ *
25
+ * Rule: docs/decisions/adr-0227-*.md
26
+ */
27
+ EventCategory["SystemTlsCertChanged"] = "system.tls-cert-changed";
28
+ /**
20
29
  * A newer addon or server-root package version was found by the
21
30
  * authoritative registry check. Emitted once when any observed
22
31
  * `latestVersion` changes (or a package/node first appears behind);
@@ -8013,6 +8022,15 @@ var LabelDefinitionSchema = object({
8013
8022
  description: string().optional(),
8014
8023
  icon: string().optional()
8015
8024
  });
8025
+ var ClassMapDefinitionSchema = object({
8026
+ mapping: record(string(), _enum([
8027
+ "person",
8028
+ "vehicle",
8029
+ "animal",
8030
+ "package"
8031
+ ])),
8032
+ preserveOriginal: boolean()
8033
+ });
8016
8034
  var MODEL_FORMATS = [
8017
8035
  "onnx",
8018
8036
  "coreml",
@@ -8191,7 +8209,13 @@ var ModelCatalogEntrySchema = object({
8191
8209
  * `id` stays the source of truth for resolution/download/persistence; grouping
8192
8210
  * is a presentation overlay resolved back to an `id`.
8193
8211
  */
8194
- group: ModelVariantGroupSchema.optional()
8212
+ group: ModelVariantGroupSchema.optional(),
8213
+ /**
8214
+ * Per-MODEL class map override. Absent ⇒ the step's `StepDefinition.classMap`
8215
+ * applies (Frigate / COCO public catalog). Set on a custom model whose raw
8216
+ * labels already ARE the CamStack macros (Scrypted identity map).
8217
+ */
8218
+ classMap: ClassMapDefinitionSchema.optional()
8195
8219
  });
8196
8220
  var ConvertTargetSchema = discriminatedUnion("format", [object({
8197
8221
  format: literal("openvino"),
@@ -8220,7 +8244,8 @@ var ModelConvertMetadataSchema = object({
8220
8244
  "ocr",
8221
8245
  "segmentation"
8222
8246
  ]),
8223
- faceAlignment: boolean().optional()
8247
+ faceAlignment: boolean().optional(),
8248
+ classMap: ClassMapDefinitionSchema.optional()
8224
8249
  });
8225
8250
  var ConvertResultSchema = object({
8226
8251
  entry: ModelCatalogEntrySchema,
@@ -17513,6 +17538,33 @@ var NativeCropRefSchema = object({
17513
17538
  h: number()
17514
17539
  })
17515
17540
  });
17541
+ object({
17542
+ crop: object({
17543
+ left: number(),
17544
+ top: number(),
17545
+ width: number().positive(),
17546
+ height: number().positive()
17547
+ }).optional(),
17548
+ content: object({
17549
+ width: number().int().positive(),
17550
+ height: number().int().positive()
17551
+ }),
17552
+ fit: _enum(["stretch", "contain"]),
17553
+ format: _enum([
17554
+ "rgb",
17555
+ "gray",
17556
+ "jpeg"
17557
+ ])
17558
+ });
17559
+ var FrameRefSchema = object({
17560
+ registryId: string().min(1),
17561
+ id: string().min(1),
17562
+ width: number().int().positive(),
17563
+ height: number().int().positive(),
17564
+ format: _enum(["rgb", "gray"]),
17565
+ timestamp: number(),
17566
+ capturedAt: number().optional()
17567
+ });
17516
17568
  var ModelFormatSchema$1 = _enum([
17517
17569
  "onnx",
17518
17570
  "coreml",
@@ -17757,6 +17809,12 @@ method(_void(), array(PipelineEngineChoiceSchema)), method(_void(), PipelineEngi
17757
17809
  steps: array(PipelineStepInputSchema).min(1),
17758
17810
  frame: FrameInputSchema.optional(),
17759
17811
  /**
17812
+ * Process-local lazy frame. Valid only when caller and provider resolve
17813
+ * in the same execution-group process; split/cross-node callers use
17814
+ * `frame`/`image` inline compatibility instead.
17815
+ */
17816
+ frameRef: FrameRefSchema.optional(),
17817
+ /**
17760
17818
  * CB5 shm passthrough — a `FrameHandle` naming the same ring slot
17761
17819
  * the decoded pixels live in. One more member of the one-of
17762
17820
  * frame/frameHandle/image/imageBase64/referenceImage group.
@@ -18052,7 +18110,10 @@ var NativeCropResultSchema = object({
18052
18110
  * Which source served this crop, so a quality-sensitive consumer (the native
18053
18111
  * `keyFrame`) can reject a degraded fallback:
18054
18112
  * - `native` — cut from the decode worker's retained NATIVE surface (the
18055
- * quality path).
18113
+ * quality path). A subject-tile serve is also native-resolution and stays
18114
+ * `native` here: the public enum cannot name `tile` without a breaking cap
18115
+ * change. Runner telemetry distinguishes lease vs tile via `source` on the
18116
+ * internal crop result (`nativeHits` vs `tileHits`).
18056
18117
  * - `ram-fullframe` — the native surface MISSED but the request was full-frame,
18057
18118
  * so the ≤640 RAM `RetainedFrameStore` served it (honest lower-res; legit for
18058
18119
  * the blank-frame guard / 640-snapshot resolve, NOT for the clean keyFrame).
@@ -18543,12 +18604,41 @@ var RunnerLocalLoadSchema = object({
18543
18604
  * legacy `OrchestratorMetricsSchema` shape so existing dashboards keep
18544
18605
  * working unchanged when they switch to reading from the runner cap.
18545
18606
  */
18607
+ var FrameLazyCountersSchema = object({
18608
+ framesDecoded: number(),
18609
+ framesAdmitted: number(),
18610
+ framesDroppedPixelFree: number(),
18611
+ viewsMaterialized: number(),
18612
+ viewsSkipped: number(),
18613
+ workerToRunnerBytes: number(),
18614
+ runnerToPoolRawBytes: number(),
18615
+ runnerToPoolJpegBytes: number(),
18616
+ onDemandFullFrameRequests: number(),
18617
+ onDemandCropRequests: number(),
18618
+ nativeHits: number(),
18619
+ nativeMisses: number(),
18620
+ tileHits: number(),
18621
+ tileMisses: number(),
18622
+ fallbackHits: number(),
18623
+ fallbackMisses: number(),
18624
+ retainedWritesAvoided: number(),
18625
+ residentRefs: number(),
18626
+ residentBytes: number(),
18627
+ releases: number(),
18628
+ evictions: number(),
18629
+ staleMisses: number()
18630
+ });
18631
+ var FrameLazyMetricsSchema = object({
18632
+ node: FrameLazyCountersSchema,
18633
+ cameras: array(FrameLazyCountersSchema.extend({ deviceId: number() }))
18634
+ });
18546
18635
  var RunnerLocalMetricsSchema = object({
18547
18636
  nodeId: string(),
18548
18637
  activeCameras: number(),
18549
18638
  throttledCameras: number(),
18550
18639
  avgInferenceTimeMs: number(),
18551
- queueDepth: number()
18640
+ queueDepth: number(),
18641
+ frameLazy: FrameLazyMetricsSchema.optional()
18552
18642
  });
18553
18643
  method(RunnerCameraConfigSchema, object({ success: literal(true) }), { kind: "mutation" }), method(object({ deviceId: number() }), object({ success: literal(true) }), { kind: "mutation" }), method(ReportMotionInputSchema, object({ success: literal(true) }), { kind: "mutation" }), method(_void(), RunnerLocalLoadSchema), method(_void(), RunnerLocalMetricsSchema), method(object({ deviceId: number() }), CameraMetricsSchema.nullable()), method(_void(), array(CameraMetricsWithDeviceIdSchema).readonly()), method(_void(), array(number()).readonly()), method(object({
18554
18644
  handle: FrameHandleSchema,
@@ -19848,6 +19938,9 @@ method(_void(), ProviderInfoSchema), method(object({ config: record(string(), un
19848
19938
  location: StorageLocationSchema,
19849
19939
  relativePath: string()
19850
19940
  }), _void(), { kind: "mutation" }), method(object({ location: StorageLocationSchema }), number().nullable()), method(BeginUploadInputSchema, BeginUploadResultSchema, { kind: "mutation" }), method(WriteChunkInputSchema, _void(), { kind: "mutation" }), method(FinalizeUploadInputSchema, _void(), { kind: "mutation" }), method(AbortUploadInputSchema, _void(), { kind: "mutation" }), method(BeginDownloadInputSchema, BeginDownloadResultSchema, { kind: "mutation" }), method(ReadChunkInputSchema, _instanceof(Uint8Array)), method(EndDownloadInputSchema, _void(), { kind: "mutation" });
19941
+ /** Profile-exported FormBuilder schema. Shape is ConfigUISchema at the UI. */
19942
+ var ProfileSettingsSchemaBridge = unknown().nullable();
19943
+ var ProfileSettingsBagSchema = record(string(), unknown());
19851
19944
  /**
19852
19945
  * A live terminal session hosted by the provider addon. Output and input do
19853
19946
  * NOT flow through the capability — they use the addon data plane
@@ -19877,7 +19970,14 @@ var TerminalSessionInfoSchema = object({
19877
19970
  var TerminalProfileInfoSchema = object({
19878
19971
  profileId: string(),
19879
19972
  label: string(),
19880
- description: string().optional()
19973
+ description: string().optional(),
19974
+ /** Spawn defaults the instance form copies on create. */
19975
+ executable: string().optional(),
19976
+ args: array(string()).readonly().optional(),
19977
+ cwd: string().optional(),
19978
+ environment: array(string()).readonly().optional(),
19979
+ /** ConfigUISchema for instance knobs, or null when the profile has none. */
19980
+ settingsSchema: ProfileSettingsSchemaBridge.optional()
19881
19981
  });
19882
19982
  /**
19883
19983
  * A durable operator-created Terminal instance. Profiles are templates; only
@@ -19890,7 +19990,12 @@ var TerminalInstanceInfoSchema = object({
19890
19990
  profileId: string(),
19891
19991
  profileLabel: string(),
19892
19992
  name: string(),
19893
- enabled: boolean()
19993
+ enabled: boolean(),
19994
+ executable: string(),
19995
+ args: array(string()).readonly(),
19996
+ cwd: string(),
19997
+ environment: array(string()).readonly(),
19998
+ profileSettings: ProfileSettingsBagSchema
19894
19999
  });
19895
20000
  var TerminalLegacyCameraSchema = object({
19896
20001
  stableId: string(),
@@ -19920,7 +20025,23 @@ var TerminalOutputBatchSchema = object({
19920
20025
  method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }), method(_void(), array(TerminalInstanceInfoSchema).readonly(), { auth: "admin" }), method(object({
19921
20026
  targetNodeId: string().min(1),
19922
20027
  profileId: string().min(1),
19923
- name: string().trim().min(1).max(160).optional()
20028
+ name: string().trim().min(1).max(160).optional(),
20029
+ executable: string().max(1024).optional(),
20030
+ args: array(string().max(2048)).max(64).optional(),
20031
+ cwd: string().max(1024).optional(),
20032
+ environment: array(string().max(4096)).max(64).optional(),
20033
+ profileSettings: ProfileSettingsBagSchema.optional()
20034
+ }), TerminalInstanceInfoSchema, {
20035
+ kind: "mutation",
20036
+ auth: "admin"
20037
+ }), method(object({
20038
+ instanceId: string().min(1),
20039
+ name: string().trim().min(1).max(160).optional(),
20040
+ executable: string().max(1024).optional(),
20041
+ args: array(string().max(2048)).max(64).optional(),
20042
+ cwd: string().max(1024).optional(),
20043
+ environment: array(string().max(4096)).max(64).optional(),
20044
+ profileSettings: ProfileSettingsBagSchema.optional()
19924
20045
  }), TerminalInstanceInfoSchema, {
19925
20046
  kind: "mutation",
19926
20047
  auth: "admin"
@@ -19942,7 +20063,11 @@ method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }),
19942
20063
  }), method(_void(), array(TerminalSessionInfoSchema).readonly(), { auth: "admin" }), method(object({
19943
20064
  profileId: string(),
19944
20065
  cols: number().int().positive(),
19945
- rows: number().int().positive()
20066
+ rows: number().int().positive(),
20067
+ executable: string().max(1024).optional(),
20068
+ args: array(string().max(2048)).max(64).optional(),
20069
+ cwd: string().max(1024).optional(),
20070
+ environment: array(string().max(4096)).max(64).optional()
19946
20071
  }), TerminalSessionInfoSchema, {
19947
20072
  kind: "mutation",
19948
20073
  auth: "admin"
@@ -23817,10 +23942,10 @@ var lawnMowerControlCapability = {
23817
23942
  *
23818
23943
  * • The SDK (mobile / web client) consumes `getConnectionEndpoints()`
23819
23944
  * to receive an ordered list of candidate base URLs it should race
23820
- * on connect — LAN IPv4 first (lowest latency when on same network),
23821
- * then public hostname (if a tunnel is up), then IPv6. The SDK can
23822
- * race them with short timeouts and stick with the winner for the
23823
- * session.
23945
+ * on connect — LAN IPv4 and stable LAN IPv6 first (lowest latency
23946
+ * when on the same network), then public hostname (if a tunnel is
23947
+ * up). The SDK can race them with short timeouts and stick with the
23948
+ * winner for the session.
23824
23949
  *
23825
23950
  * Why hub-only: agents are not directly addressable by the operator's
23826
23951
  * clients — they reverse-connect to the hub. Exposing their interfaces
@@ -23975,6 +24100,17 @@ var NotificationEndpointSchema = object({
23975
24100
  /** What the ranking currently resolves to (null when nothing is reachable). */
23976
24101
  resolved: string().nullable()
23977
24102
  });
24103
+ /**
24104
+ * The URLs the SDK / viewer should race for API access. `baseUrls` empty =
24105
+ * AUTO (every LAN IPv4 + the public tunnel). `resolved` is what that choice
24106
+ * currently expands to, so the UI can show the effective set either way.
24107
+ */
24108
+ var ViewerEndpointsSchema = object({
24109
+ /** The operator's explicit race set, or empty for AUTO. */
24110
+ baseUrls: array(string()).readonly(),
24111
+ /** What the ranking currently resolves to (may be empty if nothing is up). */
24112
+ resolved: array(string()).readonly()
24113
+ });
23978
24114
  var AllowedAddressesSchema = object({
23979
24115
  /**
23980
24116
  * Allowlist of interface addresses operators have explicitly opted
@@ -23983,6 +24119,20 @@ var AllowedAddressesSchema = object({
23983
24119
  * Network Addresses admin page and persisted by the addon.
23984
24120
  */
23985
24121
  addresses: array(string()).readonly() });
24122
+ var TlsStatusSchema = object({
24123
+ mode: _enum([
24124
+ "generated",
24125
+ "uploaded",
24126
+ "disabled"
24127
+ ]),
24128
+ leafFingerprintSha256: string().nullable(),
24129
+ caFingerprintSha256: string().nullable(),
24130
+ validTo: string().nullable(),
24131
+ sans: array(string()),
24132
+ caCertPem: string().nullable(),
24133
+ reissueError: string().nullable(),
24134
+ restartRequired: boolean()
24135
+ });
23986
24136
  method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(object({
23987
24137
  /**
23988
24138
  * LEGACY HINT — do not send from new code. Kept optional so clients
@@ -23992,17 +24142,31 @@ method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(obje
23992
24142
  */
23993
24143
  port: number().int().min(1).max(65535).optional(),
23994
24144
  /** Include `http(s)://127.0.0.1:<port>` as the lowest-priority
23995
- * candidate. Default `true`. */
24145
+ * candidate. Default `false` — loopback is not a client route. */
23996
24146
  includeLoopback: boolean().optional(),
23997
- /** Skip IPv6 entries. Some legacy clients can't parse them.
23998
- * Default `false`. */
24147
+ /** Skip IPv6 entries. Default `false` the palette includes stable
24148
+ * LAN IPv6 (ICE/WebRTC uses dual-stack regardless). Pass `true` to
24149
+ * hide them. The viewer HTTP/WS race is `getViewerEndpoints`. */
23999
24150
  ipv4Only: boolean().optional(),
24000
24151
  /** Scheme to emit for LAN/loopback URLs. Default `'http'`.
24001
24152
  * Pass `'https'` when the caller is itself loaded over HTTPS
24002
24153
  * to avoid mixed-content blocks in the browser. The public
24003
24154
  * tunnel always emits `https://` regardless. */
24004
24155
  scheme: _enum(["http", "https"]).optional()
24005
- }), GetConnectionEndpointsResultSchema), method(_void(), NotificationEndpointSchema), method(object({ baseUrl: string().nullable() }), NotificationEndpointSchema, { kind: "mutation" }), method(_void(), AllowedAddressesSchema), method(AllowedAddressesSchema, object({ success: literal(true) }), { kind: "mutation" }), method(_void(), AllowedAddressesSchema, { kind: "mutation" });
24156
+ }), GetConnectionEndpointsResultSchema), method(_void(), NotificationEndpointSchema), method(object({ baseUrl: string().nullable() }), NotificationEndpointSchema, { kind: "mutation" }), method(_void(), ViewerEndpointsSchema), method(object({ baseUrls: array(string()).readonly() }), ViewerEndpointsSchema, { kind: "mutation" }), method(_void(), AllowedAddressesSchema), method(AllowedAddressesSchema, object({ success: literal(true) }), { kind: "mutation" }), method(_void(), AllowedAddressesSchema, { kind: "mutation" }), method(_void(), TlsStatusSchema), method(object({ reason: string().optional() }), TlsStatusSchema, {
24157
+ kind: "mutation",
24158
+ auth: "admin"
24159
+ }), method(object({
24160
+ certPem: string().min(1),
24161
+ keyPem: string().min(1),
24162
+ caPem: string().optional()
24163
+ }), TlsStatusSchema, {
24164
+ kind: "mutation",
24165
+ auth: "admin"
24166
+ }), method(_void(), object({ pem: string() })), method(_void(), TlsStatusSchema, {
24167
+ kind: "mutation",
24168
+ auth: "admin"
24169
+ });
24006
24170
  var LockControlStatusSchema = object({
24007
24171
  /** Lifecycle state of the lock. `jammed` means the motor reported
24008
24172
  * failure to reach the target — operator intervention required. */
@@ -32029,6 +32193,12 @@ Object.freeze({
32029
32193
  addonId: null,
32030
32194
  access: "create"
32031
32195
  },
32196
+ "localNetwork.downloadCa": {
32197
+ capName: "local-network",
32198
+ capScope: "system",
32199
+ addonId: null,
32200
+ access: "view"
32201
+ },
32032
32202
  "localNetwork.getAllowedAddresses": {
32033
32203
  capName: "local-network",
32034
32204
  capScope: "system",
@@ -32053,18 +32223,42 @@ Object.freeze({
32053
32223
  addonId: null,
32054
32224
  access: "view"
32055
32225
  },
32226
+ "localNetwork.getTlsStatus": {
32227
+ capName: "local-network",
32228
+ capScope: "system",
32229
+ addonId: null,
32230
+ access: "view"
32231
+ },
32232
+ "localNetwork.getViewerEndpoints": {
32233
+ capName: "local-network",
32234
+ capScope: "system",
32235
+ addonId: null,
32236
+ access: "view"
32237
+ },
32056
32238
  "localNetwork.list": {
32057
32239
  capName: "local-network",
32058
32240
  capScope: "system",
32059
32241
  addonId: null,
32060
32242
  access: "view"
32061
32243
  },
32244
+ "localNetwork.regenerateCertificate": {
32245
+ capName: "local-network",
32246
+ capScope: "system",
32247
+ addonId: null,
32248
+ access: "create"
32249
+ },
32062
32250
  "localNetwork.resetAllowlistToBestMatch": {
32063
32251
  capName: "local-network",
32064
32252
  capScope: "system",
32065
32253
  addonId: null,
32066
32254
  access: "delete"
32067
32255
  },
32256
+ "localNetwork.revertToGeneratedCertificate": {
32257
+ capName: "local-network",
32258
+ capScope: "system",
32259
+ addonId: null,
32260
+ access: "create"
32261
+ },
32068
32262
  "localNetwork.setAllowedAddresses": {
32069
32263
  capName: "local-network",
32070
32264
  capScope: "system",
@@ -32077,6 +32271,18 @@ Object.freeze({
32077
32271
  addonId: null,
32078
32272
  access: "create"
32079
32273
  },
32274
+ "localNetwork.setViewerEndpoints": {
32275
+ capName: "local-network",
32276
+ capScope: "system",
32277
+ addonId: null,
32278
+ access: "create"
32279
+ },
32280
+ "localNetwork.uploadCertificate": {
32281
+ capName: "local-network",
32282
+ capScope: "system",
32283
+ addonId: null,
32284
+ access: "create"
32285
+ },
32080
32286
  "lockControl.lock": {
32081
32287
  capName: "lock-control",
32082
32288
  capScope: "device",
@@ -34957,6 +35163,12 @@ Object.freeze({
34957
35163
  addonId: null,
34958
35164
  access: "create"
34959
35165
  },
35166
+ "terminalSession.updateInstance": {
35167
+ capName: "terminal-session",
35168
+ capScope: "system",
35169
+ addonId: null,
35170
+ access: "create"
35171
+ },
34960
35172
  "terminalSession.writeInput": {
34961
35173
  capName: "terminal-session",
34962
35174
  capScope: "system",
@@ -37444,6 +37656,35 @@ Object.freeze(Object.fromEntries([{
37444
37656
  }]
37445
37657
  }].map((s) => [s.stepId, s.defaultModelId])));
37446
37658
  string().min(1);
37659
+ var CLUSTER_STEP_SETTING_FIELDS = [{
37660
+ stepId: "face-embedding",
37661
+ key: "minLandmarkFaceSize",
37662
+ label: "Min face size for recognition (detection px)",
37663
+ description: "Refuse to embed a face smaller than this IN THE DETECTION FRAME. Applies to every node — the enrolled gallery is one index, and two admission floors would fill it from two policies. 0 disables the gate.",
37664
+ type: "slider",
37665
+ min: 0,
37666
+ max: 64,
37667
+ step: 2,
37668
+ default: 24
37669
+ }];
37670
+ function clusterStepSettingKey(stepId, fieldKey) {
37671
+ return `clusterStepSetting:${stepId}:${fieldKey}`;
37672
+ }
37673
+ var ClusterSettingNumberSchema = number().finite();
37674
+ function readClusterStepSettings(config) {
37675
+ const out = {};
37676
+ for (const field of CLUSTER_STEP_SETTING_FIELDS) {
37677
+ const parsed = ClusterSettingNumberSchema.safeParse(config[clusterStepSettingKey(field.stepId, field.key)]);
37678
+ const value = parsed.success ? parsed.data : field.default;
37679
+ const existing = out[field.stepId] ?? {};
37680
+ out[field.stepId] = {
37681
+ ...existing,
37682
+ [field.key]: value
37683
+ };
37684
+ }
37685
+ return out;
37686
+ }
37687
+ readClusterStepSettings({});
37447
37688
  object({
37448
37689
  /**
37449
37690
  * Fraction of the box's own size added on EACH side before cutting.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-ecowitt",
3
- "version": "0.2.24",
3
+ "version": "0.2.26",
4
4
  "description": "Ecowitt weather-station device-provider addon for CamStack — wraps the @apocaliss92/nodewitt local-poll / push client",
5
5
  "keywords": [
6
6
  "camstack",