@camstack/addon-provider-rademacher 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
@@ -984,7 +984,7 @@ var Rademacher = class {
984
984
  }
985
985
  };
986
986
  //#endregion
987
- //#region ../types/dist/event-category-XfKNtfCc.mjs
987
+ //#region ../types/dist/event-category-CIa_iT6b.mjs
988
988
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
989
989
  EventCategory["SystemBoot"] = "system.boot";
990
990
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -1000,6 +1000,15 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
1000
1000
  */
1001
1001
  EventCategory["SystemRestartCompleted"] = "system.restart-completed";
1002
1002
  /**
1003
+ * The hub reissued its own TLS certificate at boot (`ensureTlsCert`).
1004
+ * Emitted only when the material on disk actually changed, so an
1005
+ * operator who trusted the old certificate by hand is told rather than
1006
+ * discovering it as a browser error. Payload `TlsCertChangedPayload`.
1007
+ *
1008
+ * Rule: docs/decisions/adr-0227-*.md
1009
+ */
1010
+ EventCategory["SystemTlsCertChanged"] = "system.tls-cert-changed";
1011
+ /**
1003
1012
  * A newer addon or server-root package version was found by the
1004
1013
  * authoritative registry check. Emitted once when any observed
1005
1014
  * `latestVersion` changes (or a package/node first appears behind);
@@ -8996,6 +9005,15 @@ var LabelDefinitionSchema = object({
8996
9005
  description: string().optional(),
8997
9006
  icon: string().optional()
8998
9007
  });
9008
+ var ClassMapDefinitionSchema = object({
9009
+ mapping: record(string(), _enum([
9010
+ "person",
9011
+ "vehicle",
9012
+ "animal",
9013
+ "package"
9014
+ ])),
9015
+ preserveOriginal: boolean()
9016
+ });
8999
9017
  var MODEL_FORMATS = [
9000
9018
  "onnx",
9001
9019
  "coreml",
@@ -9174,7 +9192,13 @@ var ModelCatalogEntrySchema = object({
9174
9192
  * `id` stays the source of truth for resolution/download/persistence; grouping
9175
9193
  * is a presentation overlay resolved back to an `id`.
9176
9194
  */
9177
- group: ModelVariantGroupSchema.optional()
9195
+ group: ModelVariantGroupSchema.optional(),
9196
+ /**
9197
+ * Per-MODEL class map override. Absent ⇒ the step's `StepDefinition.classMap`
9198
+ * applies (Frigate / COCO public catalog). Set on a custom model whose raw
9199
+ * labels already ARE the CamStack macros (Scrypted identity map).
9200
+ */
9201
+ classMap: ClassMapDefinitionSchema.optional()
9178
9202
  });
9179
9203
  var ConvertTargetSchema = discriminatedUnion("format", [object({
9180
9204
  format: literal("openvino"),
@@ -9203,7 +9227,8 @@ var ModelConvertMetadataSchema = object({
9203
9227
  "ocr",
9204
9228
  "segmentation"
9205
9229
  ]),
9206
- faceAlignment: boolean().optional()
9230
+ faceAlignment: boolean().optional(),
9231
+ classMap: ClassMapDefinitionSchema.optional()
9207
9232
  });
9208
9233
  var ConvertResultSchema = object({
9209
9234
  entry: ModelCatalogEntrySchema,
@@ -18496,6 +18521,33 @@ var NativeCropRefSchema = object({
18496
18521
  h: number()
18497
18522
  })
18498
18523
  });
18524
+ object({
18525
+ crop: object({
18526
+ left: number(),
18527
+ top: number(),
18528
+ width: number().positive(),
18529
+ height: number().positive()
18530
+ }).optional(),
18531
+ content: object({
18532
+ width: number().int().positive(),
18533
+ height: number().int().positive()
18534
+ }),
18535
+ fit: _enum(["stretch", "contain"]),
18536
+ format: _enum([
18537
+ "rgb",
18538
+ "gray",
18539
+ "jpeg"
18540
+ ])
18541
+ });
18542
+ var FrameRefSchema = object({
18543
+ registryId: string().min(1),
18544
+ id: string().min(1),
18545
+ width: number().int().positive(),
18546
+ height: number().int().positive(),
18547
+ format: _enum(["rgb", "gray"]),
18548
+ timestamp: number(),
18549
+ capturedAt: number().optional()
18550
+ });
18499
18551
  var ModelFormatSchema$1 = _enum([
18500
18552
  "onnx",
18501
18553
  "coreml",
@@ -18740,6 +18792,12 @@ method(_void(), array(PipelineEngineChoiceSchema)), method(_void(), PipelineEngi
18740
18792
  steps: array(PipelineStepInputSchema).min(1),
18741
18793
  frame: FrameInputSchema.optional(),
18742
18794
  /**
18795
+ * Process-local lazy frame. Valid only when caller and provider resolve
18796
+ * in the same execution-group process; split/cross-node callers use
18797
+ * `frame`/`image` inline compatibility instead.
18798
+ */
18799
+ frameRef: FrameRefSchema.optional(),
18800
+ /**
18743
18801
  * CB5 shm passthrough — a `FrameHandle` naming the same ring slot
18744
18802
  * the decoded pixels live in. One more member of the one-of
18745
18803
  * frame/frameHandle/image/imageBase64/referenceImage group.
@@ -19035,7 +19093,10 @@ var NativeCropResultSchema = object({
19035
19093
  * Which source served this crop, so a quality-sensitive consumer (the native
19036
19094
  * `keyFrame`) can reject a degraded fallback:
19037
19095
  * - `native` — cut from the decode worker's retained NATIVE surface (the
19038
- * quality path).
19096
+ * quality path). A subject-tile serve is also native-resolution and stays
19097
+ * `native` here: the public enum cannot name `tile` without a breaking cap
19098
+ * change. Runner telemetry distinguishes lease vs tile via `source` on the
19099
+ * internal crop result (`nativeHits` vs `tileHits`).
19039
19100
  * - `ram-fullframe` — the native surface MISSED but the request was full-frame,
19040
19101
  * so the ≤640 RAM `RetainedFrameStore` served it (honest lower-res; legit for
19041
19102
  * the blank-frame guard / 640-snapshot resolve, NOT for the clean keyFrame).
@@ -19526,12 +19587,41 @@ var RunnerLocalLoadSchema = object({
19526
19587
  * legacy `OrchestratorMetricsSchema` shape so existing dashboards keep
19527
19588
  * working unchanged when they switch to reading from the runner cap.
19528
19589
  */
19590
+ var FrameLazyCountersSchema = object({
19591
+ framesDecoded: number(),
19592
+ framesAdmitted: number(),
19593
+ framesDroppedPixelFree: number(),
19594
+ viewsMaterialized: number(),
19595
+ viewsSkipped: number(),
19596
+ workerToRunnerBytes: number(),
19597
+ runnerToPoolRawBytes: number(),
19598
+ runnerToPoolJpegBytes: number(),
19599
+ onDemandFullFrameRequests: number(),
19600
+ onDemandCropRequests: number(),
19601
+ nativeHits: number(),
19602
+ nativeMisses: number(),
19603
+ tileHits: number(),
19604
+ tileMisses: number(),
19605
+ fallbackHits: number(),
19606
+ fallbackMisses: number(),
19607
+ retainedWritesAvoided: number(),
19608
+ residentRefs: number(),
19609
+ residentBytes: number(),
19610
+ releases: number(),
19611
+ evictions: number(),
19612
+ staleMisses: number()
19613
+ });
19614
+ var FrameLazyMetricsSchema = object({
19615
+ node: FrameLazyCountersSchema,
19616
+ cameras: array(FrameLazyCountersSchema.extend({ deviceId: number() }))
19617
+ });
19529
19618
  var RunnerLocalMetricsSchema = object({
19530
19619
  nodeId: string(),
19531
19620
  activeCameras: number(),
19532
19621
  throttledCameras: number(),
19533
19622
  avgInferenceTimeMs: number(),
19534
- queueDepth: number()
19623
+ queueDepth: number(),
19624
+ frameLazy: FrameLazyMetricsSchema.optional()
19535
19625
  });
19536
19626
  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({
19537
19627
  handle: FrameHandleSchema,
@@ -20831,6 +20921,9 @@ method(_void(), ProviderInfoSchema), method(object({ config: record(string(), un
20831
20921
  location: StorageLocationSchema,
20832
20922
  relativePath: string()
20833
20923
  }), _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" });
20924
+ /** Profile-exported FormBuilder schema. Shape is ConfigUISchema at the UI. */
20925
+ var ProfileSettingsSchemaBridge = unknown().nullable();
20926
+ var ProfileSettingsBagSchema = record(string(), unknown());
20834
20927
  /**
20835
20928
  * A live terminal session hosted by the provider addon. Output and input do
20836
20929
  * NOT flow through the capability — they use the addon data plane
@@ -20860,7 +20953,14 @@ var TerminalSessionInfoSchema = object({
20860
20953
  var TerminalProfileInfoSchema = object({
20861
20954
  profileId: string(),
20862
20955
  label: string(),
20863
- description: string().optional()
20956
+ description: string().optional(),
20957
+ /** Spawn defaults the instance form copies on create. */
20958
+ executable: string().optional(),
20959
+ args: array(string()).readonly().optional(),
20960
+ cwd: string().optional(),
20961
+ environment: array(string()).readonly().optional(),
20962
+ /** ConfigUISchema for instance knobs, or null when the profile has none. */
20963
+ settingsSchema: ProfileSettingsSchemaBridge.optional()
20864
20964
  });
20865
20965
  /**
20866
20966
  * A durable operator-created Terminal instance. Profiles are templates; only
@@ -20873,7 +20973,12 @@ var TerminalInstanceInfoSchema = object({
20873
20973
  profileId: string(),
20874
20974
  profileLabel: string(),
20875
20975
  name: string(),
20876
- enabled: boolean()
20976
+ enabled: boolean(),
20977
+ executable: string(),
20978
+ args: array(string()).readonly(),
20979
+ cwd: string(),
20980
+ environment: array(string()).readonly(),
20981
+ profileSettings: ProfileSettingsBagSchema
20877
20982
  });
20878
20983
  var TerminalLegacyCameraSchema = object({
20879
20984
  stableId: string(),
@@ -20903,7 +21008,23 @@ var TerminalOutputBatchSchema = object({
20903
21008
  method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }), method(_void(), array(TerminalInstanceInfoSchema).readonly(), { auth: "admin" }), method(object({
20904
21009
  targetNodeId: string().min(1),
20905
21010
  profileId: string().min(1),
20906
- name: string().trim().min(1).max(160).optional()
21011
+ name: string().trim().min(1).max(160).optional(),
21012
+ executable: string().max(1024).optional(),
21013
+ args: array(string().max(2048)).max(64).optional(),
21014
+ cwd: string().max(1024).optional(),
21015
+ environment: array(string().max(4096)).max(64).optional(),
21016
+ profileSettings: ProfileSettingsBagSchema.optional()
21017
+ }), TerminalInstanceInfoSchema, {
21018
+ kind: "mutation",
21019
+ auth: "admin"
21020
+ }), method(object({
21021
+ instanceId: string().min(1),
21022
+ name: string().trim().min(1).max(160).optional(),
21023
+ executable: string().max(1024).optional(),
21024
+ args: array(string().max(2048)).max(64).optional(),
21025
+ cwd: string().max(1024).optional(),
21026
+ environment: array(string().max(4096)).max(64).optional(),
21027
+ profileSettings: ProfileSettingsBagSchema.optional()
20907
21028
  }), TerminalInstanceInfoSchema, {
20908
21029
  kind: "mutation",
20909
21030
  auth: "admin"
@@ -20925,7 +21046,11 @@ method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }),
20925
21046
  }), method(_void(), array(TerminalSessionInfoSchema).readonly(), { auth: "admin" }), method(object({
20926
21047
  profileId: string(),
20927
21048
  cols: number().int().positive(),
20928
- rows: number().int().positive()
21049
+ rows: number().int().positive(),
21050
+ executable: string().max(1024).optional(),
21051
+ args: array(string().max(2048)).max(64).optional(),
21052
+ cwd: string().max(1024).optional(),
21053
+ environment: array(string().max(4096)).max(64).optional()
20929
21054
  }), TerminalSessionInfoSchema, {
20930
21055
  kind: "mutation",
20931
21056
  auth: "admin"
@@ -24800,10 +24925,10 @@ var lawnMowerControlCapability = {
24800
24925
  *
24801
24926
  * • The SDK (mobile / web client) consumes `getConnectionEndpoints()`
24802
24927
  * to receive an ordered list of candidate base URLs it should race
24803
- * on connect — LAN IPv4 first (lowest latency when on same network),
24804
- * then public hostname (if a tunnel is up), then IPv6. The SDK can
24805
- * race them with short timeouts and stick with the winner for the
24806
- * session.
24928
+ * on connect — LAN IPv4 and stable LAN IPv6 first (lowest latency
24929
+ * when on the same network), then public hostname (if a tunnel is
24930
+ * up). The SDK can race them with short timeouts and stick with the
24931
+ * winner for the session.
24807
24932
  *
24808
24933
  * Why hub-only: agents are not directly addressable by the operator's
24809
24934
  * clients — they reverse-connect to the hub. Exposing their interfaces
@@ -24958,6 +25083,17 @@ var NotificationEndpointSchema = object({
24958
25083
  /** What the ranking currently resolves to (null when nothing is reachable). */
24959
25084
  resolved: string().nullable()
24960
25085
  });
25086
+ /**
25087
+ * The URLs the SDK / viewer should race for API access. `baseUrls` empty =
25088
+ * AUTO (every LAN IPv4 + the public tunnel). `resolved` is what that choice
25089
+ * currently expands to, so the UI can show the effective set either way.
25090
+ */
25091
+ var ViewerEndpointsSchema = object({
25092
+ /** The operator's explicit race set, or empty for AUTO. */
25093
+ baseUrls: array(string()).readonly(),
25094
+ /** What the ranking currently resolves to (may be empty if nothing is up). */
25095
+ resolved: array(string()).readonly()
25096
+ });
24961
25097
  var AllowedAddressesSchema = object({
24962
25098
  /**
24963
25099
  * Allowlist of interface addresses operators have explicitly opted
@@ -24966,6 +25102,20 @@ var AllowedAddressesSchema = object({
24966
25102
  * Network Addresses admin page and persisted by the addon.
24967
25103
  */
24968
25104
  addresses: array(string()).readonly() });
25105
+ var TlsStatusSchema = object({
25106
+ mode: _enum([
25107
+ "generated",
25108
+ "uploaded",
25109
+ "disabled"
25110
+ ]),
25111
+ leafFingerprintSha256: string().nullable(),
25112
+ caFingerprintSha256: string().nullable(),
25113
+ validTo: string().nullable(),
25114
+ sans: array(string()),
25115
+ caCertPem: string().nullable(),
25116
+ reissueError: string().nullable(),
25117
+ restartRequired: boolean()
25118
+ });
24969
25119
  method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(object({
24970
25120
  /**
24971
25121
  * LEGACY HINT — do not send from new code. Kept optional so clients
@@ -24975,17 +25125,31 @@ method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(obje
24975
25125
  */
24976
25126
  port: number().int().min(1).max(65535).optional(),
24977
25127
  /** Include `http(s)://127.0.0.1:<port>` as the lowest-priority
24978
- * candidate. Default `true`. */
25128
+ * candidate. Default `false` — loopback is not a client route. */
24979
25129
  includeLoopback: boolean().optional(),
24980
- /** Skip IPv6 entries. Some legacy clients can't parse them.
24981
- * Default `false`. */
25130
+ /** Skip IPv6 entries. Default `false` the palette includes stable
25131
+ * LAN IPv6 (ICE/WebRTC uses dual-stack regardless). Pass `true` to
25132
+ * hide them. The viewer HTTP/WS race is `getViewerEndpoints`. */
24982
25133
  ipv4Only: boolean().optional(),
24983
25134
  /** Scheme to emit for LAN/loopback URLs. Default `'http'`.
24984
25135
  * Pass `'https'` when the caller is itself loaded over HTTPS
24985
25136
  * to avoid mixed-content blocks in the browser. The public
24986
25137
  * tunnel always emits `https://` regardless. */
24987
25138
  scheme: _enum(["http", "https"]).optional()
24988
- }), 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" });
25139
+ }), 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, {
25140
+ kind: "mutation",
25141
+ auth: "admin"
25142
+ }), method(object({
25143
+ certPem: string().min(1),
25144
+ keyPem: string().min(1),
25145
+ caPem: string().optional()
25146
+ }), TlsStatusSchema, {
25147
+ kind: "mutation",
25148
+ auth: "admin"
25149
+ }), method(_void(), object({ pem: string() })), method(_void(), TlsStatusSchema, {
25150
+ kind: "mutation",
25151
+ auth: "admin"
25152
+ });
24989
25153
  var LockControlStatusSchema = object({
24990
25154
  /** Lifecycle state of the lock. `jammed` means the motor reported
24991
25155
  * failure to reach the target — operator intervention required. */
@@ -33012,6 +33176,12 @@ Object.freeze({
33012
33176
  addonId: null,
33013
33177
  access: "create"
33014
33178
  },
33179
+ "localNetwork.downloadCa": {
33180
+ capName: "local-network",
33181
+ capScope: "system",
33182
+ addonId: null,
33183
+ access: "view"
33184
+ },
33015
33185
  "localNetwork.getAllowedAddresses": {
33016
33186
  capName: "local-network",
33017
33187
  capScope: "system",
@@ -33036,18 +33206,42 @@ Object.freeze({
33036
33206
  addonId: null,
33037
33207
  access: "view"
33038
33208
  },
33209
+ "localNetwork.getTlsStatus": {
33210
+ capName: "local-network",
33211
+ capScope: "system",
33212
+ addonId: null,
33213
+ access: "view"
33214
+ },
33215
+ "localNetwork.getViewerEndpoints": {
33216
+ capName: "local-network",
33217
+ capScope: "system",
33218
+ addonId: null,
33219
+ access: "view"
33220
+ },
33039
33221
  "localNetwork.list": {
33040
33222
  capName: "local-network",
33041
33223
  capScope: "system",
33042
33224
  addonId: null,
33043
33225
  access: "view"
33044
33226
  },
33227
+ "localNetwork.regenerateCertificate": {
33228
+ capName: "local-network",
33229
+ capScope: "system",
33230
+ addonId: null,
33231
+ access: "create"
33232
+ },
33045
33233
  "localNetwork.resetAllowlistToBestMatch": {
33046
33234
  capName: "local-network",
33047
33235
  capScope: "system",
33048
33236
  addonId: null,
33049
33237
  access: "delete"
33050
33238
  },
33239
+ "localNetwork.revertToGeneratedCertificate": {
33240
+ capName: "local-network",
33241
+ capScope: "system",
33242
+ addonId: null,
33243
+ access: "create"
33244
+ },
33051
33245
  "localNetwork.setAllowedAddresses": {
33052
33246
  capName: "local-network",
33053
33247
  capScope: "system",
@@ -33060,6 +33254,18 @@ Object.freeze({
33060
33254
  addonId: null,
33061
33255
  access: "create"
33062
33256
  },
33257
+ "localNetwork.setViewerEndpoints": {
33258
+ capName: "local-network",
33259
+ capScope: "system",
33260
+ addonId: null,
33261
+ access: "create"
33262
+ },
33263
+ "localNetwork.uploadCertificate": {
33264
+ capName: "local-network",
33265
+ capScope: "system",
33266
+ addonId: null,
33267
+ access: "create"
33268
+ },
33063
33269
  "lockControl.lock": {
33064
33270
  capName: "lock-control",
33065
33271
  capScope: "device",
@@ -35940,6 +36146,12 @@ Object.freeze({
35940
36146
  addonId: null,
35941
36147
  access: "create"
35942
36148
  },
36149
+ "terminalSession.updateInstance": {
36150
+ capName: "terminal-session",
36151
+ capScope: "system",
36152
+ addonId: null,
36153
+ access: "create"
36154
+ },
35943
36155
  "terminalSession.writeInput": {
35944
36156
  capName: "terminal-session",
35945
36157
  capScope: "system",
@@ -38427,6 +38639,35 @@ Object.freeze(Object.fromEntries([{
38427
38639
  }]
38428
38640
  }].map((s) => [s.stepId, s.defaultModelId])));
38429
38641
  string().min(1);
38642
+ var CLUSTER_STEP_SETTING_FIELDS = [{
38643
+ stepId: "face-embedding",
38644
+ key: "minLandmarkFaceSize",
38645
+ label: "Min face size for recognition (detection px)",
38646
+ 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.",
38647
+ type: "slider",
38648
+ min: 0,
38649
+ max: 64,
38650
+ step: 2,
38651
+ default: 24
38652
+ }];
38653
+ function clusterStepSettingKey(stepId, fieldKey) {
38654
+ return `clusterStepSetting:${stepId}:${fieldKey}`;
38655
+ }
38656
+ var ClusterSettingNumberSchema = number().finite();
38657
+ function readClusterStepSettings(config) {
38658
+ const out = {};
38659
+ for (const field of CLUSTER_STEP_SETTING_FIELDS) {
38660
+ const parsed = ClusterSettingNumberSchema.safeParse(config[clusterStepSettingKey(field.stepId, field.key)]);
38661
+ const value = parsed.success ? parsed.data : field.default;
38662
+ const existing = out[field.stepId] ?? {};
38663
+ out[field.stepId] = {
38664
+ ...existing,
38665
+ [field.key]: value
38666
+ };
38667
+ }
38668
+ return out;
38669
+ }
38670
+ readClusterStepSettings({});
38430
38671
  object({
38431
38672
  /**
38432
38673
  * Fraction of the box's own size added on EACH side before cutting.
package/dist/addon.mjs CHANGED
@@ -983,7 +983,7 @@ var Rademacher = class {
983
983
  }
984
984
  };
985
985
  //#endregion
986
- //#region ../types/dist/event-category-XfKNtfCc.mjs
986
+ //#region ../types/dist/event-category-CIa_iT6b.mjs
987
987
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
988
988
  EventCategory["SystemBoot"] = "system.boot";
989
989
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -999,6 +999,15 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
999
999
  */
1000
1000
  EventCategory["SystemRestartCompleted"] = "system.restart-completed";
1001
1001
  /**
1002
+ * The hub reissued its own TLS certificate at boot (`ensureTlsCert`).
1003
+ * Emitted only when the material on disk actually changed, so an
1004
+ * operator who trusted the old certificate by hand is told rather than
1005
+ * discovering it as a browser error. Payload `TlsCertChangedPayload`.
1006
+ *
1007
+ * Rule: docs/decisions/adr-0227-*.md
1008
+ */
1009
+ EventCategory["SystemTlsCertChanged"] = "system.tls-cert-changed";
1010
+ /**
1002
1011
  * A newer addon or server-root package version was found by the
1003
1012
  * authoritative registry check. Emitted once when any observed
1004
1013
  * `latestVersion` changes (or a package/node first appears behind);
@@ -8995,6 +9004,15 @@ var LabelDefinitionSchema = object({
8995
9004
  description: string().optional(),
8996
9005
  icon: string().optional()
8997
9006
  });
9007
+ var ClassMapDefinitionSchema = object({
9008
+ mapping: record(string(), _enum([
9009
+ "person",
9010
+ "vehicle",
9011
+ "animal",
9012
+ "package"
9013
+ ])),
9014
+ preserveOriginal: boolean()
9015
+ });
8998
9016
  var MODEL_FORMATS = [
8999
9017
  "onnx",
9000
9018
  "coreml",
@@ -9173,7 +9191,13 @@ var ModelCatalogEntrySchema = object({
9173
9191
  * `id` stays the source of truth for resolution/download/persistence; grouping
9174
9192
  * is a presentation overlay resolved back to an `id`.
9175
9193
  */
9176
- group: ModelVariantGroupSchema.optional()
9194
+ group: ModelVariantGroupSchema.optional(),
9195
+ /**
9196
+ * Per-MODEL class map override. Absent ⇒ the step's `StepDefinition.classMap`
9197
+ * applies (Frigate / COCO public catalog). Set on a custom model whose raw
9198
+ * labels already ARE the CamStack macros (Scrypted identity map).
9199
+ */
9200
+ classMap: ClassMapDefinitionSchema.optional()
9177
9201
  });
9178
9202
  var ConvertTargetSchema = discriminatedUnion("format", [object({
9179
9203
  format: literal("openvino"),
@@ -9202,7 +9226,8 @@ var ModelConvertMetadataSchema = object({
9202
9226
  "ocr",
9203
9227
  "segmentation"
9204
9228
  ]),
9205
- faceAlignment: boolean().optional()
9229
+ faceAlignment: boolean().optional(),
9230
+ classMap: ClassMapDefinitionSchema.optional()
9206
9231
  });
9207
9232
  var ConvertResultSchema = object({
9208
9233
  entry: ModelCatalogEntrySchema,
@@ -18495,6 +18520,33 @@ var NativeCropRefSchema = object({
18495
18520
  h: number()
18496
18521
  })
18497
18522
  });
18523
+ object({
18524
+ crop: object({
18525
+ left: number(),
18526
+ top: number(),
18527
+ width: number().positive(),
18528
+ height: number().positive()
18529
+ }).optional(),
18530
+ content: object({
18531
+ width: number().int().positive(),
18532
+ height: number().int().positive()
18533
+ }),
18534
+ fit: _enum(["stretch", "contain"]),
18535
+ format: _enum([
18536
+ "rgb",
18537
+ "gray",
18538
+ "jpeg"
18539
+ ])
18540
+ });
18541
+ var FrameRefSchema = object({
18542
+ registryId: string().min(1),
18543
+ id: string().min(1),
18544
+ width: number().int().positive(),
18545
+ height: number().int().positive(),
18546
+ format: _enum(["rgb", "gray"]),
18547
+ timestamp: number(),
18548
+ capturedAt: number().optional()
18549
+ });
18498
18550
  var ModelFormatSchema$1 = _enum([
18499
18551
  "onnx",
18500
18552
  "coreml",
@@ -18739,6 +18791,12 @@ method(_void(), array(PipelineEngineChoiceSchema)), method(_void(), PipelineEngi
18739
18791
  steps: array(PipelineStepInputSchema).min(1),
18740
18792
  frame: FrameInputSchema.optional(),
18741
18793
  /**
18794
+ * Process-local lazy frame. Valid only when caller and provider resolve
18795
+ * in the same execution-group process; split/cross-node callers use
18796
+ * `frame`/`image` inline compatibility instead.
18797
+ */
18798
+ frameRef: FrameRefSchema.optional(),
18799
+ /**
18742
18800
  * CB5 shm passthrough — a `FrameHandle` naming the same ring slot
18743
18801
  * the decoded pixels live in. One more member of the one-of
18744
18802
  * frame/frameHandle/image/imageBase64/referenceImage group.
@@ -19034,7 +19092,10 @@ var NativeCropResultSchema = object({
19034
19092
  * Which source served this crop, so a quality-sensitive consumer (the native
19035
19093
  * `keyFrame`) can reject a degraded fallback:
19036
19094
  * - `native` — cut from the decode worker's retained NATIVE surface (the
19037
- * quality path).
19095
+ * quality path). A subject-tile serve is also native-resolution and stays
19096
+ * `native` here: the public enum cannot name `tile` without a breaking cap
19097
+ * change. Runner telemetry distinguishes lease vs tile via `source` on the
19098
+ * internal crop result (`nativeHits` vs `tileHits`).
19038
19099
  * - `ram-fullframe` — the native surface MISSED but the request was full-frame,
19039
19100
  * so the ≤640 RAM `RetainedFrameStore` served it (honest lower-res; legit for
19040
19101
  * the blank-frame guard / 640-snapshot resolve, NOT for the clean keyFrame).
@@ -19525,12 +19586,41 @@ var RunnerLocalLoadSchema = object({
19525
19586
  * legacy `OrchestratorMetricsSchema` shape so existing dashboards keep
19526
19587
  * working unchanged when they switch to reading from the runner cap.
19527
19588
  */
19589
+ var FrameLazyCountersSchema = object({
19590
+ framesDecoded: number(),
19591
+ framesAdmitted: number(),
19592
+ framesDroppedPixelFree: number(),
19593
+ viewsMaterialized: number(),
19594
+ viewsSkipped: number(),
19595
+ workerToRunnerBytes: number(),
19596
+ runnerToPoolRawBytes: number(),
19597
+ runnerToPoolJpegBytes: number(),
19598
+ onDemandFullFrameRequests: number(),
19599
+ onDemandCropRequests: number(),
19600
+ nativeHits: number(),
19601
+ nativeMisses: number(),
19602
+ tileHits: number(),
19603
+ tileMisses: number(),
19604
+ fallbackHits: number(),
19605
+ fallbackMisses: number(),
19606
+ retainedWritesAvoided: number(),
19607
+ residentRefs: number(),
19608
+ residentBytes: number(),
19609
+ releases: number(),
19610
+ evictions: number(),
19611
+ staleMisses: number()
19612
+ });
19613
+ var FrameLazyMetricsSchema = object({
19614
+ node: FrameLazyCountersSchema,
19615
+ cameras: array(FrameLazyCountersSchema.extend({ deviceId: number() }))
19616
+ });
19528
19617
  var RunnerLocalMetricsSchema = object({
19529
19618
  nodeId: string(),
19530
19619
  activeCameras: number(),
19531
19620
  throttledCameras: number(),
19532
19621
  avgInferenceTimeMs: number(),
19533
- queueDepth: number()
19622
+ queueDepth: number(),
19623
+ frameLazy: FrameLazyMetricsSchema.optional()
19534
19624
  });
19535
19625
  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({
19536
19626
  handle: FrameHandleSchema,
@@ -20830,6 +20920,9 @@ method(_void(), ProviderInfoSchema), method(object({ config: record(string(), un
20830
20920
  location: StorageLocationSchema,
20831
20921
  relativePath: string()
20832
20922
  }), _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" });
20923
+ /** Profile-exported FormBuilder schema. Shape is ConfigUISchema at the UI. */
20924
+ var ProfileSettingsSchemaBridge = unknown().nullable();
20925
+ var ProfileSettingsBagSchema = record(string(), unknown());
20833
20926
  /**
20834
20927
  * A live terminal session hosted by the provider addon. Output and input do
20835
20928
  * NOT flow through the capability — they use the addon data plane
@@ -20859,7 +20952,14 @@ var TerminalSessionInfoSchema = object({
20859
20952
  var TerminalProfileInfoSchema = object({
20860
20953
  profileId: string(),
20861
20954
  label: string(),
20862
- description: string().optional()
20955
+ description: string().optional(),
20956
+ /** Spawn defaults the instance form copies on create. */
20957
+ executable: string().optional(),
20958
+ args: array(string()).readonly().optional(),
20959
+ cwd: string().optional(),
20960
+ environment: array(string()).readonly().optional(),
20961
+ /** ConfigUISchema for instance knobs, or null when the profile has none. */
20962
+ settingsSchema: ProfileSettingsSchemaBridge.optional()
20863
20963
  });
20864
20964
  /**
20865
20965
  * A durable operator-created Terminal instance. Profiles are templates; only
@@ -20872,7 +20972,12 @@ var TerminalInstanceInfoSchema = object({
20872
20972
  profileId: string(),
20873
20973
  profileLabel: string(),
20874
20974
  name: string(),
20875
- enabled: boolean()
20975
+ enabled: boolean(),
20976
+ executable: string(),
20977
+ args: array(string()).readonly(),
20978
+ cwd: string(),
20979
+ environment: array(string()).readonly(),
20980
+ profileSettings: ProfileSettingsBagSchema
20876
20981
  });
20877
20982
  var TerminalLegacyCameraSchema = object({
20878
20983
  stableId: string(),
@@ -20902,7 +21007,23 @@ var TerminalOutputBatchSchema = object({
20902
21007
  method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }), method(_void(), array(TerminalInstanceInfoSchema).readonly(), { auth: "admin" }), method(object({
20903
21008
  targetNodeId: string().min(1),
20904
21009
  profileId: string().min(1),
20905
- name: string().trim().min(1).max(160).optional()
21010
+ name: string().trim().min(1).max(160).optional(),
21011
+ executable: string().max(1024).optional(),
21012
+ args: array(string().max(2048)).max(64).optional(),
21013
+ cwd: string().max(1024).optional(),
21014
+ environment: array(string().max(4096)).max(64).optional(),
21015
+ profileSettings: ProfileSettingsBagSchema.optional()
21016
+ }), TerminalInstanceInfoSchema, {
21017
+ kind: "mutation",
21018
+ auth: "admin"
21019
+ }), method(object({
21020
+ instanceId: string().min(1),
21021
+ name: string().trim().min(1).max(160).optional(),
21022
+ executable: string().max(1024).optional(),
21023
+ args: array(string().max(2048)).max(64).optional(),
21024
+ cwd: string().max(1024).optional(),
21025
+ environment: array(string().max(4096)).max(64).optional(),
21026
+ profileSettings: ProfileSettingsBagSchema.optional()
20906
21027
  }), TerminalInstanceInfoSchema, {
20907
21028
  kind: "mutation",
20908
21029
  auth: "admin"
@@ -20924,7 +21045,11 @@ method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }),
20924
21045
  }), method(_void(), array(TerminalSessionInfoSchema).readonly(), { auth: "admin" }), method(object({
20925
21046
  profileId: string(),
20926
21047
  cols: number().int().positive(),
20927
- rows: number().int().positive()
21048
+ rows: number().int().positive(),
21049
+ executable: string().max(1024).optional(),
21050
+ args: array(string().max(2048)).max(64).optional(),
21051
+ cwd: string().max(1024).optional(),
21052
+ environment: array(string().max(4096)).max(64).optional()
20928
21053
  }), TerminalSessionInfoSchema, {
20929
21054
  kind: "mutation",
20930
21055
  auth: "admin"
@@ -24799,10 +24924,10 @@ var lawnMowerControlCapability = {
24799
24924
  *
24800
24925
  * • The SDK (mobile / web client) consumes `getConnectionEndpoints()`
24801
24926
  * to receive an ordered list of candidate base URLs it should race
24802
- * on connect — LAN IPv4 first (lowest latency when on same network),
24803
- * then public hostname (if a tunnel is up), then IPv6. The SDK can
24804
- * race them with short timeouts and stick with the winner for the
24805
- * session.
24927
+ * on connect — LAN IPv4 and stable LAN IPv6 first (lowest latency
24928
+ * when on the same network), then public hostname (if a tunnel is
24929
+ * up). The SDK can race them with short timeouts and stick with the
24930
+ * winner for the session.
24806
24931
  *
24807
24932
  * Why hub-only: agents are not directly addressable by the operator's
24808
24933
  * clients — they reverse-connect to the hub. Exposing their interfaces
@@ -24957,6 +25082,17 @@ var NotificationEndpointSchema = object({
24957
25082
  /** What the ranking currently resolves to (null when nothing is reachable). */
24958
25083
  resolved: string().nullable()
24959
25084
  });
25085
+ /**
25086
+ * The URLs the SDK / viewer should race for API access. `baseUrls` empty =
25087
+ * AUTO (every LAN IPv4 + the public tunnel). `resolved` is what that choice
25088
+ * currently expands to, so the UI can show the effective set either way.
25089
+ */
25090
+ var ViewerEndpointsSchema = object({
25091
+ /** The operator's explicit race set, or empty for AUTO. */
25092
+ baseUrls: array(string()).readonly(),
25093
+ /** What the ranking currently resolves to (may be empty if nothing is up). */
25094
+ resolved: array(string()).readonly()
25095
+ });
24960
25096
  var AllowedAddressesSchema = object({
24961
25097
  /**
24962
25098
  * Allowlist of interface addresses operators have explicitly opted
@@ -24965,6 +25101,20 @@ var AllowedAddressesSchema = object({
24965
25101
  * Network Addresses admin page and persisted by the addon.
24966
25102
  */
24967
25103
  addresses: array(string()).readonly() });
25104
+ var TlsStatusSchema = object({
25105
+ mode: _enum([
25106
+ "generated",
25107
+ "uploaded",
25108
+ "disabled"
25109
+ ]),
25110
+ leafFingerprintSha256: string().nullable(),
25111
+ caFingerprintSha256: string().nullable(),
25112
+ validTo: string().nullable(),
25113
+ sans: array(string()),
25114
+ caCertPem: string().nullable(),
25115
+ reissueError: string().nullable(),
25116
+ restartRequired: boolean()
25117
+ });
24968
25118
  method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(object({
24969
25119
  /**
24970
25120
  * LEGACY HINT — do not send from new code. Kept optional so clients
@@ -24974,17 +25124,31 @@ method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(obje
24974
25124
  */
24975
25125
  port: number().int().min(1).max(65535).optional(),
24976
25126
  /** Include `http(s)://127.0.0.1:<port>` as the lowest-priority
24977
- * candidate. Default `true`. */
25127
+ * candidate. Default `false` — loopback is not a client route. */
24978
25128
  includeLoopback: boolean().optional(),
24979
- /** Skip IPv6 entries. Some legacy clients can't parse them.
24980
- * Default `false`. */
25129
+ /** Skip IPv6 entries. Default `false` the palette includes stable
25130
+ * LAN IPv6 (ICE/WebRTC uses dual-stack regardless). Pass `true` to
25131
+ * hide them. The viewer HTTP/WS race is `getViewerEndpoints`. */
24981
25132
  ipv4Only: boolean().optional(),
24982
25133
  /** Scheme to emit for LAN/loopback URLs. Default `'http'`.
24983
25134
  * Pass `'https'` when the caller is itself loaded over HTTPS
24984
25135
  * to avoid mixed-content blocks in the browser. The public
24985
25136
  * tunnel always emits `https://` regardless. */
24986
25137
  scheme: _enum(["http", "https"]).optional()
24987
- }), 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" });
25138
+ }), 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, {
25139
+ kind: "mutation",
25140
+ auth: "admin"
25141
+ }), method(object({
25142
+ certPem: string().min(1),
25143
+ keyPem: string().min(1),
25144
+ caPem: string().optional()
25145
+ }), TlsStatusSchema, {
25146
+ kind: "mutation",
25147
+ auth: "admin"
25148
+ }), method(_void(), object({ pem: string() })), method(_void(), TlsStatusSchema, {
25149
+ kind: "mutation",
25150
+ auth: "admin"
25151
+ });
24988
25152
  var LockControlStatusSchema = object({
24989
25153
  /** Lifecycle state of the lock. `jammed` means the motor reported
24990
25154
  * failure to reach the target — operator intervention required. */
@@ -33011,6 +33175,12 @@ Object.freeze({
33011
33175
  addonId: null,
33012
33176
  access: "create"
33013
33177
  },
33178
+ "localNetwork.downloadCa": {
33179
+ capName: "local-network",
33180
+ capScope: "system",
33181
+ addonId: null,
33182
+ access: "view"
33183
+ },
33014
33184
  "localNetwork.getAllowedAddresses": {
33015
33185
  capName: "local-network",
33016
33186
  capScope: "system",
@@ -33035,18 +33205,42 @@ Object.freeze({
33035
33205
  addonId: null,
33036
33206
  access: "view"
33037
33207
  },
33208
+ "localNetwork.getTlsStatus": {
33209
+ capName: "local-network",
33210
+ capScope: "system",
33211
+ addonId: null,
33212
+ access: "view"
33213
+ },
33214
+ "localNetwork.getViewerEndpoints": {
33215
+ capName: "local-network",
33216
+ capScope: "system",
33217
+ addonId: null,
33218
+ access: "view"
33219
+ },
33038
33220
  "localNetwork.list": {
33039
33221
  capName: "local-network",
33040
33222
  capScope: "system",
33041
33223
  addonId: null,
33042
33224
  access: "view"
33043
33225
  },
33226
+ "localNetwork.regenerateCertificate": {
33227
+ capName: "local-network",
33228
+ capScope: "system",
33229
+ addonId: null,
33230
+ access: "create"
33231
+ },
33044
33232
  "localNetwork.resetAllowlistToBestMatch": {
33045
33233
  capName: "local-network",
33046
33234
  capScope: "system",
33047
33235
  addonId: null,
33048
33236
  access: "delete"
33049
33237
  },
33238
+ "localNetwork.revertToGeneratedCertificate": {
33239
+ capName: "local-network",
33240
+ capScope: "system",
33241
+ addonId: null,
33242
+ access: "create"
33243
+ },
33050
33244
  "localNetwork.setAllowedAddresses": {
33051
33245
  capName: "local-network",
33052
33246
  capScope: "system",
@@ -33059,6 +33253,18 @@ Object.freeze({
33059
33253
  addonId: null,
33060
33254
  access: "create"
33061
33255
  },
33256
+ "localNetwork.setViewerEndpoints": {
33257
+ capName: "local-network",
33258
+ capScope: "system",
33259
+ addonId: null,
33260
+ access: "create"
33261
+ },
33262
+ "localNetwork.uploadCertificate": {
33263
+ capName: "local-network",
33264
+ capScope: "system",
33265
+ addonId: null,
33266
+ access: "create"
33267
+ },
33062
33268
  "lockControl.lock": {
33063
33269
  capName: "lock-control",
33064
33270
  capScope: "device",
@@ -35939,6 +36145,12 @@ Object.freeze({
35939
36145
  addonId: null,
35940
36146
  access: "create"
35941
36147
  },
36148
+ "terminalSession.updateInstance": {
36149
+ capName: "terminal-session",
36150
+ capScope: "system",
36151
+ addonId: null,
36152
+ access: "create"
36153
+ },
35942
36154
  "terminalSession.writeInput": {
35943
36155
  capName: "terminal-session",
35944
36156
  capScope: "system",
@@ -38426,6 +38638,35 @@ Object.freeze(Object.fromEntries([{
38426
38638
  }]
38427
38639
  }].map((s) => [s.stepId, s.defaultModelId])));
38428
38640
  string().min(1);
38641
+ var CLUSTER_STEP_SETTING_FIELDS = [{
38642
+ stepId: "face-embedding",
38643
+ key: "minLandmarkFaceSize",
38644
+ label: "Min face size for recognition (detection px)",
38645
+ 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.",
38646
+ type: "slider",
38647
+ min: 0,
38648
+ max: 64,
38649
+ step: 2,
38650
+ default: 24
38651
+ }];
38652
+ function clusterStepSettingKey(stepId, fieldKey) {
38653
+ return `clusterStepSetting:${stepId}:${fieldKey}`;
38654
+ }
38655
+ var ClusterSettingNumberSchema = number().finite();
38656
+ function readClusterStepSettings(config) {
38657
+ const out = {};
38658
+ for (const field of CLUSTER_STEP_SETTING_FIELDS) {
38659
+ const parsed = ClusterSettingNumberSchema.safeParse(config[clusterStepSettingKey(field.stepId, field.key)]);
38660
+ const value = parsed.success ? parsed.data : field.default;
38661
+ const existing = out[field.stepId] ?? {};
38662
+ out[field.stepId] = {
38663
+ ...existing,
38664
+ [field.key]: value
38665
+ };
38666
+ }
38667
+ return out;
38668
+ }
38669
+ readClusterStepSettings({});
38429
38670
  object({
38430
38671
  /**
38431
38672
  * 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-rademacher",
3
- "version": "0.2.24",
3
+ "version": "0.2.26",
4
4
  "description": "Rademacher HomePilot device-provider addon for CamStack — wraps the @apocaliss92/noderademacher local-hub client (roller shutters over the cover cap)",
5
5
  "keywords": [
6
6
  "camstack",