@camstack/addon-provider-rademacher 0.2.50 → 0.2.52

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/addon.js CHANGED
@@ -8703,7 +8703,8 @@ var OpsLogReasonSchema = _enum([
8703
8703
  "quota",
8704
8704
  "manual",
8705
8705
  "operator",
8706
- "maintenance"
8706
+ "maintenance",
8707
+ "orphaned-device"
8707
8708
  ]);
8708
8709
  /** One audit row, shared verbatim by both domains. */
8709
8710
  var OpsLogEntrySchema = object({
@@ -13998,10 +13999,39 @@ var DevicePersistConfigPayloadSchema = object({
13998
13999
  deviceId: number(),
13999
14000
  data: record(string(), unknown())
14000
14001
  });
14002
+ /** What a migration actually did, per switch. `unreachable` is a first-class
14003
+ * answer: a camera that could not be asked is not a camera that was silenced. */
14004
+ var MigrateSwitchOutcomeSchema = _enum([
14005
+ "off",
14006
+ "on",
14007
+ "not-offered",
14008
+ "unreachable"
14009
+ ]);
14010
+ var MigrateSwitchReportSchema = object({
14011
+ deviceId: number(),
14012
+ switchId: CameraSwitchIdSchema,
14013
+ outcome: MigrateSwitchOutcomeSchema,
14014
+ detail: string().optional()
14015
+ });
14016
+ var MigrateDeviceResultSchema = object({
14017
+ sourceId: number(),
14018
+ targetId: number(),
14019
+ switches: array(MigrateSwitchReportSchema).readonly(),
14020
+ /** Switches that could NOT be turned off on the source. Empty is the only
14021
+ * value meaning the replaced hardware is quiet. */
14022
+ sourceStillLive: array(CameraSwitchIdSchema).readonly(),
14023
+ swapped: boolean()
14024
+ });
14001
14025
  method(object({
14002
14026
  addonId: string(),
14003
14027
  stableId: string()
14004
- }), object({ id: number() }), { kind: "mutation" }), method(DeviceRegisterPayloadSchema, _void(), { kind: "mutation" }), method(DeviceRemovePayloadSchema, _void(), { kind: "mutation" }), method(DevicePersistConfigPayloadSchema, _void(), { kind: "mutation" }), method(object({ deviceId: number() }), record(string(), unknown())), method(object({ deviceId: number() }), record(string(), unknown())), method(object({ deviceId: number() }), DeviceMetaSchema.nullable()), method(object({
14028
+ }), object({ id: number() }), { kind: "mutation" }), method(object({
14029
+ sourceId: number(),
14030
+ targetId: number()
14031
+ }), MigrateDeviceResultSchema, {
14032
+ kind: "mutation",
14033
+ auth: "admin"
14034
+ }), method(DeviceRegisterPayloadSchema, _void(), { kind: "mutation" }), method(DeviceRemovePayloadSchema, _void(), { kind: "mutation" }), method(DevicePersistConfigPayloadSchema, _void(), { kind: "mutation" }), method(object({ deviceId: number() }), record(string(), unknown())), method(object({ deviceId: number() }), record(string(), unknown())), method(object({ deviceId: number() }), DeviceMetaSchema.nullable()), method(object({
14005
14035
  deviceId: number(),
14006
14036
  name: string()
14007
14037
  }), _void(), {
@@ -20176,6 +20206,34 @@ var KeyEventsForDeviceSchema = object({
20176
20206
  deviceId: number(),
20177
20207
  events: array(KeyEventSchema).readonly()
20178
20208
  });
20209
+ /** One non-empty bucket of the timeline histogram. */
20210
+ var EventDensityBucketSchema = object({
20211
+ bucketStart: number(),
20212
+ motion: number().int(),
20213
+ object: number().int(),
20214
+ audio: number().int()
20215
+ });
20216
+ /**
20217
+ * One camera's row in a `getEventDensityBatch` answer.
20218
+ *
20219
+ * TWO facts, and the timeline draws them differently:
20220
+ *
20221
+ * - `read: 'read'` — the histogram for this camera. `buckets: []` is a real
20222
+ * claim: read, and nothing happened in the window.
20223
+ * - `read: 'unreadable'` — nobody could answer for this camera. `buckets` is
20224
+ * empty and that emptiness means NOTHING; the timeline must render unknown,
20225
+ * not quiet.
20226
+ *
20227
+ * Fanned out per camera the difference was free — one query rejected while the
20228
+ * others resolved — and collapsing to a batch is the exact moment it is lost
20229
+ * silently. Every requested id gets a row: a caller that asked for twelve and
20230
+ * got eleven cannot tell which one is missing, or that any is.
20231
+ */
20232
+ var EventDensityForDeviceSchema = object({
20233
+ deviceId: number(),
20234
+ read: _enum(["read", "unreadable"]),
20235
+ buckets: array(EventDensityBucketSchema).readonly()
20236
+ });
20179
20237
  object({
20180
20238
  trackId: string(),
20181
20239
  className: string(),
@@ -20486,12 +20544,12 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
20486
20544
  since: number(),
20487
20545
  until: number(),
20488
20546
  bucketMs: number().int().positive()
20489
- }), array(object({
20490
- bucketStart: number(),
20491
- motion: number().int(),
20492
- object: number().int(),
20493
- audio: number().int()
20494
- })).readonly()), method(object({
20547
+ }), array(EventDensityBucketSchema).readonly()), method(object({
20548
+ deviceIds: array(number()).min(1).max(200),
20549
+ since: number(),
20550
+ until: number(),
20551
+ bucketMs: number().int().positive()
20552
+ }), array(EventDensityForDeviceSchema).readonly()), method(object({
20495
20553
  deviceId: number(),
20496
20554
  cutoffMs: number()
20497
20555
  }), object({
@@ -29516,6 +29574,39 @@ var RecordingDaysSchema = object({
29516
29574
  /** Local-midnight epochs (UTC ms) of days that have ≥1 recorded segment. */
29517
29575
  days: array(number())
29518
29576
  });
29577
+ /**
29578
+ * One camera's row in a `getAvailabilityBatch` answer.
29579
+ *
29580
+ * `ranges` is EXACTLY what `getAvailability` returns for that camera — the
29581
+ * batch collapses the transport, not the work — plus the one thing the singular
29582
+ * method never had to say:
29583
+ *
29584
+ * - `read: 'read'` — answered. `ranges: []` means "read, and this camera has
29585
+ * no footage in the window", which is a real claim.
29586
+ * - `read: 'unreadable'` — the recorder could not answer for this camera (its
29587
+ * calendar rung threw, its location is unmounted). `ranges` is empty and
29588
+ * that emptiness means NOTHING.
29589
+ *
29590
+ * A timeline that renders the second as the first tells an operator there is no
29591
+ * footage when the truth is that nobody looked.
29592
+ */
29593
+ var RecordingAvailabilityForDeviceSchema = object({
29594
+ deviceId: number(),
29595
+ read: _enum(["read", "unreadable"]),
29596
+ ranges: array(RecordingRangeSchema).readonly()
29597
+ });
29598
+ /**
29599
+ * One camera's row in a `getDaysWithRecordingsBatch` answer. Same rule as
29600
+ * {@link RecordingAvailabilityForDeviceSchema}: `days: []` on a `'read'` row is
29601
+ * "no footage in this month", `read: 'unreadable'` is "nobody could look", and
29602
+ * the date-picker's day dots must not spell them the same.
29603
+ */
29604
+ var RecordingDaysForDeviceSchema = object({
29605
+ deviceId: number(),
29606
+ read: _enum(["read", "unreadable"]),
29607
+ /** Local-midnight epochs (UTC ms) of days that have ≥1 recorded segment. */
29608
+ days: array(number()).readonly()
29609
+ });
29519
29610
  var RecordingManifestSchema = object({
29520
29611
  deviceId: number(),
29521
29612
  /** Local filesystem path to the master playlist; null when no recording exists for the requested range. */
@@ -29750,6 +29841,13 @@ method(object({
29750
29841
  }), RecordingAvailabilitySchema, {
29751
29842
  kind: "query",
29752
29843
  auth: "protected"
29844
+ }), method(object({
29845
+ deviceIds: array(number()).min(1).max(200),
29846
+ fromMs: number(),
29847
+ toMs: number()
29848
+ }), array(RecordingAvailabilityForDeviceSchema).readonly(), {
29849
+ kind: "query",
29850
+ auth: "protected"
29753
29851
  }), method(object({
29754
29852
  deviceId: number(),
29755
29853
  fromMs: number(),
@@ -29758,6 +29856,14 @@ method(object({
29758
29856
  }), RecordingDaysSchema, {
29759
29857
  kind: "query",
29760
29858
  auth: "protected"
29859
+ }), method(object({
29860
+ deviceIds: array(number()).min(1).max(200),
29861
+ fromMs: number(),
29862
+ toMs: number(),
29863
+ tzOffsetMinutes: number()
29864
+ }), array(RecordingDaysForDeviceSchema).readonly(), {
29865
+ kind: "query",
29866
+ auth: "protected"
29761
29867
  }), method(object({
29762
29868
  deviceId: number(),
29763
29869
  fromMs: number(),
@@ -34920,6 +35026,12 @@ Object.freeze({
34920
35026
  addonId: null,
34921
35027
  access: "view"
34922
35028
  },
35029
+ "deviceManager.migrateDevice": {
35030
+ capName: "device-manager",
35031
+ capScope: "system",
35032
+ addonId: null,
35033
+ access: "create"
35034
+ },
34923
35035
  "deviceManager.persistConfig": {
34924
35036
  capName: "device-manager",
34925
35037
  capScope: "system",
@@ -36654,6 +36766,12 @@ Object.freeze({
36654
36766
  addonId: null,
36655
36767
  access: "view"
36656
36768
  },
36769
+ "pipelineAnalytics.getEventDensityBatch": {
36770
+ capName: "pipeline-analytics",
36771
+ capScope: "device",
36772
+ addonId: null,
36773
+ access: "view"
36774
+ },
36657
36775
  "pipelineAnalytics.getEventMedia": {
36658
36776
  capName: "pipeline-analytics",
36659
36777
  capScope: "device",
@@ -37776,12 +37894,24 @@ Object.freeze({
37776
37894
  addonId: null,
37777
37895
  access: "view"
37778
37896
  },
37897
+ "recording.getAvailabilityBatch": {
37898
+ capName: "recording",
37899
+ capScope: "system",
37900
+ addonId: null,
37901
+ access: "view"
37902
+ },
37779
37903
  "recording.getDaysWithRecordings": {
37780
37904
  capName: "recording",
37781
37905
  capScope: "system",
37782
37906
  addonId: null,
37783
37907
  access: "view"
37784
37908
  },
37909
+ "recording.getDaysWithRecordingsBatch": {
37910
+ capName: "recording",
37911
+ capScope: "system",
37912
+ addonId: null,
37913
+ access: "view"
37914
+ },
37785
37915
  "recording.getDeviceConfig": {
37786
37916
  capName: "recording",
37787
37917
  capScope: "system",
@@ -40386,6 +40516,11 @@ Object.freeze({
40386
40516
  form: "single",
40387
40517
  optional: false
40388
40518
  }],
40519
+ "pipelineAnalytics.getEventDensityBatch": [{
40520
+ name: "deviceIds",
40521
+ form: "array",
40522
+ optional: false
40523
+ }],
40389
40524
  "pipelineAnalytics.getEventMedia": [{
40390
40525
  name: "deviceId",
40391
40526
  form: "single",
@@ -40801,11 +40936,21 @@ Object.freeze({
40801
40936
  form: "single",
40802
40937
  optional: false
40803
40938
  }],
40939
+ "recording.getAvailabilityBatch": [{
40940
+ name: "deviceIds",
40941
+ form: "array",
40942
+ optional: false
40943
+ }],
40804
40944
  "recording.getDaysWithRecordings": [{
40805
40945
  name: "deviceId",
40806
40946
  form: "single",
40807
40947
  optional: false
40808
40948
  }],
40949
+ "recording.getDaysWithRecordingsBatch": [{
40950
+ name: "deviceIds",
40951
+ form: "array",
40952
+ optional: false
40953
+ }],
40809
40954
  "recording.getDeviceConfig": [{
40810
40955
  name: "deviceId",
40811
40956
  form: "single",
package/dist/addon.mjs CHANGED
@@ -8702,7 +8702,8 @@ var OpsLogReasonSchema = _enum([
8702
8702
  "quota",
8703
8703
  "manual",
8704
8704
  "operator",
8705
- "maintenance"
8705
+ "maintenance",
8706
+ "orphaned-device"
8706
8707
  ]);
8707
8708
  /** One audit row, shared verbatim by both domains. */
8708
8709
  var OpsLogEntrySchema = object({
@@ -13997,10 +13998,39 @@ var DevicePersistConfigPayloadSchema = object({
13997
13998
  deviceId: number(),
13998
13999
  data: record(string(), unknown())
13999
14000
  });
14001
+ /** What a migration actually did, per switch. `unreachable` is a first-class
14002
+ * answer: a camera that could not be asked is not a camera that was silenced. */
14003
+ var MigrateSwitchOutcomeSchema = _enum([
14004
+ "off",
14005
+ "on",
14006
+ "not-offered",
14007
+ "unreachable"
14008
+ ]);
14009
+ var MigrateSwitchReportSchema = object({
14010
+ deviceId: number(),
14011
+ switchId: CameraSwitchIdSchema,
14012
+ outcome: MigrateSwitchOutcomeSchema,
14013
+ detail: string().optional()
14014
+ });
14015
+ var MigrateDeviceResultSchema = object({
14016
+ sourceId: number(),
14017
+ targetId: number(),
14018
+ switches: array(MigrateSwitchReportSchema).readonly(),
14019
+ /** Switches that could NOT be turned off on the source. Empty is the only
14020
+ * value meaning the replaced hardware is quiet. */
14021
+ sourceStillLive: array(CameraSwitchIdSchema).readonly(),
14022
+ swapped: boolean()
14023
+ });
14000
14024
  method(object({
14001
14025
  addonId: string(),
14002
14026
  stableId: string()
14003
- }), object({ id: number() }), { kind: "mutation" }), method(DeviceRegisterPayloadSchema, _void(), { kind: "mutation" }), method(DeviceRemovePayloadSchema, _void(), { kind: "mutation" }), method(DevicePersistConfigPayloadSchema, _void(), { kind: "mutation" }), method(object({ deviceId: number() }), record(string(), unknown())), method(object({ deviceId: number() }), record(string(), unknown())), method(object({ deviceId: number() }), DeviceMetaSchema.nullable()), method(object({
14027
+ }), object({ id: number() }), { kind: "mutation" }), method(object({
14028
+ sourceId: number(),
14029
+ targetId: number()
14030
+ }), MigrateDeviceResultSchema, {
14031
+ kind: "mutation",
14032
+ auth: "admin"
14033
+ }), method(DeviceRegisterPayloadSchema, _void(), { kind: "mutation" }), method(DeviceRemovePayloadSchema, _void(), { kind: "mutation" }), method(DevicePersistConfigPayloadSchema, _void(), { kind: "mutation" }), method(object({ deviceId: number() }), record(string(), unknown())), method(object({ deviceId: number() }), record(string(), unknown())), method(object({ deviceId: number() }), DeviceMetaSchema.nullable()), method(object({
14004
14034
  deviceId: number(),
14005
14035
  name: string()
14006
14036
  }), _void(), {
@@ -20175,6 +20205,34 @@ var KeyEventsForDeviceSchema = object({
20175
20205
  deviceId: number(),
20176
20206
  events: array(KeyEventSchema).readonly()
20177
20207
  });
20208
+ /** One non-empty bucket of the timeline histogram. */
20209
+ var EventDensityBucketSchema = object({
20210
+ bucketStart: number(),
20211
+ motion: number().int(),
20212
+ object: number().int(),
20213
+ audio: number().int()
20214
+ });
20215
+ /**
20216
+ * One camera's row in a `getEventDensityBatch` answer.
20217
+ *
20218
+ * TWO facts, and the timeline draws them differently:
20219
+ *
20220
+ * - `read: 'read'` — the histogram for this camera. `buckets: []` is a real
20221
+ * claim: read, and nothing happened in the window.
20222
+ * - `read: 'unreadable'` — nobody could answer for this camera. `buckets` is
20223
+ * empty and that emptiness means NOTHING; the timeline must render unknown,
20224
+ * not quiet.
20225
+ *
20226
+ * Fanned out per camera the difference was free — one query rejected while the
20227
+ * others resolved — and collapsing to a batch is the exact moment it is lost
20228
+ * silently. Every requested id gets a row: a caller that asked for twelve and
20229
+ * got eleven cannot tell which one is missing, or that any is.
20230
+ */
20231
+ var EventDensityForDeviceSchema = object({
20232
+ deviceId: number(),
20233
+ read: _enum(["read", "unreadable"]),
20234
+ buckets: array(EventDensityBucketSchema).readonly()
20235
+ });
20178
20236
  object({
20179
20237
  trackId: string(),
20180
20238
  className: string(),
@@ -20485,12 +20543,12 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
20485
20543
  since: number(),
20486
20544
  until: number(),
20487
20545
  bucketMs: number().int().positive()
20488
- }), array(object({
20489
- bucketStart: number(),
20490
- motion: number().int(),
20491
- object: number().int(),
20492
- audio: number().int()
20493
- })).readonly()), method(object({
20546
+ }), array(EventDensityBucketSchema).readonly()), method(object({
20547
+ deviceIds: array(number()).min(1).max(200),
20548
+ since: number(),
20549
+ until: number(),
20550
+ bucketMs: number().int().positive()
20551
+ }), array(EventDensityForDeviceSchema).readonly()), method(object({
20494
20552
  deviceId: number(),
20495
20553
  cutoffMs: number()
20496
20554
  }), object({
@@ -29515,6 +29573,39 @@ var RecordingDaysSchema = object({
29515
29573
  /** Local-midnight epochs (UTC ms) of days that have ≥1 recorded segment. */
29516
29574
  days: array(number())
29517
29575
  });
29576
+ /**
29577
+ * One camera's row in a `getAvailabilityBatch` answer.
29578
+ *
29579
+ * `ranges` is EXACTLY what `getAvailability` returns for that camera — the
29580
+ * batch collapses the transport, not the work — plus the one thing the singular
29581
+ * method never had to say:
29582
+ *
29583
+ * - `read: 'read'` — answered. `ranges: []` means "read, and this camera has
29584
+ * no footage in the window", which is a real claim.
29585
+ * - `read: 'unreadable'` — the recorder could not answer for this camera (its
29586
+ * calendar rung threw, its location is unmounted). `ranges` is empty and
29587
+ * that emptiness means NOTHING.
29588
+ *
29589
+ * A timeline that renders the second as the first tells an operator there is no
29590
+ * footage when the truth is that nobody looked.
29591
+ */
29592
+ var RecordingAvailabilityForDeviceSchema = object({
29593
+ deviceId: number(),
29594
+ read: _enum(["read", "unreadable"]),
29595
+ ranges: array(RecordingRangeSchema).readonly()
29596
+ });
29597
+ /**
29598
+ * One camera's row in a `getDaysWithRecordingsBatch` answer. Same rule as
29599
+ * {@link RecordingAvailabilityForDeviceSchema}: `days: []` on a `'read'` row is
29600
+ * "no footage in this month", `read: 'unreadable'` is "nobody could look", and
29601
+ * the date-picker's day dots must not spell them the same.
29602
+ */
29603
+ var RecordingDaysForDeviceSchema = object({
29604
+ deviceId: number(),
29605
+ read: _enum(["read", "unreadable"]),
29606
+ /** Local-midnight epochs (UTC ms) of days that have ≥1 recorded segment. */
29607
+ days: array(number()).readonly()
29608
+ });
29518
29609
  var RecordingManifestSchema = object({
29519
29610
  deviceId: number(),
29520
29611
  /** Local filesystem path to the master playlist; null when no recording exists for the requested range. */
@@ -29749,6 +29840,13 @@ method(object({
29749
29840
  }), RecordingAvailabilitySchema, {
29750
29841
  kind: "query",
29751
29842
  auth: "protected"
29843
+ }), method(object({
29844
+ deviceIds: array(number()).min(1).max(200),
29845
+ fromMs: number(),
29846
+ toMs: number()
29847
+ }), array(RecordingAvailabilityForDeviceSchema).readonly(), {
29848
+ kind: "query",
29849
+ auth: "protected"
29752
29850
  }), method(object({
29753
29851
  deviceId: number(),
29754
29852
  fromMs: number(),
@@ -29757,6 +29855,14 @@ method(object({
29757
29855
  }), RecordingDaysSchema, {
29758
29856
  kind: "query",
29759
29857
  auth: "protected"
29858
+ }), method(object({
29859
+ deviceIds: array(number()).min(1).max(200),
29860
+ fromMs: number(),
29861
+ toMs: number(),
29862
+ tzOffsetMinutes: number()
29863
+ }), array(RecordingDaysForDeviceSchema).readonly(), {
29864
+ kind: "query",
29865
+ auth: "protected"
29760
29866
  }), method(object({
29761
29867
  deviceId: number(),
29762
29868
  fromMs: number(),
@@ -34919,6 +35025,12 @@ Object.freeze({
34919
35025
  addonId: null,
34920
35026
  access: "view"
34921
35027
  },
35028
+ "deviceManager.migrateDevice": {
35029
+ capName: "device-manager",
35030
+ capScope: "system",
35031
+ addonId: null,
35032
+ access: "create"
35033
+ },
34922
35034
  "deviceManager.persistConfig": {
34923
35035
  capName: "device-manager",
34924
35036
  capScope: "system",
@@ -36653,6 +36765,12 @@ Object.freeze({
36653
36765
  addonId: null,
36654
36766
  access: "view"
36655
36767
  },
36768
+ "pipelineAnalytics.getEventDensityBatch": {
36769
+ capName: "pipeline-analytics",
36770
+ capScope: "device",
36771
+ addonId: null,
36772
+ access: "view"
36773
+ },
36656
36774
  "pipelineAnalytics.getEventMedia": {
36657
36775
  capName: "pipeline-analytics",
36658
36776
  capScope: "device",
@@ -37775,12 +37893,24 @@ Object.freeze({
37775
37893
  addonId: null,
37776
37894
  access: "view"
37777
37895
  },
37896
+ "recording.getAvailabilityBatch": {
37897
+ capName: "recording",
37898
+ capScope: "system",
37899
+ addonId: null,
37900
+ access: "view"
37901
+ },
37778
37902
  "recording.getDaysWithRecordings": {
37779
37903
  capName: "recording",
37780
37904
  capScope: "system",
37781
37905
  addonId: null,
37782
37906
  access: "view"
37783
37907
  },
37908
+ "recording.getDaysWithRecordingsBatch": {
37909
+ capName: "recording",
37910
+ capScope: "system",
37911
+ addonId: null,
37912
+ access: "view"
37913
+ },
37784
37914
  "recording.getDeviceConfig": {
37785
37915
  capName: "recording",
37786
37916
  capScope: "system",
@@ -40385,6 +40515,11 @@ Object.freeze({
40385
40515
  form: "single",
40386
40516
  optional: false
40387
40517
  }],
40518
+ "pipelineAnalytics.getEventDensityBatch": [{
40519
+ name: "deviceIds",
40520
+ form: "array",
40521
+ optional: false
40522
+ }],
40388
40523
  "pipelineAnalytics.getEventMedia": [{
40389
40524
  name: "deviceId",
40390
40525
  form: "single",
@@ -40800,11 +40935,21 @@ Object.freeze({
40800
40935
  form: "single",
40801
40936
  optional: false
40802
40937
  }],
40938
+ "recording.getAvailabilityBatch": [{
40939
+ name: "deviceIds",
40940
+ form: "array",
40941
+ optional: false
40942
+ }],
40803
40943
  "recording.getDaysWithRecordings": [{
40804
40944
  name: "deviceId",
40805
40945
  form: "single",
40806
40946
  optional: false
40807
40947
  }],
40948
+ "recording.getDaysWithRecordingsBatch": [{
40949
+ name: "deviceIds",
40950
+ form: "array",
40951
+ optional: false
40952
+ }],
40808
40953
  "recording.getDeviceConfig": [{
40809
40954
  name: "deviceId",
40810
40955
  form: "single",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-rademacher",
3
- "version": "0.2.50",
3
+ "version": "0.2.52",
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",