@camstack/addon-provider-petkit 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
@@ -8820,7 +8820,8 @@ var OpsLogReasonSchema = _enum([
8820
8820
  "quota",
8821
8821
  "manual",
8822
8822
  "operator",
8823
- "maintenance"
8823
+ "maintenance",
8824
+ "orphaned-device"
8824
8825
  ]);
8825
8826
  /** One audit row, shared verbatim by both domains. */
8826
8827
  var OpsLogEntrySchema = object({
@@ -14132,10 +14133,39 @@ var DevicePersistConfigPayloadSchema = object({
14132
14133
  deviceId: number(),
14133
14134
  data: record(string(), unknown())
14134
14135
  });
14136
+ /** What a migration actually did, per switch. `unreachable` is a first-class
14137
+ * answer: a camera that could not be asked is not a camera that was silenced. */
14138
+ var MigrateSwitchOutcomeSchema = _enum([
14139
+ "off",
14140
+ "on",
14141
+ "not-offered",
14142
+ "unreachable"
14143
+ ]);
14144
+ var MigrateSwitchReportSchema = object({
14145
+ deviceId: number(),
14146
+ switchId: CameraSwitchIdSchema,
14147
+ outcome: MigrateSwitchOutcomeSchema,
14148
+ detail: string().optional()
14149
+ });
14150
+ var MigrateDeviceResultSchema = object({
14151
+ sourceId: number(),
14152
+ targetId: number(),
14153
+ switches: array(MigrateSwitchReportSchema).readonly(),
14154
+ /** Switches that could NOT be turned off on the source. Empty is the only
14155
+ * value meaning the replaced hardware is quiet. */
14156
+ sourceStillLive: array(CameraSwitchIdSchema).readonly(),
14157
+ swapped: boolean()
14158
+ });
14135
14159
  method(object({
14136
14160
  addonId: string(),
14137
14161
  stableId: string()
14138
- }), 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({
14162
+ }), object({ id: number() }), { kind: "mutation" }), method(object({
14163
+ sourceId: number(),
14164
+ targetId: number()
14165
+ }), MigrateDeviceResultSchema, {
14166
+ kind: "mutation",
14167
+ auth: "admin"
14168
+ }), 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({
14139
14169
  deviceId: number(),
14140
14170
  name: string()
14141
14171
  }), _void(), {
@@ -20310,6 +20340,34 @@ var KeyEventsForDeviceSchema = object({
20310
20340
  deviceId: number(),
20311
20341
  events: array(KeyEventSchema).readonly()
20312
20342
  });
20343
+ /** One non-empty bucket of the timeline histogram. */
20344
+ var EventDensityBucketSchema = object({
20345
+ bucketStart: number(),
20346
+ motion: number().int(),
20347
+ object: number().int(),
20348
+ audio: number().int()
20349
+ });
20350
+ /**
20351
+ * One camera's row in a `getEventDensityBatch` answer.
20352
+ *
20353
+ * TWO facts, and the timeline draws them differently:
20354
+ *
20355
+ * - `read: 'read'` — the histogram for this camera. `buckets: []` is a real
20356
+ * claim: read, and nothing happened in the window.
20357
+ * - `read: 'unreadable'` — nobody could answer for this camera. `buckets` is
20358
+ * empty and that emptiness means NOTHING; the timeline must render unknown,
20359
+ * not quiet.
20360
+ *
20361
+ * Fanned out per camera the difference was free — one query rejected while the
20362
+ * others resolved — and collapsing to a batch is the exact moment it is lost
20363
+ * silently. Every requested id gets a row: a caller that asked for twelve and
20364
+ * got eleven cannot tell which one is missing, or that any is.
20365
+ */
20366
+ var EventDensityForDeviceSchema = object({
20367
+ deviceId: number(),
20368
+ read: _enum(["read", "unreadable"]),
20369
+ buckets: array(EventDensityBucketSchema).readonly()
20370
+ });
20313
20371
  object({
20314
20372
  trackId: string(),
20315
20373
  className: string(),
@@ -20620,12 +20678,12 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
20620
20678
  since: number(),
20621
20679
  until: number(),
20622
20680
  bucketMs: number().int().positive()
20623
- }), array(object({
20624
- bucketStart: number(),
20625
- motion: number().int(),
20626
- object: number().int(),
20627
- audio: number().int()
20628
- })).readonly()), method(object({
20681
+ }), array(EventDensityBucketSchema).readonly()), method(object({
20682
+ deviceIds: array(number()).min(1).max(200),
20683
+ since: number(),
20684
+ until: number(),
20685
+ bucketMs: number().int().positive()
20686
+ }), array(EventDensityForDeviceSchema).readonly()), method(object({
20629
20687
  deviceId: number(),
20630
20688
  cutoffMs: number()
20631
20689
  }), object({
@@ -29658,6 +29716,39 @@ var RecordingDaysSchema = object({
29658
29716
  /** Local-midnight epochs (UTC ms) of days that have ≥1 recorded segment. */
29659
29717
  days: array(number())
29660
29718
  });
29719
+ /**
29720
+ * One camera's row in a `getAvailabilityBatch` answer.
29721
+ *
29722
+ * `ranges` is EXACTLY what `getAvailability` returns for that camera — the
29723
+ * batch collapses the transport, not the work — plus the one thing the singular
29724
+ * method never had to say:
29725
+ *
29726
+ * - `read: 'read'` — answered. `ranges: []` means "read, and this camera has
29727
+ * no footage in the window", which is a real claim.
29728
+ * - `read: 'unreadable'` — the recorder could not answer for this camera (its
29729
+ * calendar rung threw, its location is unmounted). `ranges` is empty and
29730
+ * that emptiness means NOTHING.
29731
+ *
29732
+ * A timeline that renders the second as the first tells an operator there is no
29733
+ * footage when the truth is that nobody looked.
29734
+ */
29735
+ var RecordingAvailabilityForDeviceSchema = object({
29736
+ deviceId: number(),
29737
+ read: _enum(["read", "unreadable"]),
29738
+ ranges: array(RecordingRangeSchema).readonly()
29739
+ });
29740
+ /**
29741
+ * One camera's row in a `getDaysWithRecordingsBatch` answer. Same rule as
29742
+ * {@link RecordingAvailabilityForDeviceSchema}: `days: []` on a `'read'` row is
29743
+ * "no footage in this month", `read: 'unreadable'` is "nobody could look", and
29744
+ * the date-picker's day dots must not spell them the same.
29745
+ */
29746
+ var RecordingDaysForDeviceSchema = object({
29747
+ deviceId: number(),
29748
+ read: _enum(["read", "unreadable"]),
29749
+ /** Local-midnight epochs (UTC ms) of days that have ≥1 recorded segment. */
29750
+ days: array(number()).readonly()
29751
+ });
29661
29752
  var RecordingManifestSchema = object({
29662
29753
  deviceId: number(),
29663
29754
  /** Local filesystem path to the master playlist; null when no recording exists for the requested range. */
@@ -29892,6 +29983,13 @@ method(object({
29892
29983
  }), RecordingAvailabilitySchema, {
29893
29984
  kind: "query",
29894
29985
  auth: "protected"
29986
+ }), method(object({
29987
+ deviceIds: array(number()).min(1).max(200),
29988
+ fromMs: number(),
29989
+ toMs: number()
29990
+ }), array(RecordingAvailabilityForDeviceSchema).readonly(), {
29991
+ kind: "query",
29992
+ auth: "protected"
29895
29993
  }), method(object({
29896
29994
  deviceId: number(),
29897
29995
  fromMs: number(),
@@ -29900,6 +29998,14 @@ method(object({
29900
29998
  }), RecordingDaysSchema, {
29901
29999
  kind: "query",
29902
30000
  auth: "protected"
30001
+ }), method(object({
30002
+ deviceIds: array(number()).min(1).max(200),
30003
+ fromMs: number(),
30004
+ toMs: number(),
30005
+ tzOffsetMinutes: number()
30006
+ }), array(RecordingDaysForDeviceSchema).readonly(), {
30007
+ kind: "query",
30008
+ auth: "protected"
29903
30009
  }), method(object({
29904
30010
  deviceId: number(),
29905
30011
  fromMs: number(),
@@ -35062,6 +35168,12 @@ Object.freeze({
35062
35168
  addonId: null,
35063
35169
  access: "view"
35064
35170
  },
35171
+ "deviceManager.migrateDevice": {
35172
+ capName: "device-manager",
35173
+ capScope: "system",
35174
+ addonId: null,
35175
+ access: "create"
35176
+ },
35065
35177
  "deviceManager.persistConfig": {
35066
35178
  capName: "device-manager",
35067
35179
  capScope: "system",
@@ -36796,6 +36908,12 @@ Object.freeze({
36796
36908
  addonId: null,
36797
36909
  access: "view"
36798
36910
  },
36911
+ "pipelineAnalytics.getEventDensityBatch": {
36912
+ capName: "pipeline-analytics",
36913
+ capScope: "device",
36914
+ addonId: null,
36915
+ access: "view"
36916
+ },
36799
36917
  "pipelineAnalytics.getEventMedia": {
36800
36918
  capName: "pipeline-analytics",
36801
36919
  capScope: "device",
@@ -37918,12 +38036,24 @@ Object.freeze({
37918
38036
  addonId: null,
37919
38037
  access: "view"
37920
38038
  },
38039
+ "recording.getAvailabilityBatch": {
38040
+ capName: "recording",
38041
+ capScope: "system",
38042
+ addonId: null,
38043
+ access: "view"
38044
+ },
37921
38045
  "recording.getDaysWithRecordings": {
37922
38046
  capName: "recording",
37923
38047
  capScope: "system",
37924
38048
  addonId: null,
37925
38049
  access: "view"
37926
38050
  },
38051
+ "recording.getDaysWithRecordingsBatch": {
38052
+ capName: "recording",
38053
+ capScope: "system",
38054
+ addonId: null,
38055
+ access: "view"
38056
+ },
37927
38057
  "recording.getDeviceConfig": {
37928
38058
  capName: "recording",
37929
38059
  capScope: "system",
@@ -40528,6 +40658,11 @@ Object.freeze({
40528
40658
  form: "single",
40529
40659
  optional: false
40530
40660
  }],
40661
+ "pipelineAnalytics.getEventDensityBatch": [{
40662
+ name: "deviceIds",
40663
+ form: "array",
40664
+ optional: false
40665
+ }],
40531
40666
  "pipelineAnalytics.getEventMedia": [{
40532
40667
  name: "deviceId",
40533
40668
  form: "single",
@@ -40943,11 +41078,21 @@ Object.freeze({
40943
41078
  form: "single",
40944
41079
  optional: false
40945
41080
  }],
41081
+ "recording.getAvailabilityBatch": [{
41082
+ name: "deviceIds",
41083
+ form: "array",
41084
+ optional: false
41085
+ }],
40946
41086
  "recording.getDaysWithRecordings": [{
40947
41087
  name: "deviceId",
40948
41088
  form: "single",
40949
41089
  optional: false
40950
41090
  }],
41091
+ "recording.getDaysWithRecordingsBatch": [{
41092
+ name: "deviceIds",
41093
+ form: "array",
41094
+ optional: false
41095
+ }],
40951
41096
  "recording.getDeviceConfig": [{
40952
41097
  name: "deviceId",
40953
41098
  form: "single",
package/dist/addon.mjs CHANGED
@@ -8819,7 +8819,8 @@ var OpsLogReasonSchema = _enum([
8819
8819
  "quota",
8820
8820
  "manual",
8821
8821
  "operator",
8822
- "maintenance"
8822
+ "maintenance",
8823
+ "orphaned-device"
8823
8824
  ]);
8824
8825
  /** One audit row, shared verbatim by both domains. */
8825
8826
  var OpsLogEntrySchema = object({
@@ -14131,10 +14132,39 @@ var DevicePersistConfigPayloadSchema = object({
14131
14132
  deviceId: number(),
14132
14133
  data: record(string(), unknown())
14133
14134
  });
14135
+ /** What a migration actually did, per switch. `unreachable` is a first-class
14136
+ * answer: a camera that could not be asked is not a camera that was silenced. */
14137
+ var MigrateSwitchOutcomeSchema = _enum([
14138
+ "off",
14139
+ "on",
14140
+ "not-offered",
14141
+ "unreachable"
14142
+ ]);
14143
+ var MigrateSwitchReportSchema = object({
14144
+ deviceId: number(),
14145
+ switchId: CameraSwitchIdSchema,
14146
+ outcome: MigrateSwitchOutcomeSchema,
14147
+ detail: string().optional()
14148
+ });
14149
+ var MigrateDeviceResultSchema = object({
14150
+ sourceId: number(),
14151
+ targetId: number(),
14152
+ switches: array(MigrateSwitchReportSchema).readonly(),
14153
+ /** Switches that could NOT be turned off on the source. Empty is the only
14154
+ * value meaning the replaced hardware is quiet. */
14155
+ sourceStillLive: array(CameraSwitchIdSchema).readonly(),
14156
+ swapped: boolean()
14157
+ });
14134
14158
  method(object({
14135
14159
  addonId: string(),
14136
14160
  stableId: string()
14137
- }), 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({
14161
+ }), object({ id: number() }), { kind: "mutation" }), method(object({
14162
+ sourceId: number(),
14163
+ targetId: number()
14164
+ }), MigrateDeviceResultSchema, {
14165
+ kind: "mutation",
14166
+ auth: "admin"
14167
+ }), 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({
14138
14168
  deviceId: number(),
14139
14169
  name: string()
14140
14170
  }), _void(), {
@@ -20309,6 +20339,34 @@ var KeyEventsForDeviceSchema = object({
20309
20339
  deviceId: number(),
20310
20340
  events: array(KeyEventSchema).readonly()
20311
20341
  });
20342
+ /** One non-empty bucket of the timeline histogram. */
20343
+ var EventDensityBucketSchema = object({
20344
+ bucketStart: number(),
20345
+ motion: number().int(),
20346
+ object: number().int(),
20347
+ audio: number().int()
20348
+ });
20349
+ /**
20350
+ * One camera's row in a `getEventDensityBatch` answer.
20351
+ *
20352
+ * TWO facts, and the timeline draws them differently:
20353
+ *
20354
+ * - `read: 'read'` — the histogram for this camera. `buckets: []` is a real
20355
+ * claim: read, and nothing happened in the window.
20356
+ * - `read: 'unreadable'` — nobody could answer for this camera. `buckets` is
20357
+ * empty and that emptiness means NOTHING; the timeline must render unknown,
20358
+ * not quiet.
20359
+ *
20360
+ * Fanned out per camera the difference was free — one query rejected while the
20361
+ * others resolved — and collapsing to a batch is the exact moment it is lost
20362
+ * silently. Every requested id gets a row: a caller that asked for twelve and
20363
+ * got eleven cannot tell which one is missing, or that any is.
20364
+ */
20365
+ var EventDensityForDeviceSchema = object({
20366
+ deviceId: number(),
20367
+ read: _enum(["read", "unreadable"]),
20368
+ buckets: array(EventDensityBucketSchema).readonly()
20369
+ });
20312
20370
  object({
20313
20371
  trackId: string(),
20314
20372
  className: string(),
@@ -20619,12 +20677,12 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
20619
20677
  since: number(),
20620
20678
  until: number(),
20621
20679
  bucketMs: number().int().positive()
20622
- }), array(object({
20623
- bucketStart: number(),
20624
- motion: number().int(),
20625
- object: number().int(),
20626
- audio: number().int()
20627
- })).readonly()), method(object({
20680
+ }), array(EventDensityBucketSchema).readonly()), method(object({
20681
+ deviceIds: array(number()).min(1).max(200),
20682
+ since: number(),
20683
+ until: number(),
20684
+ bucketMs: number().int().positive()
20685
+ }), array(EventDensityForDeviceSchema).readonly()), method(object({
20628
20686
  deviceId: number(),
20629
20687
  cutoffMs: number()
20630
20688
  }), object({
@@ -29657,6 +29715,39 @@ var RecordingDaysSchema = object({
29657
29715
  /** Local-midnight epochs (UTC ms) of days that have ≥1 recorded segment. */
29658
29716
  days: array(number())
29659
29717
  });
29718
+ /**
29719
+ * One camera's row in a `getAvailabilityBatch` answer.
29720
+ *
29721
+ * `ranges` is EXACTLY what `getAvailability` returns for that camera — the
29722
+ * batch collapses the transport, not the work — plus the one thing the singular
29723
+ * method never had to say:
29724
+ *
29725
+ * - `read: 'read'` — answered. `ranges: []` means "read, and this camera has
29726
+ * no footage in the window", which is a real claim.
29727
+ * - `read: 'unreadable'` — the recorder could not answer for this camera (its
29728
+ * calendar rung threw, its location is unmounted). `ranges` is empty and
29729
+ * that emptiness means NOTHING.
29730
+ *
29731
+ * A timeline that renders the second as the first tells an operator there is no
29732
+ * footage when the truth is that nobody looked.
29733
+ */
29734
+ var RecordingAvailabilityForDeviceSchema = object({
29735
+ deviceId: number(),
29736
+ read: _enum(["read", "unreadable"]),
29737
+ ranges: array(RecordingRangeSchema).readonly()
29738
+ });
29739
+ /**
29740
+ * One camera's row in a `getDaysWithRecordingsBatch` answer. Same rule as
29741
+ * {@link RecordingAvailabilityForDeviceSchema}: `days: []` on a `'read'` row is
29742
+ * "no footage in this month", `read: 'unreadable'` is "nobody could look", and
29743
+ * the date-picker's day dots must not spell them the same.
29744
+ */
29745
+ var RecordingDaysForDeviceSchema = object({
29746
+ deviceId: number(),
29747
+ read: _enum(["read", "unreadable"]),
29748
+ /** Local-midnight epochs (UTC ms) of days that have ≥1 recorded segment. */
29749
+ days: array(number()).readonly()
29750
+ });
29660
29751
  var RecordingManifestSchema = object({
29661
29752
  deviceId: number(),
29662
29753
  /** Local filesystem path to the master playlist; null when no recording exists for the requested range. */
@@ -29891,6 +29982,13 @@ method(object({
29891
29982
  }), RecordingAvailabilitySchema, {
29892
29983
  kind: "query",
29893
29984
  auth: "protected"
29985
+ }), method(object({
29986
+ deviceIds: array(number()).min(1).max(200),
29987
+ fromMs: number(),
29988
+ toMs: number()
29989
+ }), array(RecordingAvailabilityForDeviceSchema).readonly(), {
29990
+ kind: "query",
29991
+ auth: "protected"
29894
29992
  }), method(object({
29895
29993
  deviceId: number(),
29896
29994
  fromMs: number(),
@@ -29899,6 +29997,14 @@ method(object({
29899
29997
  }), RecordingDaysSchema, {
29900
29998
  kind: "query",
29901
29999
  auth: "protected"
30000
+ }), method(object({
30001
+ deviceIds: array(number()).min(1).max(200),
30002
+ fromMs: number(),
30003
+ toMs: number(),
30004
+ tzOffsetMinutes: number()
30005
+ }), array(RecordingDaysForDeviceSchema).readonly(), {
30006
+ kind: "query",
30007
+ auth: "protected"
29902
30008
  }), method(object({
29903
30009
  deviceId: number(),
29904
30010
  fromMs: number(),
@@ -35061,6 +35167,12 @@ Object.freeze({
35061
35167
  addonId: null,
35062
35168
  access: "view"
35063
35169
  },
35170
+ "deviceManager.migrateDevice": {
35171
+ capName: "device-manager",
35172
+ capScope: "system",
35173
+ addonId: null,
35174
+ access: "create"
35175
+ },
35064
35176
  "deviceManager.persistConfig": {
35065
35177
  capName: "device-manager",
35066
35178
  capScope: "system",
@@ -36795,6 +36907,12 @@ Object.freeze({
36795
36907
  addonId: null,
36796
36908
  access: "view"
36797
36909
  },
36910
+ "pipelineAnalytics.getEventDensityBatch": {
36911
+ capName: "pipeline-analytics",
36912
+ capScope: "device",
36913
+ addonId: null,
36914
+ access: "view"
36915
+ },
36798
36916
  "pipelineAnalytics.getEventMedia": {
36799
36917
  capName: "pipeline-analytics",
36800
36918
  capScope: "device",
@@ -37917,12 +38035,24 @@ Object.freeze({
37917
38035
  addonId: null,
37918
38036
  access: "view"
37919
38037
  },
38038
+ "recording.getAvailabilityBatch": {
38039
+ capName: "recording",
38040
+ capScope: "system",
38041
+ addonId: null,
38042
+ access: "view"
38043
+ },
37920
38044
  "recording.getDaysWithRecordings": {
37921
38045
  capName: "recording",
37922
38046
  capScope: "system",
37923
38047
  addonId: null,
37924
38048
  access: "view"
37925
38049
  },
38050
+ "recording.getDaysWithRecordingsBatch": {
38051
+ capName: "recording",
38052
+ capScope: "system",
38053
+ addonId: null,
38054
+ access: "view"
38055
+ },
37926
38056
  "recording.getDeviceConfig": {
37927
38057
  capName: "recording",
37928
38058
  capScope: "system",
@@ -40527,6 +40657,11 @@ Object.freeze({
40527
40657
  form: "single",
40528
40658
  optional: false
40529
40659
  }],
40660
+ "pipelineAnalytics.getEventDensityBatch": [{
40661
+ name: "deviceIds",
40662
+ form: "array",
40663
+ optional: false
40664
+ }],
40530
40665
  "pipelineAnalytics.getEventMedia": [{
40531
40666
  name: "deviceId",
40532
40667
  form: "single",
@@ -40942,11 +41077,21 @@ Object.freeze({
40942
41077
  form: "single",
40943
41078
  optional: false
40944
41079
  }],
41080
+ "recording.getAvailabilityBatch": [{
41081
+ name: "deviceIds",
41082
+ form: "array",
41083
+ optional: false
41084
+ }],
40945
41085
  "recording.getDaysWithRecordings": [{
40946
41086
  name: "deviceId",
40947
41087
  form: "single",
40948
41088
  optional: false
40949
41089
  }],
41090
+ "recording.getDaysWithRecordingsBatch": [{
41091
+ name: "deviceIds",
41092
+ form: "array",
41093
+ optional: false
41094
+ }],
40950
41095
  "recording.getDeviceConfig": [{
40951
41096
  name: "deviceId",
40952
41097
  form: "single",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-petkit",
3
- "version": "0.2.51",
3
+ "version": "0.2.53",
4
4
  "description": "PetKit smart-feeder device-provider addon for CamStack — wraps the @apocaliss92/nodepetkit PetKit cloud client",
5
5
  "keywords": [
6
6
  "camstack",