@camstack/addon-provider-dreo 0.2.51 → 0.2.53

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
@@ -7754,7 +7754,8 @@ var OpsLogReasonSchema = _enum([
7754
7754
  "quota",
7755
7755
  "manual",
7756
7756
  "operator",
7757
- "maintenance"
7757
+ "maintenance",
7758
+ "orphaned-device"
7758
7759
  ]);
7759
7760
  /** One audit row, shared verbatim by both domains. */
7760
7761
  var OpsLogEntrySchema = object({
@@ -13066,10 +13067,39 @@ var DevicePersistConfigPayloadSchema = object({
13066
13067
  deviceId: number(),
13067
13068
  data: record(string(), unknown())
13068
13069
  });
13070
+ /** What a migration actually did, per switch. `unreachable` is a first-class
13071
+ * answer: a camera that could not be asked is not a camera that was silenced. */
13072
+ var MigrateSwitchOutcomeSchema = _enum([
13073
+ "off",
13074
+ "on",
13075
+ "not-offered",
13076
+ "unreachable"
13077
+ ]);
13078
+ var MigrateSwitchReportSchema = object({
13079
+ deviceId: number(),
13080
+ switchId: CameraSwitchIdSchema,
13081
+ outcome: MigrateSwitchOutcomeSchema,
13082
+ detail: string().optional()
13083
+ });
13084
+ var MigrateDeviceResultSchema = object({
13085
+ sourceId: number(),
13086
+ targetId: number(),
13087
+ switches: array(MigrateSwitchReportSchema).readonly(),
13088
+ /** Switches that could NOT be turned off on the source. Empty is the only
13089
+ * value meaning the replaced hardware is quiet. */
13090
+ sourceStillLive: array(CameraSwitchIdSchema).readonly(),
13091
+ swapped: boolean()
13092
+ });
13069
13093
  method(object({
13070
13094
  addonId: string(),
13071
13095
  stableId: string()
13072
- }), 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({
13096
+ }), object({ id: number() }), { kind: "mutation" }), method(object({
13097
+ sourceId: number(),
13098
+ targetId: number()
13099
+ }), MigrateDeviceResultSchema, {
13100
+ kind: "mutation",
13101
+ auth: "admin"
13102
+ }), 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({
13073
13103
  deviceId: number(),
13074
13104
  name: string()
13075
13105
  }), _void(), {
@@ -19244,6 +19274,34 @@ var KeyEventsForDeviceSchema = object({
19244
19274
  deviceId: number(),
19245
19275
  events: array(KeyEventSchema).readonly()
19246
19276
  });
19277
+ /** One non-empty bucket of the timeline histogram. */
19278
+ var EventDensityBucketSchema = object({
19279
+ bucketStart: number(),
19280
+ motion: number().int(),
19281
+ object: number().int(),
19282
+ audio: number().int()
19283
+ });
19284
+ /**
19285
+ * One camera's row in a `getEventDensityBatch` answer.
19286
+ *
19287
+ * TWO facts, and the timeline draws them differently:
19288
+ *
19289
+ * - `read: 'read'` — the histogram for this camera. `buckets: []` is a real
19290
+ * claim: read, and nothing happened in the window.
19291
+ * - `read: 'unreadable'` — nobody could answer for this camera. `buckets` is
19292
+ * empty and that emptiness means NOTHING; the timeline must render unknown,
19293
+ * not quiet.
19294
+ *
19295
+ * Fanned out per camera the difference was free — one query rejected while the
19296
+ * others resolved — and collapsing to a batch is the exact moment it is lost
19297
+ * silently. Every requested id gets a row: a caller that asked for twelve and
19298
+ * got eleven cannot tell which one is missing, or that any is.
19299
+ */
19300
+ var EventDensityForDeviceSchema = object({
19301
+ deviceId: number(),
19302
+ read: _enum(["read", "unreadable"]),
19303
+ buckets: array(EventDensityBucketSchema).readonly()
19304
+ });
19247
19305
  object({
19248
19306
  trackId: string(),
19249
19307
  className: string(),
@@ -19554,12 +19612,12 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19554
19612
  since: number(),
19555
19613
  until: number(),
19556
19614
  bucketMs: number().int().positive()
19557
- }), array(object({
19558
- bucketStart: number(),
19559
- motion: number().int(),
19560
- object: number().int(),
19561
- audio: number().int()
19562
- })).readonly()), method(object({
19615
+ }), array(EventDensityBucketSchema).readonly()), method(object({
19616
+ deviceIds: array(number()).min(1).max(200),
19617
+ since: number(),
19618
+ until: number(),
19619
+ bucketMs: number().int().positive()
19620
+ }), array(EventDensityForDeviceSchema).readonly()), method(object({
19563
19621
  deviceId: number(),
19564
19622
  cutoffMs: number()
19565
19623
  }), object({
@@ -28592,6 +28650,39 @@ var RecordingDaysSchema = object({
28592
28650
  /** Local-midnight epochs (UTC ms) of days that have ≥1 recorded segment. */
28593
28651
  days: array(number())
28594
28652
  });
28653
+ /**
28654
+ * One camera's row in a `getAvailabilityBatch` answer.
28655
+ *
28656
+ * `ranges` is EXACTLY what `getAvailability` returns for that camera — the
28657
+ * batch collapses the transport, not the work — plus the one thing the singular
28658
+ * method never had to say:
28659
+ *
28660
+ * - `read: 'read'` — answered. `ranges: []` means "read, and this camera has
28661
+ * no footage in the window", which is a real claim.
28662
+ * - `read: 'unreadable'` — the recorder could not answer for this camera (its
28663
+ * calendar rung threw, its location is unmounted). `ranges` is empty and
28664
+ * that emptiness means NOTHING.
28665
+ *
28666
+ * A timeline that renders the second as the first tells an operator there is no
28667
+ * footage when the truth is that nobody looked.
28668
+ */
28669
+ var RecordingAvailabilityForDeviceSchema = object({
28670
+ deviceId: number(),
28671
+ read: _enum(["read", "unreadable"]),
28672
+ ranges: array(RecordingRangeSchema).readonly()
28673
+ });
28674
+ /**
28675
+ * One camera's row in a `getDaysWithRecordingsBatch` answer. Same rule as
28676
+ * {@link RecordingAvailabilityForDeviceSchema}: `days: []` on a `'read'` row is
28677
+ * "no footage in this month", `read: 'unreadable'` is "nobody could look", and
28678
+ * the date-picker's day dots must not spell them the same.
28679
+ */
28680
+ var RecordingDaysForDeviceSchema = object({
28681
+ deviceId: number(),
28682
+ read: _enum(["read", "unreadable"]),
28683
+ /** Local-midnight epochs (UTC ms) of days that have ≥1 recorded segment. */
28684
+ days: array(number()).readonly()
28685
+ });
28595
28686
  var RecordingManifestSchema = object({
28596
28687
  deviceId: number(),
28597
28688
  /** Local filesystem path to the master playlist; null when no recording exists for the requested range. */
@@ -28826,6 +28917,13 @@ method(object({
28826
28917
  }), RecordingAvailabilitySchema, {
28827
28918
  kind: "query",
28828
28919
  auth: "protected"
28920
+ }), method(object({
28921
+ deviceIds: array(number()).min(1).max(200),
28922
+ fromMs: number(),
28923
+ toMs: number()
28924
+ }), array(RecordingAvailabilityForDeviceSchema).readonly(), {
28925
+ kind: "query",
28926
+ auth: "protected"
28829
28927
  }), method(object({
28830
28928
  deviceId: number(),
28831
28929
  fromMs: number(),
@@ -28834,6 +28932,14 @@ method(object({
28834
28932
  }), RecordingDaysSchema, {
28835
28933
  kind: "query",
28836
28934
  auth: "protected"
28935
+ }), method(object({
28936
+ deviceIds: array(number()).min(1).max(200),
28937
+ fromMs: number(),
28938
+ toMs: number(),
28939
+ tzOffsetMinutes: number()
28940
+ }), array(RecordingDaysForDeviceSchema).readonly(), {
28941
+ kind: "query",
28942
+ auth: "protected"
28837
28943
  }), method(object({
28838
28944
  deviceId: number(),
28839
28945
  fromMs: number(),
@@ -33996,6 +34102,12 @@ Object.freeze({
33996
34102
  addonId: null,
33997
34103
  access: "view"
33998
34104
  },
34105
+ "deviceManager.migrateDevice": {
34106
+ capName: "device-manager",
34107
+ capScope: "system",
34108
+ addonId: null,
34109
+ access: "create"
34110
+ },
33999
34111
  "deviceManager.persistConfig": {
34000
34112
  capName: "device-manager",
34001
34113
  capScope: "system",
@@ -35730,6 +35842,12 @@ Object.freeze({
35730
35842
  addonId: null,
35731
35843
  access: "view"
35732
35844
  },
35845
+ "pipelineAnalytics.getEventDensityBatch": {
35846
+ capName: "pipeline-analytics",
35847
+ capScope: "device",
35848
+ addonId: null,
35849
+ access: "view"
35850
+ },
35733
35851
  "pipelineAnalytics.getEventMedia": {
35734
35852
  capName: "pipeline-analytics",
35735
35853
  capScope: "device",
@@ -36852,12 +36970,24 @@ Object.freeze({
36852
36970
  addonId: null,
36853
36971
  access: "view"
36854
36972
  },
36973
+ "recording.getAvailabilityBatch": {
36974
+ capName: "recording",
36975
+ capScope: "system",
36976
+ addonId: null,
36977
+ access: "view"
36978
+ },
36855
36979
  "recording.getDaysWithRecordings": {
36856
36980
  capName: "recording",
36857
36981
  capScope: "system",
36858
36982
  addonId: null,
36859
36983
  access: "view"
36860
36984
  },
36985
+ "recording.getDaysWithRecordingsBatch": {
36986
+ capName: "recording",
36987
+ capScope: "system",
36988
+ addonId: null,
36989
+ access: "view"
36990
+ },
36861
36991
  "recording.getDeviceConfig": {
36862
36992
  capName: "recording",
36863
36993
  capScope: "system",
@@ -39462,6 +39592,11 @@ Object.freeze({
39462
39592
  form: "single",
39463
39593
  optional: false
39464
39594
  }],
39595
+ "pipelineAnalytics.getEventDensityBatch": [{
39596
+ name: "deviceIds",
39597
+ form: "array",
39598
+ optional: false
39599
+ }],
39465
39600
  "pipelineAnalytics.getEventMedia": [{
39466
39601
  name: "deviceId",
39467
39602
  form: "single",
@@ -39877,11 +40012,21 @@ Object.freeze({
39877
40012
  form: "single",
39878
40013
  optional: false
39879
40014
  }],
40015
+ "recording.getAvailabilityBatch": [{
40016
+ name: "deviceIds",
40017
+ form: "array",
40018
+ optional: false
40019
+ }],
39880
40020
  "recording.getDaysWithRecordings": [{
39881
40021
  name: "deviceId",
39882
40022
  form: "single",
39883
40023
  optional: false
39884
40024
  }],
40025
+ "recording.getDaysWithRecordingsBatch": [{
40026
+ name: "deviceIds",
40027
+ form: "array",
40028
+ optional: false
40029
+ }],
39885
40030
  "recording.getDeviceConfig": [{
39886
40031
  name: "deviceId",
39887
40032
  form: "single",
package/dist/addon.mjs CHANGED
@@ -7755,7 +7755,8 @@ var OpsLogReasonSchema = _enum([
7755
7755
  "quota",
7756
7756
  "manual",
7757
7757
  "operator",
7758
- "maintenance"
7758
+ "maintenance",
7759
+ "orphaned-device"
7759
7760
  ]);
7760
7761
  /** One audit row, shared verbatim by both domains. */
7761
7762
  var OpsLogEntrySchema = object({
@@ -13067,10 +13068,39 @@ var DevicePersistConfigPayloadSchema = object({
13067
13068
  deviceId: number(),
13068
13069
  data: record(string(), unknown())
13069
13070
  });
13071
+ /** What a migration actually did, per switch. `unreachable` is a first-class
13072
+ * answer: a camera that could not be asked is not a camera that was silenced. */
13073
+ var MigrateSwitchOutcomeSchema = _enum([
13074
+ "off",
13075
+ "on",
13076
+ "not-offered",
13077
+ "unreachable"
13078
+ ]);
13079
+ var MigrateSwitchReportSchema = object({
13080
+ deviceId: number(),
13081
+ switchId: CameraSwitchIdSchema,
13082
+ outcome: MigrateSwitchOutcomeSchema,
13083
+ detail: string().optional()
13084
+ });
13085
+ var MigrateDeviceResultSchema = object({
13086
+ sourceId: number(),
13087
+ targetId: number(),
13088
+ switches: array(MigrateSwitchReportSchema).readonly(),
13089
+ /** Switches that could NOT be turned off on the source. Empty is the only
13090
+ * value meaning the replaced hardware is quiet. */
13091
+ sourceStillLive: array(CameraSwitchIdSchema).readonly(),
13092
+ swapped: boolean()
13093
+ });
13070
13094
  method(object({
13071
13095
  addonId: string(),
13072
13096
  stableId: string()
13073
- }), 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({
13097
+ }), object({ id: number() }), { kind: "mutation" }), method(object({
13098
+ sourceId: number(),
13099
+ targetId: number()
13100
+ }), MigrateDeviceResultSchema, {
13101
+ kind: "mutation",
13102
+ auth: "admin"
13103
+ }), 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({
13074
13104
  deviceId: number(),
13075
13105
  name: string()
13076
13106
  }), _void(), {
@@ -19245,6 +19275,34 @@ var KeyEventsForDeviceSchema = object({
19245
19275
  deviceId: number(),
19246
19276
  events: array(KeyEventSchema).readonly()
19247
19277
  });
19278
+ /** One non-empty bucket of the timeline histogram. */
19279
+ var EventDensityBucketSchema = object({
19280
+ bucketStart: number(),
19281
+ motion: number().int(),
19282
+ object: number().int(),
19283
+ audio: number().int()
19284
+ });
19285
+ /**
19286
+ * One camera's row in a `getEventDensityBatch` answer.
19287
+ *
19288
+ * TWO facts, and the timeline draws them differently:
19289
+ *
19290
+ * - `read: 'read'` — the histogram for this camera. `buckets: []` is a real
19291
+ * claim: read, and nothing happened in the window.
19292
+ * - `read: 'unreadable'` — nobody could answer for this camera. `buckets` is
19293
+ * empty and that emptiness means NOTHING; the timeline must render unknown,
19294
+ * not quiet.
19295
+ *
19296
+ * Fanned out per camera the difference was free — one query rejected while the
19297
+ * others resolved — and collapsing to a batch is the exact moment it is lost
19298
+ * silently. Every requested id gets a row: a caller that asked for twelve and
19299
+ * got eleven cannot tell which one is missing, or that any is.
19300
+ */
19301
+ var EventDensityForDeviceSchema = object({
19302
+ deviceId: number(),
19303
+ read: _enum(["read", "unreadable"]),
19304
+ buckets: array(EventDensityBucketSchema).readonly()
19305
+ });
19248
19306
  object({
19249
19307
  trackId: string(),
19250
19308
  className: string(),
@@ -19555,12 +19613,12 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19555
19613
  since: number(),
19556
19614
  until: number(),
19557
19615
  bucketMs: number().int().positive()
19558
- }), array(object({
19559
- bucketStart: number(),
19560
- motion: number().int(),
19561
- object: number().int(),
19562
- audio: number().int()
19563
- })).readonly()), method(object({
19616
+ }), array(EventDensityBucketSchema).readonly()), method(object({
19617
+ deviceIds: array(number()).min(1).max(200),
19618
+ since: number(),
19619
+ until: number(),
19620
+ bucketMs: number().int().positive()
19621
+ }), array(EventDensityForDeviceSchema).readonly()), method(object({
19564
19622
  deviceId: number(),
19565
19623
  cutoffMs: number()
19566
19624
  }), object({
@@ -28593,6 +28651,39 @@ var RecordingDaysSchema = object({
28593
28651
  /** Local-midnight epochs (UTC ms) of days that have ≥1 recorded segment. */
28594
28652
  days: array(number())
28595
28653
  });
28654
+ /**
28655
+ * One camera's row in a `getAvailabilityBatch` answer.
28656
+ *
28657
+ * `ranges` is EXACTLY what `getAvailability` returns for that camera — the
28658
+ * batch collapses the transport, not the work — plus the one thing the singular
28659
+ * method never had to say:
28660
+ *
28661
+ * - `read: 'read'` — answered. `ranges: []` means "read, and this camera has
28662
+ * no footage in the window", which is a real claim.
28663
+ * - `read: 'unreadable'` — the recorder could not answer for this camera (its
28664
+ * calendar rung threw, its location is unmounted). `ranges` is empty and
28665
+ * that emptiness means NOTHING.
28666
+ *
28667
+ * A timeline that renders the second as the first tells an operator there is no
28668
+ * footage when the truth is that nobody looked.
28669
+ */
28670
+ var RecordingAvailabilityForDeviceSchema = object({
28671
+ deviceId: number(),
28672
+ read: _enum(["read", "unreadable"]),
28673
+ ranges: array(RecordingRangeSchema).readonly()
28674
+ });
28675
+ /**
28676
+ * One camera's row in a `getDaysWithRecordingsBatch` answer. Same rule as
28677
+ * {@link RecordingAvailabilityForDeviceSchema}: `days: []` on a `'read'` row is
28678
+ * "no footage in this month", `read: 'unreadable'` is "nobody could look", and
28679
+ * the date-picker's day dots must not spell them the same.
28680
+ */
28681
+ var RecordingDaysForDeviceSchema = object({
28682
+ deviceId: number(),
28683
+ read: _enum(["read", "unreadable"]),
28684
+ /** Local-midnight epochs (UTC ms) of days that have ≥1 recorded segment. */
28685
+ days: array(number()).readonly()
28686
+ });
28596
28687
  var RecordingManifestSchema = object({
28597
28688
  deviceId: number(),
28598
28689
  /** Local filesystem path to the master playlist; null when no recording exists for the requested range. */
@@ -28827,6 +28918,13 @@ method(object({
28827
28918
  }), RecordingAvailabilitySchema, {
28828
28919
  kind: "query",
28829
28920
  auth: "protected"
28921
+ }), method(object({
28922
+ deviceIds: array(number()).min(1).max(200),
28923
+ fromMs: number(),
28924
+ toMs: number()
28925
+ }), array(RecordingAvailabilityForDeviceSchema).readonly(), {
28926
+ kind: "query",
28927
+ auth: "protected"
28830
28928
  }), method(object({
28831
28929
  deviceId: number(),
28832
28930
  fromMs: number(),
@@ -28835,6 +28933,14 @@ method(object({
28835
28933
  }), RecordingDaysSchema, {
28836
28934
  kind: "query",
28837
28935
  auth: "protected"
28936
+ }), method(object({
28937
+ deviceIds: array(number()).min(1).max(200),
28938
+ fromMs: number(),
28939
+ toMs: number(),
28940
+ tzOffsetMinutes: number()
28941
+ }), array(RecordingDaysForDeviceSchema).readonly(), {
28942
+ kind: "query",
28943
+ auth: "protected"
28838
28944
  }), method(object({
28839
28945
  deviceId: number(),
28840
28946
  fromMs: number(),
@@ -33997,6 +34103,12 @@ Object.freeze({
33997
34103
  addonId: null,
33998
34104
  access: "view"
33999
34105
  },
34106
+ "deviceManager.migrateDevice": {
34107
+ capName: "device-manager",
34108
+ capScope: "system",
34109
+ addonId: null,
34110
+ access: "create"
34111
+ },
34000
34112
  "deviceManager.persistConfig": {
34001
34113
  capName: "device-manager",
34002
34114
  capScope: "system",
@@ -35731,6 +35843,12 @@ Object.freeze({
35731
35843
  addonId: null,
35732
35844
  access: "view"
35733
35845
  },
35846
+ "pipelineAnalytics.getEventDensityBatch": {
35847
+ capName: "pipeline-analytics",
35848
+ capScope: "device",
35849
+ addonId: null,
35850
+ access: "view"
35851
+ },
35734
35852
  "pipelineAnalytics.getEventMedia": {
35735
35853
  capName: "pipeline-analytics",
35736
35854
  capScope: "device",
@@ -36853,12 +36971,24 @@ Object.freeze({
36853
36971
  addonId: null,
36854
36972
  access: "view"
36855
36973
  },
36974
+ "recording.getAvailabilityBatch": {
36975
+ capName: "recording",
36976
+ capScope: "system",
36977
+ addonId: null,
36978
+ access: "view"
36979
+ },
36856
36980
  "recording.getDaysWithRecordings": {
36857
36981
  capName: "recording",
36858
36982
  capScope: "system",
36859
36983
  addonId: null,
36860
36984
  access: "view"
36861
36985
  },
36986
+ "recording.getDaysWithRecordingsBatch": {
36987
+ capName: "recording",
36988
+ capScope: "system",
36989
+ addonId: null,
36990
+ access: "view"
36991
+ },
36862
36992
  "recording.getDeviceConfig": {
36863
36993
  capName: "recording",
36864
36994
  capScope: "system",
@@ -39463,6 +39593,11 @@ Object.freeze({
39463
39593
  form: "single",
39464
39594
  optional: false
39465
39595
  }],
39596
+ "pipelineAnalytics.getEventDensityBatch": [{
39597
+ name: "deviceIds",
39598
+ form: "array",
39599
+ optional: false
39600
+ }],
39466
39601
  "pipelineAnalytics.getEventMedia": [{
39467
39602
  name: "deviceId",
39468
39603
  form: "single",
@@ -39878,11 +40013,21 @@ Object.freeze({
39878
40013
  form: "single",
39879
40014
  optional: false
39880
40015
  }],
40016
+ "recording.getAvailabilityBatch": [{
40017
+ name: "deviceIds",
40018
+ form: "array",
40019
+ optional: false
40020
+ }],
39881
40021
  "recording.getDaysWithRecordings": [{
39882
40022
  name: "deviceId",
39883
40023
  form: "single",
39884
40024
  optional: false
39885
40025
  }],
40026
+ "recording.getDaysWithRecordingsBatch": [{
40027
+ name: "deviceIds",
40028
+ form: "array",
40029
+ optional: false
40030
+ }],
39886
40031
  "recording.getDeviceConfig": [{
39887
40032
  name: "deviceId",
39888
40033
  form: "single",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-dreo",
3
- "version": "0.2.51",
3
+ "version": "0.2.53",
4
4
  "description": "Dreo smart-device (fan / air-circulator / purifier / heater / humidifier) device-provider addon for CamStack — wraps the @apocaliss92/nodedreo Dreo cloud client (REST + WebSocket)",
5
5
  "keywords": [
6
6
  "camstack",