homebridge-deconz 0.1.15 → 0.1.17

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.
@@ -15,6 +15,12 @@ const { buttonEvent } = Deconz.ApiClient
15
15
  const { SINGLE, DOUBLE, LONG } = DeconzService.Button
16
16
  const rtypes = ['lights', 'sensors', 'groups']
17
17
 
18
+ const patterns = {
19
+ uniqueid: /^([0-9a-f]{1,2}(?:[:-]?(?:[0-9a-f]{1,2})){7})-([0-9a-z]{2})(?:-([0-9a-z]{4}))?$/i,
20
+ clipId: /^(S[0-9]{1,3})-([0-9a-z]{2})-([0-9a-z]{4})$/i,
21
+ swversion: /^([0-9]+)(?:\.([0-9]+)(?:\.([0-9]+)(?:_([0-9]{4}))?)?)?$/
22
+ }
23
+
18
24
  // From low to high.
19
25
  const sensorsPrios = [
20
26
  'Power',
@@ -24,7 +30,8 @@ const sensorsPrios = [
24
30
  'Motion',
25
31
  'Contact',
26
32
  'AirPurifier',
27
- 'Thermostat'
33
+ 'Thermostat',
34
+ 'Flag'
28
35
  ]
29
36
 
30
37
  // =============================================================================
@@ -51,12 +58,41 @@ class Resource {
51
58
  */
52
59
  static parseUniqueid (uniqueid) {
53
60
  toString('uniqueid', uniqueid, true)
54
- const a = uniqueid.replace(/:/g, '').toUpperCase().split('-')
61
+ const a = patterns.uniqueid.exec(uniqueid.replace(/:/g, '').toUpperCase())
62
+ return {
63
+ mac: a?.[1],
64
+ endpoint: a?.[2],
65
+ cluster: a?.[3]
66
+ }
67
+ }
68
+
69
+ /** Parse the `uniqueid` in the resource body of a resource for a CLIP sensor.
70
+ * @param {string} uniqueid - The `uniqueid`.
71
+ * @return {object} The MultiCLIP `id`, `endpoint`, and `cluster`.
72
+ */
73
+ static parseClipId (uniqueid) {
74
+ toString('uniqueid', uniqueid, true)
75
+ const a = patterns.clipId.exec(uniqueid.replace(/:/g, '').toUpperCase())
55
76
  return {
56
- mac: a?.[0],
57
- endpoint: a?.[1],
58
- cluster: a?.[2]
77
+ id: a?.[1],
78
+ endpoint: a?.[2],
79
+ cluster: a?.[3]
80
+ }
81
+ }
82
+
83
+ /** Parse the `swversion` in the resource body of a resource for a Zigbee device.
84
+ * @param {string} swversion - The `swversion`.
85
+ * @return {string} The normalised version in semver format.
86
+ */
87
+ static parseSwversion (swversion) {
88
+ if (swversion == null) {
89
+ return '0.0.0'
90
+ }
91
+ const a = patterns.swversion.exec(swversion)
92
+ if (a?.[1] === '0' && a?.[2] === '0' && a?.[3] === '0' && a?.[4] != null) {
93
+ return '0.0.' + Number(a[4]).toString()
59
94
  }
95
+ return swversion
60
96
  }
61
97
 
62
98
  /** Create a new instance of a delegate of a resource.
@@ -90,6 +126,8 @@ class Resource {
90
126
  toString('body.name', body.name, true)
91
127
  toString('body.type', body.type, true)
92
128
 
129
+ let realDevice = false
130
+
93
131
  if (
94
132
  this.rtype === 'lights' ||
95
133
  (this.rtype === 'sensors' && this.body.type.startsWith('Z'))
@@ -129,10 +167,31 @@ class Resource {
129
167
  * @type {boolean}
130
168
  */
131
169
  this.zigbee = true
132
- } else if (this.isMultiClip()) {
133
- const a = body.uniqueid.split('-')
134
- this.id = gateway.id + '-M' + a[0]
135
- this.subtype = a[1]
170
+
171
+ realDevice = true
172
+ } else if (this.rtype === 'sensors') {
173
+ const { mac, endpoint, cluster } = Resource.parseUniqueid(body.uniqueid)
174
+ if (mac != null && endpoint != null && cluster != null) {
175
+ // uniqueid for proxy device has proper mac, endpoint, cluster
176
+ this.id = mac
177
+ this.subtype = endpoint + '-' + cluster
178
+ this.endpoint = endpoint
179
+ this.cluster = cluster
180
+ realDevice = true
181
+ } else {
182
+ const { id, endpoint, cluster } = Resource.parseClipId(body.uniqueid)
183
+ if (id != null && endpoint != null && cluster != null) {
184
+ // uniqueid for MultiCLIP has proper id, endpoint, cluster
185
+ this.id = gateway.id + '-' + id
186
+ this.subtype = endpoint + '-' + cluster
187
+ this.endpoint = endpoint
188
+ this.cluster = cluster
189
+ } else {
190
+ // ignore uniqueid for regular CLIP
191
+ this.subtype = rtype[0].toUpperCase() + rid
192
+ this.id = gateway.id + '-' + this.subtype
193
+ }
194
+ }
136
195
  this.zigbee = false
137
196
  } else {
138
197
  this.subtype = rtype[0].toUpperCase() + rid
@@ -147,8 +206,8 @@ class Resource {
147
206
  * For virtual devices, this is the _Manufacturer_ for the gateway.
148
207
  * @type {string}
149
208
  */
150
- this.manufacturer = this.zigbee
151
- ? body.manufacturername
209
+ this.manufacturer = realDevice
210
+ ? body.manufacturername.replace(/\//g, '')
152
211
  : gateway.values.manufacturer
153
212
 
154
213
  /** The associated HomeKit _Model_.
@@ -158,7 +217,7 @@ class Resource {
158
217
  * For virtual devices, this is the `type` in the resource body.
159
218
  * @type {string}
160
219
  */
161
- this.model = this.zigbee ? body.modelid : body.type
220
+ this.model = realDevice ? body.modelid : body.type
162
221
 
163
222
  /** The associated HomeKit _Firmware Version_.
164
223
  *
@@ -166,8 +225,8 @@ class Resource {
166
225
  * resource body.
167
226
  * For virtual devices, this is the _Firmware Version_ for the gateway.
168
227
  */
169
- this.firmware = this.zigbee
170
- ? body.swversion == null ? '0.0.0' : body.swversion
228
+ this.firmware = realDevice
229
+ ? Resource.parseSwversion(body.swversion)
171
230
  : gateway.values.software
172
231
 
173
232
  /** The name of the {@link DeconzService} subclass of the delegate of the
@@ -195,7 +254,7 @@ class Resource {
195
254
  if (this.rtype === 'groups') {
196
255
  return -1
197
256
  }
198
- if (this.rtype === 'lights' || this.isMultiClip()) {
257
+ if (this.rtype === 'lights') {
199
258
  return 0xFF - this.endpoint
200
259
  }
201
260
  return sensorsPrios.indexOf(this.serviceName)
@@ -230,6 +289,7 @@ class Resource {
230
289
  case 'On/Off light': return 'Light'
231
290
  case 'On/Off output': return 'Outlet'
232
291
  case 'On/Off plug-in unit': return 'Outlet'
292
+ case 'On/Off switch': return 'Switch'
233
293
  case 'Smart plug': return 'Outlet'
234
294
  case 'Configuration tool': return ''
235
295
  case 'Range extender': return ''
@@ -327,36 +387,20 @@ class Resource {
327
387
  }
328
388
  }
329
389
 
330
- /** Check whether resource is a CLIP sensor for Homebridge deCONZ.
331
- * For these, we use the following conventions:
332
- * - Use `uniqueid` to indicate device ID and subtype;
333
- * - Use `swversion` to indicate readonly and range for `Flag` and `Status`.
334
- */
335
- isDeconzClip () {
336
- return this.rtype === 'sensors' &&
337
- this.body.type.startsWith('CLIP') &&
338
- this.body.modelid === this.body.type && (
339
- this.body.manufacturername === 'homebridge-deconz' ||
340
- this.body.manufacturername === 'homebridge-hue'
341
- )
342
- }
343
-
344
- isMultiClip () {
345
- return this.isDeconzClip() && /^[0-9]+-[0-9]+$/.test(this.body.uniqueid)
346
- }
347
-
348
390
  /** Patch a resource corresponding to a `Flag` service.
349
391
  */
350
392
  patchFlag () {
351
- if (this.isDeconzClip() && this.body.swversion === '0') {
352
- this.capabilities.readonly = true
393
+ if (this.endpoint != null && this.cluster === '0006') {
394
+ if (this.body.swversion === '0') {
395
+ this.capabilities.readonly = true
396
+ }
353
397
  }
354
398
  }
355
399
 
356
400
  /** Patch a resource corresponding to a `Status` service.
357
401
  */
358
402
  patchStatus () {
359
- if (this.isDeconzClip()) {
403
+ if (this.endpoint != null && this.cluster === '0012') {
360
404
  const a = this.body.swversion.split(',')
361
405
  const min = parseInt(a[0])
362
406
  const max = parseInt(a[1])
@@ -1059,11 +1103,13 @@ class Resource {
1059
1103
  }
1060
1104
  }
1061
1105
 
1062
- /** Patch a resource corresponding to a `Temperature` service.
1106
+ /** Patch a resource corresponding to a `Motion` service.
1063
1107
  */
1064
- patchTemperature () {
1065
- if (this.manufacturer === 'Develco Products A/S' && this.model === 'SMSZB-120') {
1066
- this.id += '-T'
1108
+ patchMotion () {
1109
+ if (this.manufacturer === 'Aqara' && this.model === 'PS-S02D') {
1110
+ if (this.endpoint !== '01') {
1111
+ this.id += '-' + this.endpoint
1112
+ }
1067
1113
  }
1068
1114
  }
1069
1115
 
@@ -103,6 +103,14 @@ class Gateway extends AccessoryDelegate {
103
103
  } catch (error) { this.error(error) }
104
104
  })
105
105
 
106
+ this.addPropertyDelegate({
107
+ key: 'exposeSchedules',
108
+ value: false,
109
+ silent: true
110
+ }).on('didSet', async (value) => {
111
+ this.pollNext = true
112
+ })
113
+
106
114
  this.addPropertyDelegate({
107
115
  key: 'heartrate',
108
116
  value: 30,
@@ -149,6 +157,7 @@ class Gateway extends AccessoryDelegate {
149
157
  value: false,
150
158
  silent: true
151
159
  }).on('didSet', async (value) => {
160
+ this.service.values.search = value
152
161
  if (value) {
153
162
  try {
154
163
  await this.client.search()
@@ -229,6 +238,11 @@ class Gateway extends AccessoryDelegate {
229
238
  DeconzService.Button.LONG
230
239
  })
231
240
 
241
+ /** The service delegates for the Schedule services.
242
+ * @type {Object<string, DeconzService.Schedule>}
243
+ */
244
+ this.scheduleServicesByRid = {}
245
+
232
246
  this.createClient()
233
247
  this.createWsClient()
234
248
  this.heartbeatEnabled = true
@@ -342,7 +356,9 @@ class Gateway extends AccessoryDelegate {
342
356
 
343
357
  update (config) {
344
358
  this.values.software = config.swversion
345
- this.values.firmware = config.fwversion
359
+ this.values.firmware = parseInt(config.fwversion.slice(6, 8)) + '.' +
360
+ parseInt(config.fwversion.slice(2, 4), 16) + '.' +
361
+ parseInt(config.fwversion.slice(4, 6), 16)
346
362
  this.values.wsPort = config.websocketport
347
363
  this.service.update(config)
348
364
  if (this.checkApiKeys) {
@@ -637,7 +653,6 @@ class Gateway extends AccessoryDelegate {
637
653
  '%s: resetting after expose error: %s', id, this.exposeErrorById[id]
638
654
  )
639
655
  this.deleteAccessory(id)
640
- this.deleteService(id)
641
656
  }
642
657
  }
643
658
  }
@@ -672,7 +687,6 @@ class Gateway extends AccessoryDelegate {
672
687
  '%s: resetting after expose error: %s', id, this.exposeErrorById[id]
673
688
  )
674
689
  this.deleteAccessory(id)
675
- this.deleteService(id)
676
690
  }
677
691
 
678
692
  /** Assert that migration resourcelink exists and is valid.
@@ -774,6 +788,7 @@ class Gateway extends AccessoryDelegate {
774
788
  : {
775
789
  brightnessAdjustment: this.values.brightnessAdjustment * 100,
776
790
  expose: this.values.expose,
791
+ exposeSchedules: this.values.exposeSchedules,
777
792
  heartrate: this.values.heartrate,
778
793
  logLevel: this.values.logLevel,
779
794
  periodicEvents: this.values.periodicEvents,
@@ -832,6 +847,7 @@ class Gateway extends AccessoryDelegate {
832
847
  if (this.values.apiKey != null) {
833
848
  optionParser
834
849
  .intKey('brightnessAdjustment', 10, 100)
850
+ .boolKey('exposeSchedules')
835
851
  .intKey('heartrate', 1, 60)
836
852
  .boolKey('periodicEvents')
837
853
  .boolKey('restart')
@@ -848,6 +864,7 @@ class Gateway extends AccessoryDelegate {
848
864
  responseBody[key] = this.values[key]
849
865
  break
850
866
  case 'expose':
867
+ case 'exposeSchedules':
851
868
  case 'heartrate':
852
869
  case 'logLevel':
853
870
  case 'periodicEvents':
@@ -921,6 +938,10 @@ class Gateway extends AccessoryDelegate {
921
938
  await this.wsClient.close()
922
939
  return
923
940
  }
941
+ if (config.bridgeid === '0000000000000000' || config.fwversion === '0x00000000') {
942
+ this.warn('deCONZ not ready')
943
+ return
944
+ }
924
945
  this.context.fullState.config = config
925
946
  this.context.fullState.lights = await this.client.get('/lights')
926
947
  this.context.fullState.sensors = await this.client.get('/sensors')
@@ -928,7 +949,7 @@ class Gateway extends AccessoryDelegate {
928
949
  this.context.fullState.groups = await this.client.get('/groups')
929
950
  this.context.fullState.groups[0] = await this.client.get('/groups/0')
930
951
  }
931
- if (this.nDevicesByRtype.schedules) {
952
+ if (this.values.exposeSchedules) {
932
953
  this.context.fullState.schedules = await this.client.get('/schedules')
933
954
  }
934
955
  }
@@ -964,16 +985,10 @@ class Gateway extends AccessoryDelegate {
964
985
  * {@link DeconzAccessory.Gateway#deleteAccessory deleteAccessory()} for
965
986
  * stale accessories, corresponding to devices that have been deleted from
966
987
  * the gateway, blacklisted, or excluded by device primary resource type.
967
- * 3. Analyse (pre-existing) _Device Settings_ services, calling
968
- * {@link DeconzAccessory.Gateway#deleteService deleteService()}
969
- * for stale services, corresponding to devices that have been deleted from
970
- * the gateway, un-blacklisted, or excluded by device primary resource type.
971
- * 4. Analysing supported devices with enabled device primary resource types,
972
- * calling {@link DeconzAccessory.Gateway#addAccessory addAccessory()} and
973
- * {@link DeconzAccessory.Gateway#deleteService deleteService()} for new
988
+ * 3. Analysing supported devices with enabled device primary resource types,
989
+ * calling {@link DeconzAccessory.Gateway#addAccessory addAccessory()} for new
974
990
  * _Device_ accessories, corresponding to devices added to the gateway,
975
991
  * un-blacklisted, or included by device primary resource type, and calling
976
- * {@link DeconzAccessory.Gateway#addService addService()} and
977
992
  * {@link DeconzAccessory.Gateway#deleteAccessory deleteAccessory()} for
978
993
  * accessories, corresponding to devices have been blacklisted.
979
994
  * @param {Object} fullState - The gateway full state, as returned by
@@ -1072,7 +1087,6 @@ class Gateway extends AccessoryDelegate {
1072
1087
  ) {
1073
1088
  delete this.context.settingsById[id]
1074
1089
  this.deleteAccessory(id)
1075
- // this.deleteService(id)
1076
1090
  changed = true
1077
1091
  } else {
1078
1092
  /** Emitted when the gateway has been polled.
@@ -1119,6 +1133,24 @@ class Gateway extends AccessoryDelegate {
1119
1133
  )
1120
1134
  }
1121
1135
 
1136
+ this.vdebug('analysing schedules...')
1137
+ if (this.values.exposeSchedules) {
1138
+ for (const rid in fullState.schedules) {
1139
+ if (this.scheduleServicesByRid[rid] == null) {
1140
+ this.scheduleServicesByRid[rid] = new DeconzService.Schedule(
1141
+ this, rid, fullState.schedules[rid]
1142
+ )
1143
+ }
1144
+ this.scheduleServicesByRid[rid].update(fullState.schedules[rid])
1145
+ }
1146
+ }
1147
+ for (const rid in this.scheduleServicesByRid) {
1148
+ if (!this.values.exposeSchedules || fullState.schedules[rid] == null) {
1149
+ this.scheduleServicesByRid[rid].destroy()
1150
+ delete this.scheduleServicesByRid[rid]
1151
+ }
1152
+ }
1153
+
1122
1154
  if (changed) {
1123
1155
  await this.updateMigration()
1124
1156
  this.identify()
@@ -5,6 +5,7 @@
5
5
 
6
6
  'use strict'
7
7
 
8
+ const { ServiceDelegate } = require('homebridge-lib')
8
9
  const DeconzAccessory = require('../DeconzAccessory')
9
10
 
10
11
  /** Delegate class for a HomeKit accessory, corresponding to a light device
@@ -32,6 +33,23 @@ class WarningDevice extends DeconzAccessory {
32
33
  this.createService(resource)
33
34
  }
34
35
 
36
+ const params = {}
37
+ if (this.servicesByServiceName.WarningDevice?.length === 1) {
38
+ params.switchOnDelegate = this.service.characteristicDelegate('on')
39
+ params.lastSwitchOnDelegate = this.service.addCharacteristicDelegate({
40
+ key: 'lastActivation',
41
+ Characteristic: this.Characteristics.eve.LastActivation,
42
+ silent: true
43
+ })
44
+ }
45
+ if (this.servicesByServiceName.Temperature?.length === 1) {
46
+ const service = this.servicesByServiceName.Temperature[0]
47
+ params.temperatureDelegate = service.characteristicDelegate('temperature')
48
+ }
49
+ if (Object.keys(params).length > 0) {
50
+ this.historyService = new ServiceDelegate.History(this, params)
51
+ }
52
+
35
53
  setImmediate(() => {
36
54
  this.debug('initialised')
37
55
  this.emit('initialised')
@@ -31,6 +31,16 @@ class Gateway extends ServiceDelegate {
31
31
  silent: true
32
32
  })
33
33
 
34
+ this.addCharacteristicDelegate({
35
+ key: 'search',
36
+ Characteristic: this.Characteristics.my.Search,
37
+ value: false
38
+ }).on('didSet', (value, fromHomeKit) => {
39
+ if (fromHomeKit) {
40
+ this.gateway.values.search = value
41
+ }
42
+ })
43
+
34
44
  this.addCharacteristicDelegate({
35
45
  key: 'transitionTime',
36
46
  Characteristic: this.Characteristics.my.TransitionTime,
@@ -0,0 +1,69 @@
1
+ // homebridge-deconz/lib/DeconzService/Schedule.js
2
+ // Copyright© 2022-2023 Erik Baauw. All rights reserved.
3
+ //
4
+ // Homebridge plugin for deCONZ.
5
+
6
+ 'use strict'
7
+
8
+ const Deconz = require('../Deconz')
9
+ const { ServiceDelegate } = require('homebridge-lib')
10
+
11
+ const { HttpError } = Deconz.ApiClient
12
+
13
+ /**
14
+ * @memberof DeconzService
15
+ */
16
+ class Schedule extends ServiceDelegate {
17
+ constructor (accessory, rid, body) {
18
+ super(accessory, {
19
+ id: accessory.gateway.id + '-T' + rid,
20
+ name: body.name,
21
+ Service: accessory.Services.my.Resource,
22
+ subtype: 'T' + rid,
23
+ exposeConfiguredName: true
24
+ })
25
+ this.id = accessory.gateway.id + '-T' + rid
26
+ this.gateway = accessory.gateway
27
+ this.accessory = accessory
28
+ this.client = accessory.client
29
+ this.rtype = 'schedules'
30
+ this.rid = rid
31
+ this.rpath = '/' + this.rtype + '/' + this.rid
32
+
33
+ this.addCharacteristicDelegate({
34
+ key: 'enabled',
35
+ Characteristic: this.Characteristics.my.Enabled
36
+ }).on('didSet', async (value, fromHomeKit) => {
37
+ await this.put({ status: value ? 'enabled' : 'disabled' })
38
+ this.values.statusActive = value
39
+ })
40
+
41
+ this.addCharacteristicDelegate({
42
+ key: 'statusActive',
43
+ Characteristic: this.Characteristics.hap.StatusActive
44
+ })
45
+
46
+ // this.addCharacteristicDelegate({
47
+ // key: 'index',
48
+ // Characteristic: this.Characteristics.hap.ServiceLabelIndex,
49
+ // value: rid
50
+ // })
51
+ }
52
+
53
+ update (body) {
54
+ this.values.enabled = body.status === 'enabled'
55
+ this.values.statusActive = this.values.enabled
56
+ }
57
+
58
+ async put (body) {
59
+ try {
60
+ await this.client.put(this.rpath, body)
61
+ } catch (error) {
62
+ if (!(error instanceof HttpError)) {
63
+ this.warn(error)
64
+ }
65
+ }
66
+ }
67
+ }
68
+
69
+ module.exports = Schedule
@@ -16,10 +16,15 @@ class Smoke extends DeconzService.SensorsResource {
16
16
  super(accessory, resource, params)
17
17
 
18
18
  this.addCharacteristicDelegate({
19
- key: 'fire',
19
+ key: 'smoke',
20
20
  Characteristic: this.Characteristics.hap.SmokeDetected
21
21
  })
22
22
 
23
+ this.addCharacteristicDelegate({
24
+ key: 'deviceStatus',
25
+ Characteristic: this.Characteristics.eve.ElgatoDeviceStatus
26
+ })
27
+
23
28
  super.addCharacteristicDelegates(params)
24
29
 
25
30
  this.update(resource.body, resource.rpath)
@@ -27,9 +32,17 @@ class Smoke extends DeconzService.SensorsResource {
27
32
 
28
33
  updateState (state) {
29
34
  if (state.fire != null || state.test != null) {
30
- this.values.fire = state.fire || state.test
35
+ this.values.smoke = state.fire
31
36
  ? this.Characteristics.hap.SmokeDetected.SMOKE_DETECTED
32
37
  : this.Characteristics.hap.SmokeDetected.SMOKE_NOT_DETECTED
38
+ let status = 0
39
+ if (state.fire) {
40
+ status |= this.Characteristics.eve.ElgatoDeviceStatus.SMOKE_DETECTED
41
+ }
42
+ if (state.test) {
43
+ status |= this.Characteristics.eve.ElgatoDeviceStatus.ALARM_TEST_ACTIVE
44
+ }
45
+ this.values.deviceStatus = status
33
46
  }
34
47
  super.updateState(state)
35
48
  }
@@ -9,7 +9,7 @@ const DeconzService = require('../DeconzService')
9
9
 
10
10
  class WarningDevice extends DeconzService.LightsResource {
11
11
  constructor (accessory, resource, params = {}) {
12
- params.Service = accessory.Services.hap.Outlet
12
+ params.Service = accessory.Services.hap.Switch
13
13
  super(accessory, resource, params)
14
14
 
15
15
  this.addCharacteristicDelegate({
@@ -35,6 +35,7 @@ class DeconzService extends ServiceDelegate {
35
35
  static get Motion () { return require('./Motion') }
36
36
  static get Outlet () { return require('./Outlet') }
37
37
  static get Power () { return require('./Power') }
38
+ static get Schedule () { return require('./Schedule') }
38
39
  static get SensorsResource () { return require('./SensorsResource') }
39
40
  static get Status () { return require('./Status') }
40
41
  static get Smoke () { return require('./Smoke') }
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "displayName": "Homebridge deCONZ",
5
5
  "author": "Erik Baauw",
6
6
  "license": "Apache-2.0",
7
- "version": "0.1.15",
7
+ "version": "0.1.17",
8
8
  "keywords": [
9
9
  "homebridge-plugin",
10
10
  "homekit",
@@ -26,7 +26,7 @@
26
26
  "node": "^18.16.0"
27
27
  },
28
28
  "dependencies": {
29
- "homebridge-lib": "~6.3.16",
29
+ "homebridge-lib": "~6.3.17",
30
30
  "ws": "^8.13.0",
31
31
  "xml2js": "~0.5.0"
32
32
  },