homebridge-deconz 0.0.6 → 0.0.11

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 (58) hide show
  1. package/README.md +8 -0
  2. package/cli/deconz.js +980 -0
  3. package/config.schema.json +1 -1
  4. package/lib/Client/ApiError.js +42 -0
  5. package/lib/{DeconzClient.js → Deconz/ApiClient.js} +82 -158
  6. package/lib/Deconz/ApiError.js +42 -0
  7. package/lib/Deconz/ApiResponse.js +54 -0
  8. package/lib/Deconz/Device.js +100 -0
  9. package/lib/{DeconzDiscovery.js → Deconz/Discovery.js} +4 -3
  10. package/lib/Deconz/Resource.js +1206 -0
  11. package/lib/{DeconzWsClient.js → Deconz/WsClient.js} +59 -44
  12. package/lib/Deconz/index.js +21 -0
  13. package/lib/DeconzAccessory/Contact.js +54 -0
  14. package/lib/DeconzAccessory/Gateway.js +316 -374
  15. package/lib/DeconzAccessory/Light.js +72 -0
  16. package/lib/DeconzAccessory/Motion.js +51 -0
  17. package/lib/DeconzAccessory/Sensor.js +35 -0
  18. package/lib/DeconzAccessory/Temperature.js +63 -0
  19. package/lib/DeconzAccessory/Thermostat.js +50 -0
  20. package/lib/DeconzAccessory/WarningDevice.js +56 -0
  21. package/lib/DeconzAccessory/WindowCovering.js +47 -0
  22. package/lib/DeconzAccessory/index.js +216 -0
  23. package/lib/DeconzPlatform.js +8 -3
  24. package/lib/DeconzService/AirPressure.js +43 -0
  25. package/lib/DeconzService/AirQuality.js +20 -10
  26. package/lib/DeconzService/Alarm.js +16 -9
  27. package/lib/DeconzService/Battery.js +43 -0
  28. package/lib/DeconzService/Button.js +12 -2
  29. package/lib/DeconzService/CarbonMonoxide.js +38 -0
  30. package/lib/DeconzService/Consumption.js +65 -0
  31. package/lib/DeconzService/Contact.js +60 -0
  32. package/lib/DeconzService/Daylight.js +132 -0
  33. package/lib/DeconzService/DeviceSettings.js +13 -5
  34. package/lib/DeconzService/Flag.js +52 -0
  35. package/lib/DeconzService/GatewaySettings.js +8 -58
  36. package/lib/DeconzService/Humidity.js +37 -0
  37. package/lib/DeconzService/Leak.js +38 -0
  38. package/lib/DeconzService/Light.js +376 -0
  39. package/lib/DeconzService/LightLevel.js +54 -0
  40. package/lib/DeconzService/LightsResource.js +112 -0
  41. package/lib/DeconzService/Motion.js +101 -0
  42. package/lib/DeconzService/Outlet.js +76 -0
  43. package/lib/DeconzService/Power.js +83 -0
  44. package/lib/DeconzService/SensorsResource.js +96 -0
  45. package/lib/DeconzService/Smoke.js +38 -0
  46. package/lib/DeconzService/Status.js +53 -0
  47. package/lib/DeconzService/Switch.js +93 -0
  48. package/lib/DeconzService/Temperature.js +63 -0
  49. package/lib/DeconzService/Thermostat.js +175 -0
  50. package/lib/DeconzService/WarningDevice.js +68 -0
  51. package/lib/DeconzService/WindowCovering.js +139 -0
  52. package/lib/DeconzService/index.js +94 -0
  53. package/package.json +7 -4
  54. package/lib/DeconzAccessory/Device.js +0 -91
  55. package/lib/DeconzAccessory.js +0 -16
  56. package/lib/DeconzDevice.js +0 -245
  57. package/lib/DeconzService/Sensor.js +0 -58
  58. package/lib/DeconzService.js +0 -43
@@ -6,17 +6,11 @@
6
6
  'use strict'
7
7
 
8
8
  const homebridgeLib = require('homebridge-lib')
9
- const util = require('util')
10
9
 
11
- const DeconzClient = require('../DeconzClient')
12
- const DeconzWsClient = require('../DeconzWsClient')
13
- const DeconzDevice = require('../DeconzDevice')
10
+ const Deconz = require('../Deconz')
14
11
  const DeconzAccessory = require('../DeconzAccessory')
15
12
  const DeconzService = require('../DeconzService')
16
13
 
17
- const { toInt, toObject, toString } = homebridgeLib.OptionParser
18
- const { parseUniqueid } = DeconzClient
19
-
20
14
  const rtypes = ['lights', 'sensors', 'groups']
21
15
 
22
16
  const periodicEvents = [
@@ -26,7 +20,6 @@ const periodicEvents = [
26
20
  ]
27
21
 
28
22
  /** Delegate class for a deCONZ gateway.
29
- * @class
30
23
  * @extends AccessoryDelegate
31
24
  * @memberof DeconzAccessory
32
25
  */
@@ -46,7 +39,7 @@ class Gateway extends homebridgeLib.AccessoryDelegate {
46
39
  model: params.config.modelid + ' / ' + params.config.devicename,
47
40
  firmware: '0.0.0',
48
41
  software: params.config.swversion,
49
- category: platform.Accessory.Categories.Bridge
42
+ category: platform.Accessory.Categories.BRIDGE
50
43
  })
51
44
 
52
45
  this.gateway = this
@@ -60,31 +53,75 @@ class Gateway extends homebridgeLib.AccessoryDelegate {
60
53
  * devices.
61
54
  * @property {Object} fullState - The gateway's full state, from the
62
55
  * last time the gateway was polled.
63
- * @property {string} host - Gateway hostname or IP address and port.
64
56
  */
65
57
  this.context // eslint-disable-line no-unused-expressions
66
-
67
58
  this.context.host = params.host
68
59
  this.context.config = params.config
69
60
  if (this.context.blacklist == null) {
70
61
  this.context.blacklist = {}
71
62
  }
63
+ if (this.context.fullState != null) {
64
+ this.analyseFullState(this.context.fullState, { analyseOnly: true })
65
+ }
66
+
67
+ this.addPropertyDelegate({
68
+ key: 'apiKey',
69
+ silent: true
70
+ }).on('didSet', (value) => {
71
+ this.client.apiKey = value
72
+ })
73
+
74
+ this.addPropertyDelegate({
75
+ key: 'host',
76
+ value: params.host,
77
+ silent: true
78
+ }).on('didSet', (value) => {
79
+ if (this.client != null) {
80
+ this.client.host = value
81
+ }
82
+ if (this.wsClient != null) {
83
+ this.wsClient.host = this.values.host.split(':')[0] +
84
+ ':' + this.values.wsPort
85
+ }
86
+ })
87
+ this.values.host = params.host
88
+
89
+ this.addPropertyDelegate({
90
+ key: 'rtypes',
91
+ value: [],
92
+ silent: true
93
+ }).on('didSet', async () => {
94
+ this.pollNext = true
95
+ })
96
+
97
+ this.addPropertyDelegate({
98
+ key: 'wsPort',
99
+ value: 443,
100
+ silent: true
101
+ }).on('didSet', (value) => {
102
+ if (this.wsClient != null) {
103
+ this.wsClient.host = this.values.host.split(':')[0] +
104
+ ':' + this.values.wsPort
105
+ }
106
+ })
72
107
 
73
108
  this.log(
74
109
  '%s %s gateway v%s', this.values.manufacturer, this.values.model,
75
110
  this.values.software
76
111
  )
77
112
 
78
- /** Map of Accessory delegates by id for the gayeway.
113
+ /** Map of Accessory delegates by id for the gateway.
79
114
  * @type {Object<string, DeconzAccessory.Device>}
80
115
  */
81
116
  this.accessoryById = {}
82
117
 
83
- /** Map of Accessory delegates by rpath for the gayeway.
118
+ /** Map of Accessory delegates by rpath for the gateway.
84
119
  * @type {Object<string, DeconzAccessory.Device>}
85
120
  */
86
121
  this.accessoryByRpath = {}
87
122
 
123
+ this.defaultTransitionTime = 0.4
124
+
88
125
  /** Map of errors by device ID trying to expose the corresponding accessory.
89
126
  * @type {Object<string, Error>}
90
127
  */
@@ -95,11 +132,6 @@ class Gateway extends homebridgeLib.AccessoryDelegate {
95
132
  */
96
133
  this.serviceById = {}
97
134
 
98
- /** Map of unsupported devices.
99
- * @type {Object.<string, boolean>}
100
- */
101
- this.unsupported = {}
102
-
103
135
  /** The service delegate for the Gateway Settings settings.
104
136
  * @type {DeconzService.GatewaySettings}
105
137
  */
@@ -108,6 +140,7 @@ class Gateway extends homebridgeLib.AccessoryDelegate {
108
140
  primaryService: true,
109
141
  host: params.host
110
142
  })
143
+ this.manageLogLevel(this.service.characteristicDelegate('logLevel'))
111
144
 
112
145
  /** The service delegate for the Stateless Programmable Switch service.
113
146
  * @type {DeconzService.Button}
@@ -129,6 +162,18 @@ class Gateway extends homebridgeLib.AccessoryDelegate {
129
162
  .on('shutdown', this.shutdown)
130
163
  }
131
164
 
165
+ get transitionTime () { return this.service.values.transitionTime }
166
+
167
+ async resetTransitionTime () {
168
+ if (this.resetting) {
169
+ return
170
+ }
171
+ this.resetting = true
172
+ await homebridgeLib.timeout(this.platform.config.waitTimeUpdate)
173
+ this.service.values.transitionTime = this.defaultTransitionTime
174
+ this.resetting = false
175
+ }
176
+
132
177
  /** Log debug messages.
133
178
  */
134
179
  identify () {
@@ -156,7 +201,8 @@ class Gateway extends homebridgeLib.AccessoryDelegate {
156
201
  )
157
202
  const exposeErrors = Object.keys(this.exposeErrorById).sort()
158
203
  this.vdebug(
159
- '%d accessories with expose errors', exposeErrors.length, exposeErrors
204
+ '%d accessories with expose errors: %j', exposeErrors.length,
205
+ exposeErrors
160
206
  )
161
207
  const blacklist = Object.keys(this.context.blacklist).sort()
162
208
  this.vdebug(
@@ -170,9 +216,14 @@ class Gateway extends homebridgeLib.AccessoryDelegate {
170
216
  try {
171
217
  this.debug('initialising...')
172
218
  this.initialBeat = beat
173
- if (this.context.fullState != null) {
174
- this.analyseFullState(this.context.fullState)
175
- }
219
+ // if (this.context.fullState != null) {
220
+ // this.analyseFullState(this.context.fullState, { logUnsupported: true })
221
+ // for (const id of this.restoredAccessories) {
222
+ // try {
223
+ // this.addAccessory(id)
224
+ // } catch (error) { this.error(error) }
225
+ // }
226
+ // }
176
227
  await this.connect()
177
228
  this.initialised = true
178
229
  this.debug('initialised')
@@ -186,7 +237,7 @@ class Gateway extends homebridgeLib.AccessoryDelegate {
186
237
  * GET `/config` (from {@link DeconzDiscovery#config config()}.
187
238
  */
188
239
  found (host, config) {
189
- this.service.values.host = host
240
+ this.values.host = host
190
241
  this.context.config = config
191
242
  this.values.software = config.swversion
192
243
  }
@@ -208,10 +259,7 @@ class Gateway extends homebridgeLib.AccessoryDelegate {
208
259
  }
209
260
  }
210
261
  }
211
- if (beat - this.pollBeat >= this.service.values.heartrate) {
212
- this.pollNext = true
213
- }
214
- if (this.pollNext) {
262
+ if (beat - this.pollBeat >= this.service.values.heartrate || this.pollNext) {
215
263
  this.pollBeat = beat
216
264
  await this.poll()
217
265
  }
@@ -221,11 +269,12 @@ class Gateway extends homebridgeLib.AccessoryDelegate {
221
269
  update (config) {
222
270
  this.values.software = config.swversion
223
271
  this.values.firmware = config.fwversion
272
+ this.values.wsPort = config.websocketport
224
273
  this.service.update(config)
225
274
  if (this.checkApiKeys) {
226
- const myEntry = config.whitelist[this.service.values.username]
275
+ const myEntry = config.whitelist[this.values.apiKey]
227
276
  for (const key in config.whitelist) {
228
- if (key !== this.service.values.username) {
277
+ if (key !== this.values.apiKey) {
229
278
  const entry = config.whitelist[key]
230
279
  if (entry.name === myEntry.name) {
231
280
  this.warn('%s: potentially stale api key: %j', key, entry)
@@ -242,10 +291,10 @@ class Gateway extends homebridgeLib.AccessoryDelegate {
242
291
  /** REST API client for the gateway.
243
292
  * @type {DeconzClient}
244
293
  */
245
- this.client = new DeconzClient({
246
- host: this.service.values.host,
294
+ this.client = new Deconz.ApiClient({
295
+ apiKey: this.values.apiKey,
247
296
  config: this.context.config,
248
- username: this.service.values.username,
297
+ host: this.values.host,
249
298
  maxSockets: this.platform.config.parallelRequests,
250
299
  timeout: this.platform.config.timeout,
251
300
  waitTimePut: this.platform.config.waitTimePut,
@@ -298,8 +347,8 @@ class Gateway extends homebridgeLib.AccessoryDelegate {
298
347
  /** Client for gateway web socket notifications.
299
348
  * @type {DeconzWsClient}
300
349
  */
301
- this.wsClient = new DeconzWsClient({
302
- host: this.service.values.wsHost,
350
+ this.wsClient = new Deconz.WsClient({
351
+ host: this.values.host.split(':')[0] + ':' + this.values.wsPort,
303
352
  retryTime: 15
304
353
  })
305
354
  this.wsClient
@@ -309,19 +358,14 @@ class Gateway extends homebridgeLib.AccessoryDelegate {
309
358
  .on('listening', (url) => {
310
359
  this.log('websocket connected to %s', url)
311
360
  })
312
- .on('changed', (rpath, obj) => {
313
- this.vdebug('%s: event: %j', rpath, obj)
361
+ .on('changed', (rtype, rid, body) => {
314
362
  try {
315
- const a = rpath.split('/')
316
- rpath = a.slice(0, 3).join('/')
363
+ const rpath = '/' + rtype + '/' + rid
364
+ this.vdebug('%s: changed: %j', rpath, body)
317
365
  const accessory = this.accessoryByRpath[rpath]
318
366
  if (accessory != null) {
319
- let body = obj
320
- if (a[3] != null) {
321
- body = {}
322
- body[a[3]] = obj
323
- }
324
- /** Emitted when resource has been polled.
367
+ /** Emitted when a change notificatoin for a resource has been
368
+ * received over the web socket.
325
369
  * @event DeconzAccessory.Device#changed
326
370
  * @param {string} rpath - The resource path.
327
371
  * @param {Object} body - The resource body.
@@ -332,6 +376,18 @@ class Gateway extends homebridgeLib.AccessoryDelegate {
332
376
  this.warn('websocket error: %s', error)
333
377
  }
334
378
  })
379
+ .on('added', (rtype, rid, body) => {
380
+ this.vdebug('/%s/%d: added: %j', rtype, rid, body)
381
+ if (this.service.values[rtype]) {
382
+ this.pollNext = true
383
+ }
384
+ })
385
+ .on('deleted', (rtype, rid) => {
386
+ this.vdebug('/%s/%d: deleted', rtype, rid)
387
+ if (this.service.values[rtype]) {
388
+ this.pollNext = true
389
+ }
390
+ })
335
391
  .on('closed', (url, retryTime) => {
336
392
  if (retryTime > 0) {
337
393
  this.log(
@@ -351,24 +407,24 @@ class Gateway extends homebridgeLib.AccessoryDelegate {
351
407
  */
352
408
  async connect (retry = 0) {
353
409
  if (!this.service.values.expose) {
354
- this.warn('unlock gayeway and set Expose to obtain an API key')
410
+ this.warn('unlock gateway and set Expose to obtain an API key')
355
411
  return
356
412
  }
357
413
  try {
358
- if (this.service.values.username == null) {
359
- this.service.values.username =
360
- await this.client.createUser('homebridge-deconz')
414
+ if (this.values.apiKey == null) {
415
+ this.values.apiKey =
416
+ await this.client.getApiKey('homebridge-deconz')
361
417
  }
362
418
  this.wsClient.listen()
363
419
  this.checkApiKeys = true
364
420
  for (const id in this.exposeErrorById) {
365
421
  this.resetExposeError(id)
366
422
  }
423
+ this.context.fullState = null
367
424
  this.pollNext = true
368
425
  } catch (error) {
369
426
  if (
370
- error instanceof DeconzClient.DeconzError && error.type === 101 &&
371
- retry < 8
427
+ error instanceof Deconz.ApiError && error.type === 101 && retry < 8
372
428
  ) {
373
429
  this.log('unlock gateway to obtain API key - retrying in 15s')
374
430
  await homebridgeLib.timeout(15000)
@@ -387,14 +443,14 @@ class Gateway extends homebridgeLib.AccessoryDelegate {
387
443
  * the gateway.
388
444
  */
389
445
  async reset () {
390
- if (this.service.values.username == null) {
446
+ if (this.values.apiKey == null) {
391
447
  return
392
448
  }
393
449
  try {
394
450
  try {
395
- await this.client.deleteUser()
451
+ await this.client.deleteApiKey()
396
452
  } catch (error) {}
397
- this.service.values.username = null
453
+ this.values.apiKey = null
398
454
  await this.wsClient.close()
399
455
  for (const id in this.accessoryById) {
400
456
  if (id !== this.id) {
@@ -406,12 +462,13 @@ class Gateway extends homebridgeLib.AccessoryDelegate {
406
462
  }
407
463
  this.exposeErrors = {}
408
464
  this.context.blacklist = {}
409
- this.context.fullState = {}
465
+ this.context.fullState = null
410
466
  this.service.values.lights = false
411
467
  this.service.values.sensors = false
412
468
  this.service.values.groups = false
413
469
  this.service.values.schedules = false
414
- this.service.values.rtypes = []
470
+ this.service.values.logLevel = 2
471
+ this.values.rtypes = []
415
472
  } catch (error) { this.error(error) }
416
473
  }
417
474
 
@@ -451,32 +508,15 @@ class Gateway extends homebridgeLib.AccessoryDelegate {
451
508
  if (this.deviceById[id] == null) {
452
509
  throw new RangeError(`${id}: unknown device ID`)
453
510
  }
454
- if (this.unsupported[id] != null) {
455
- return
456
- }
457
511
  if (this.accessoryById[id] == null) {
458
512
  const device = this.deviceById[id]
459
- this.debug(
460
- '%s: new device, %d resources', id, device.rpaths.length
461
- )
462
513
  delete this.exposeErrorById[id]
463
- const { body, category, rtype } = device.resource
514
+ const { body } = device.resource
515
+ this.log('%s: add accessory', body.name)
464
516
  let { serviceName } = device.resource
465
- if (category == null || serviceName == null) {
466
- let message
467
- if (category == null) {
468
- message = util.format('%s: unknown %s type', body.type, rtype)
469
- this.warn('%s: %s', body.name, message)
470
- } else {
471
- message = util.format('%s: unsupported %s type', body.type, rtype)
472
- this.debug('%s: %s', body.name, message)
473
- }
474
- this.unsupported[id] = message
475
- return
476
- }
477
517
  if (DeconzAccessory[serviceName] == null) {
478
- this.warn('%s: %s: not yet supported %s type', body.name, body.type, rtype)
479
- serviceName = 'Device'
518
+ // this.warn('%s: %s: not yet supported %s type', body.name, body.type, rtype)
519
+ serviceName = 'Sensor'
480
520
  }
481
521
  const accessory = new DeconzAccessory[serviceName](this, device)
482
522
  this.accessoryById[id] = accessory
@@ -497,6 +537,7 @@ class Gateway extends homebridgeLib.AccessoryDelegate {
497
537
  throw new RangeError(`${id}: gateway ID`)
498
538
  }
499
539
  if (this.accessoryById[id] != null) {
540
+ this.log('%s: delete accessory', this.accessoryById[id].name)
500
541
  this.monitorResources(this.accessoryById[id], false)
501
542
  this.accessoryById[id].destroy()
502
543
  delete this.accessoryById[id]
@@ -537,6 +578,7 @@ class Gateway extends homebridgeLib.AccessoryDelegate {
537
578
  /** Reset expose error for device.
538
579
  *
539
580
  * Remove the un-exposed accessory, so it will be re-created on next poll.
581
+ * @params {string} id - The device ID.
540
582
  */
541
583
  resetExposeError (id) {
542
584
  this.log(
@@ -546,7 +588,7 @@ class Gateway extends homebridgeLib.AccessoryDelegate {
546
588
  this.deleteService(id)
547
589
  }
548
590
 
549
- /** Add a service to the gateway accessory to un-blacklist the device.
591
+ /** Add a service to the gateway accessory to un-blacklist a device.
550
592
  * @params {string} id - The device ID.
551
593
  * @return {DeconzService.DeviceSettings} - The service delegate.
552
594
  */
@@ -558,6 +600,9 @@ class Gateway extends homebridgeLib.AccessoryDelegate {
558
600
  throw new RangeError(`${id}: unknown device ID`)
559
601
  }
560
602
  if (this.serviceById[id] == null) {
603
+ if (Object.keys(this.serviceById).length >= 97) {
604
+ return null
605
+ }
561
606
  const { resource, rpaths } = this.deviceById[id]
562
607
  const { body } = resource
563
608
  const service = new DeconzService.DeviceSettings(this, {
@@ -571,12 +616,12 @@ class Gateway extends homebridgeLib.AccessoryDelegate {
571
616
  return this.serviceById[id]
572
617
  }
573
618
 
574
- /** Delete the service on the gateway accessory to un-blacklist the device.
619
+ /** Delete the service on the gateway accessory to un-blacklist a device.
575
620
  * @params {string} id - The device ID.
576
621
  */
577
622
  deleteService (id) {
578
623
  if (id === this.id) {
579
- return
624
+ throw new RangeError(`${id}: gateway ID`)
580
625
  }
581
626
  if (this.serviceById[id] != null) {
582
627
  this.serviceById[id].destroy()
@@ -588,39 +633,39 @@ class Gateway extends homebridgeLib.AccessoryDelegate {
588
633
 
589
634
  /** Poll the gateway.
590
635
  *
591
- * Periodically read and analyse the gateway full state.<br>
592
- * The `/groups`, `/lights`, and `/sensors` resources are analysed to build
593
- * a map of zigbee and virtual devices exposed by the gateway, while:
594
- * - Creating new accessories for new devices;
595
- * - Updating existing accessories for existing devices;
596
- * - Deleting existing accessories for deleted devices.
636
+ * Periodically get the gateway full state and call
637
+ * {@link DeconzAccessory.Gateway#analyseFullState()}.<br>
597
638
  */
598
639
  async poll () {
599
- if (this.polling || this.service.values.username == null) {
640
+ if (this.polling || this.values.apiKey == null) {
600
641
  return
601
642
  }
602
643
  try {
603
644
  this.polling = true
604
645
  this.vdebug('%spolling...', this.pollNext ? 'priority ' : '')
605
- const config = await this.client.get('/config')
606
- if (config.bridgeid === this.id && config.UTC == null) {
607
- this.service.values.expose = false
608
- this.service.values.username = null
609
- await this.wsClient.close()
610
- return
611
- }
612
- this.context.fullState = { config: config }
613
- this.update(this.context.fullState.config)
614
- if (this.service.values.lights || this.service.values.sensors) {
615
- this.context.fullState.lights = await this.client.get('/lights')
616
- this.context.fullState.sensors = await this.client.get('/sensors')
617
- }
618
- if (this.service.values.groups) {
619
- this.context.fullState.groups = await this.client.get('/groups')
646
+ if (this.context.fullState == null) {
647
+ this.context.fullState = await this.client.get('/')
620
648
  this.context.fullState.groups[0] = await this.client.get('/groups/0')
621
- }
622
- if (this.service.values.schedules) {
623
- this.context.fullState.schedules = await this.client.get('/schedules')
649
+ } else {
650
+ const config = await this.client.get('/config')
651
+ if (config.bridgeid === this.id && config.UTC == null) {
652
+ this.service.values.expose = false
653
+ this.values.apiKey = null
654
+ await this.wsClient.close()
655
+ return
656
+ }
657
+ this.context.fullState.config = config
658
+ if (this.service.values.lights || this.service.values.sensors) {
659
+ this.context.fullState.lights = await this.client.get('/lights')
660
+ this.context.fullState.sensors = await this.client.get('/sensors')
661
+ }
662
+ if (this.service.values.groups) {
663
+ this.context.fullState.groups = await this.client.get('/groups')
664
+ this.context.fullState.groups[0] = await this.client.get('/groups/0')
665
+ }
666
+ if (this.service.values.schedules) {
667
+ this.context.fullState.schedules = await this.client.get('/schedules')
668
+ }
624
669
  }
625
670
  this.analyseFullState(this.context.fullState)
626
671
  } catch (error) {
@@ -632,23 +677,71 @@ class Gateway extends homebridgeLib.AccessoryDelegate {
632
677
  }
633
678
  }
634
679
 
635
- analyseFullState (fullState) {
636
- /** Devices by device ID.
637
- * @type {Object<string, DeconzDevice>}
680
+ /** Analyse the peristed full state of the gateway,
681
+ * adding, re-configuring, and deleting delegates for corresponding HomeKit
682
+ * accessories and services.
683
+ *
684
+ * The analysis consists of the following steps:
685
+ * 1. Analyse the resources, updating:
686
+ * {@link DeconzAccessory.Gateway#deviceById deviceById},
687
+ * {@link DeconzAccessory.Gateway#deviceByRidByRtype deviceByRidByRtype},
688
+ * {@link DeconzAccessory.Gateway#nDevices nDevices},
689
+ * {@link DeconzAccessory.Gateway#nDevicesByRtype nDevicesByRtype},
690
+ * {@link DeconzAccessory.Gateway#nResources nResources},
691
+ * {@link DeconzAccessory.Gateway#resourceByRpath resourceByRpath}.
692
+ * 2. Analyse (pre-existing) _Device_ accessories, emitting
693
+ * {@link DeconzAccessory.Device#event.polled}, and calling
694
+ * {@link DeconzAccessory.Gateway#deleteAccessory deleteAccessory()} for
695
+ * stale accessories, corresponding to devices that have been deleted from
696
+ * the gateway, blacklisted, or excluded by device primary resource type.
697
+ * 3. Analyse (pre-existing) _Device Settings_ services, calling
698
+ * {@link DeconzAccessory.Gateway#deleteService deleteService()}
699
+ * for stale services, corresponding to devices that have been deleted from
700
+ * the gateway, un-blacklisted, or excluded by device primary resource type.
701
+ * 4. Analysing supported devices with enabled device primary resource types,
702
+ * calling {@link DeconzAccessory.Gateway#addAccessory addAccessory()} and
703
+ * {@link DeconzAccessory.Gateway#deleteService deleteService()} for new
704
+ * _Device_ accessories, corresponding to devices added to the gateway,
705
+ * un-blacklisted, or included by device primary resource type, and calling
706
+ * {@link DeconzAccessory.Gateway#addService addService()} and
707
+ * {@link DeconzAccessory.Gateway#deleteAccessory deleteAccessory()} for
708
+ * accessories, corresponding to devices have been blacklisted.
709
+ * @param {Object} fullState - The gateway full state, as returned by
710
+ * {@link DeconzAccessory.Gateway#poll poll()}.
711
+ * @param {Object} params - Parameters
712
+ * @param {boolean} [params.logUnsupportedResources=false] - Issue debug
713
+ * messsages for unsupported resources.
714
+ * @param {boolean} [params.analyseOnly=false]
715
+ */
716
+ analyseFullState (fullState, params = {}) {
717
+ /** Supported devices by device ID.
718
+ *
719
+ * Updated by
720
+ * {@link DeconzAccessory.Gateway#analyseFullState analyseFullState()}.
721
+ * @type {Object<string, Deconz.Device>}
638
722
  */
639
723
  this.deviceById = {}
640
724
 
641
- /** Resources by resource path.
642
- * @type {Object<string, DeconzDevice.Resource>}
725
+ /** Supported resources by resource path.
726
+ *
727
+ * Updated by {@link DeconzAccessory.Gateway#analyseFullState analyseFullState()}.
728
+ * @type {Object<string, Deconz.Resource>}
643
729
  */
644
730
  this.resourceByRpath = {}
645
731
 
646
- /** Devices by resource ID by resource type.
647
- * @type {Object<string, Object<string, DeconzDevice>>}
732
+ /** Supported devices by resource ID by resource type, of the primary
733
+ * resource for the device.
734
+ *
735
+ * Updated by
736
+ * {@link DeconzAccessory.Gateway#analyseFullState analyseFullState()}.
737
+ * @type {Object<string, Object<string, Deconz.Device>>}
648
738
  */
649
739
  this.deviceByRidByRtype = {}
650
740
 
651
- /** Map of number of devices by resource type.
741
+ /** Number of supported devices by resource type.
742
+ *
743
+ * Updated by
744
+ * {@link DeconzAccessory.Gateway#analyseFullState analyseFullState()}.
652
745
  * @type {Object<string, integer>}
653
746
  */
654
747
  this.nDevicesByRtype = {}
@@ -657,12 +750,30 @@ class Gateway extends homebridgeLib.AccessoryDelegate {
657
750
  for (const rtype of rtypes) {
658
751
  this.deviceByRidByRtype[rtype] = {}
659
752
  for (const rid in fullState[rtype]) {
660
- const body = fullState[rtype][rid]
661
- this.analyseResource(rtype, rid, body)
753
+ try {
754
+ const body = fullState[rtype][rid]
755
+ this.analyseResource(rtype, rid, body, params.logUnsupported)
756
+ } catch (error) { this.error(error) }
662
757
  }
663
758
  }
759
+
760
+ /** Number of supported devices.
761
+ *
762
+ * Updated by
763
+ * {@link DeconzAccessory.Gateway#analyseFullState analyseFullState()}.
764
+ * @type {integer}
765
+ */
766
+
664
767
  this.nDevices = Object.keys(this.deviceById).length
768
+
769
+ /** Number of supported resources.
770
+ *
771
+ * Updated by
772
+ * {@link DeconzAccessory.Gateway#analyseFullState analyseFullState()}.
773
+ * @type {integer}
774
+ */
665
775
  this.nResources = Object.keys(this.resourceByRpath).length
776
+
666
777
  this.vdebug('%d devices, %d resources', this.nDevices, this.nResources)
667
778
  for (const id in this.deviceById) {
668
779
  const device = this.deviceById[id]
@@ -675,76 +786,87 @@ class Gateway extends homebridgeLib.AccessoryDelegate {
675
786
  this.vdebug('%d %s devices', this.nDevicesByRtype[rtype], rtype)
676
787
  }
677
788
 
789
+ if (params.analyseOnly) {
790
+ return
791
+ }
792
+
793
+ this.update(fullState.config)
794
+
678
795
  let changed = false
679
796
 
680
797
  this.vdebug('analysing accessories...')
681
798
  for (const id in this.accessoryById) {
682
- if (
683
- this.deviceById[id] == null ||
684
- !this.service.values.rtypes.includes(this.deviceById[id].resource.rtype)
685
- ) {
686
- delete this.context.blacklist[id]
687
- this.deleteAccessory(id)
688
- this.deleteService(id)
689
- changed = true
690
- } else {
691
- /** Emitted when resource has been polled.
692
- * @event DeconzAccessory.Device#polled
693
- * @param {DeconzDevice} device - The device.
694
- */
695
- this.accessoryById[id].emit('polled', this.deviceById[id])
696
- }
697
- }
698
- this.nAccessories = Object.keys(this.accessoryById).length
699
- this.nExposeErrors = Object.keys(this.exposeErrorById).length
700
- if (this.nExposeErrors === 0) {
701
- this.vdebug('%d accessories', this.nAccessories)
702
- } else {
703
- this.vdebug(
704
- '%d accessories, %d expose errors', this.nAccessories, this.nExposeErrors
705
- )
799
+ try {
800
+ if (
801
+ this.deviceById[id] == null
802
+ ) {
803
+ delete this.context.blacklist[id]
804
+ this.deleteAccessory(id)
805
+ this.deleteService(id)
806
+ changed = true
807
+ } else {
808
+ /** Emitted when the gateway has been polled.
809
+ * @event DeconzAccessory.Device#polled
810
+ * @param {Deconz.Device} device - The updated device.
811
+ */
812
+ this.accessoryById[id].emit('polled', this.deviceById[id])
813
+ }
814
+ } catch (error) { this.error(error) }
706
815
  }
707
816
 
708
817
  this.vdebug('analysing services...')
709
818
  for (const id in this.serviceById) {
710
- if (
711
- this.deviceById[id] == null ||
712
- !this.service.values.rtypes.includes(this.deviceById[id].resource.rtype)
713
- ) {
714
- delete this.context.blacklist[id]
715
- delete this.exposeErrorById[id]
716
- this.deleteService(id)
717
- changed = true
718
- }
819
+ try {
820
+ if (
821
+ this.deviceById[id] == null ||
822
+ !this.values.rtypes.includes(this.deviceById[id].resource.rtype)
823
+ ) {
824
+ delete this.exposeErrorById[id]
825
+ this.deleteService(id)
826
+ changed = true
827
+ }
828
+ } catch (error) { this.error(error) }
719
829
  }
720
830
 
721
- for (const rtype of this.service.values.rtypes) {
831
+ for (const rtype of this.values.rtypes) {
722
832
  this.vdebug('analysing %s devices...', rtype)
723
833
  const rids = Object.keys(this.deviceByRidByRtype[rtype]).sort()
724
834
  for (const rid of rids) {
725
- const { id } = this.deviceByRidByRtype[rtype][rid]
726
- if (this.context.blacklist[id] == null) {
727
- if (this.accessoryById[id] == null && this.unsupported[id] == null) {
728
- this.addAccessory(id)
729
- changed = true
730
- }
731
- if (this.serviceById[id] != null) {
732
- this.deleteService(id)
733
- changed = true
734
- }
735
- } else {
736
- if (this.serviceById[id] == null && this.unsupported[id] == null) {
737
- this.addService(id)
738
- changed = true
739
- }
740
- if (this.accessoryById[id] != null) {
741
- this.deleteAccessory(id)
742
- changed = true
835
+ try {
836
+ const { id } = this.deviceByRidByRtype[rtype][rid]
837
+ if (this.context.blacklist[id] == null) {
838
+ if (this.accessoryById[id] == null) {
839
+ this.addAccessory(id)
840
+ changed = true
841
+ }
842
+ if (this.serviceById[id] != null) {
843
+ this.deleteService(id)
844
+ changed = true
845
+ }
846
+ } else {
847
+ if (this.serviceById[id] == null) {
848
+ this.addService(id)
849
+ changed = true
850
+ }
851
+ if (this.accessoryById[id] != null) {
852
+ this.deleteAccessory(id)
853
+ changed = true
854
+ }
743
855
  }
744
- }
856
+ } catch (error) { this.error(error) }
745
857
  }
746
858
  }
747
859
 
860
+ this.nAccessories = Object.keys(this.accessoryById).length
861
+ this.nExposeErrors = Object.keys(this.exposeErrorById).length
862
+ if (this.nExposeErrors === 0) {
863
+ this.vdebug('%d accessories', this.nAccessories)
864
+ } else {
865
+ this.vdebug(
866
+ '%d accessories, %d expose errors', this.nAccessories, this.nExposeErrors
867
+ )
868
+ }
869
+
748
870
  if (changed) {
749
871
  this.nResourcesMonitored = Object.keys(this.accessoryByRpath).length
750
872
  this.identify()
@@ -753,226 +875,46 @@ class Gateway extends homebridgeLib.AccessoryDelegate {
753
875
  }
754
876
  }
755
877
 
756
- /** Anayse a resource, updating
878
+ /** Anayse a gateway resource, updating
757
879
  * {@link DeconzAccessory.Gateway#deviceById deviceById} and
758
- * {@link DeconzAccessory.Gateway#resourceByRpath resourceByRpath} and
880
+ * {@link DeconzAccessory.Gateway#resourceByRpath resourceByRpath} for
881
+ * supported resources.
759
882
  *
760
883
  * @param {string} rtype - The type of the resource:
761
- * `group`, `light`, or `sensor`.
884
+ * `groups`, `lights`, or `sensors`.
762
885
  * @param {integer} rid - The resource ID of the resource.
763
886
  * @param {object} body - The body of the resource.
887
+ * @param {boolean} logUnsupported - Issue a debug message for
888
+ * unsupported resources.
764
889
  */
765
- analyseResource (rtype, rid, body) {
766
- const attrs = this.resourceAttrs(rtype, rid, body)
767
- const { id } = attrs
768
- if (id === this.id) {
890
+ analyseResource (rtype, rid, body, logUnsupported) {
891
+ const resource = new Deconz.Resource(this, rtype, rid, body)
892
+ const { id, serviceName } = resource
893
+ if (id === this.id || serviceName === '') {
894
+ const debug = (logUnsupported ? this.debug : this.vdebug).bind(this)
895
+ debug(
896
+ '%s: /%s/%d: %s: ignoring unsupported %s type',
897
+ id, rtype, rid, body.type, rtype
898
+ )
899
+ return
900
+ }
901
+ if (serviceName == null) {
902
+ const warn = (logUnsupported ? this.warn : this.vdebug).bind(this)
903
+ warn(
904
+ '%s: /%s/%d: %s: ignoring unknown %s type',
905
+ id, rtype, rid, body.type, rtype
906
+ )
769
907
  return
770
908
  }
771
909
  if (this.deviceById[id] == null) {
772
- this.deviceById[id] = new DeconzDevice(rtype, rid, body, attrs)
910
+ this.deviceById[id] = new Deconz.Device(resource)
773
911
  this.vdebug('%s: device', id)
912
+ } else {
913
+ this.deviceById[id].addResource(resource)
774
914
  }
775
- const resource = this.deviceById[id].addResource(rtype, rid, body, attrs)
776
915
  const { rpath } = resource
777
- this.vdebug('%s: %s: device resource', id, rpath)
778
916
  this.resourceByRpath[rpath] = resource
779
- }
780
-
781
- /** Determine the derived attributes for a resource.
782
- *
783
- * @param {string} rtype - The type of the resource:
784
- * `config`, `group`, `light`, or `sensor`.
785
- * @param {integer} rid - The resource ID of the resource.
786
- * @param {object} body - The body of the resource.
787
- * @returns {DeconzDevice.ResourceAttributes} - The derived
788
- * resource attributes.
789
- */
790
- resourceAttrs (rtype, rid, body) {
791
- toString('params.rtype', rtype, true)
792
- if (!(rtypes.includes(rtype))) {
793
- throw new RangeError(`rtype: ${rtype}: not a valid resource type`)
794
- }
795
- toInt('rid', rid)
796
- toObject('body', body)
797
- toString('body.name', body.name, true)
798
- toString('body.type', body.type, true)
799
- if (rtype === 'lights' || (rtype === 'sensors' && body.type.startsWith('Z'))) {
800
- const { mac, endpoint, cluster } = parseUniqueid(body.uniqueid)
801
- return new DeconzDevice.ResourceAttributes(
802
- mac,
803
- endpoint + (cluster == null ? '' : '-' + cluster),
804
- rtype === 'lights'
805
- ? this.lightsTypeAttributes(body.type)
806
- : this.sensorsTypeAttributes(body.type),
807
- true
808
- )
809
- }
810
- if (rtype === 'groups') {
811
- return new DeconzDevice.ResourceAttributes(
812
- this.id + '-G' + rid,
813
- 'G' + rid,
814
- new DeconzDevice.TypeAttributes(
815
- this.Accessory.Categories.LIGHTBULB, 'LightBulb'
816
- ),
817
- false
818
- )
819
- }
820
- return new DeconzDevice.ResourceAttributes(
821
- this.id + '-S' + rid,
822
- 'S' + rid,
823
- this.sensorsTypeAttributes(body.type),
824
- false
825
- )
826
- }
827
-
828
- /** Derive the attributes for a `/lights` resource type.
829
- *
830
- * @params {string} type - The `type` attribute of the `/lights` resource.
831
- * @return {DeconzDevice.TypeAttributes} - The derived type
832
- * attributes.
833
- */
834
- lightsTypeAttributes (type) {
835
- const Cats = this.Accessory.Categories
836
- switch (type) {
837
- case 'Color dimmable light':
838
- return new DeconzDevice.TypeAttributes(Cats.LIGHTBULB, 'LightBulb', 4)
839
- case 'Color light':
840
- return new DeconzDevice.TypeAttributes(Cats.LIGHTBULB, 'LightBulb', 3)
841
- case 'Color temperature light':
842
- return new DeconzDevice.TypeAttributes(Cats.LIGHTBULB, 'LightBulb', 2)
843
- case 'Dimmable light':
844
- return new DeconzDevice.TypeAttributes(Cats.LIGHTBULB, 'LightBulb', 1)
845
- case 'Dimmable plug-in unit':
846
- return new DeconzDevice.TypeAttributes(Cats.OUTLET, 'LightBulb')
847
- case 'Extended color light':
848
- return new DeconzDevice.TypeAttributes(Cats.LIGHTBULB, 'LightBulb', 5)
849
- // case 'Consumption awareness device':
850
- // return new DeconzDevice.TypeAttributes(Cats.OTHER)
851
- case 'Dimmer switch':
852
- return new DeconzDevice.TypeAttributes(Cats.LIGHTBULB, 'LightBulb')
853
- case 'Level control switch':
854
- return new DeconzDevice.TypeAttributes(Cats.LIGHTBULB, 'LightBulb')
855
- // case 'Level controllable output':
856
- // return new DeconzDevice.TypeAttributes(Cats.OTHER)
857
- // case 'Door Lock':
858
- // return new DeconzDevice.TypeAttributes(Cats.DOOR_LOCK)
859
- // case 'Door Lock Unit':
860
- // return new DeconzDevice.TypeAttributes(Cats.DOOR_LOCK)
861
- // case 'Fan':
862
- // return new DeconzDevice.TypeAttributes(Cats.FAN)
863
- case 'On/Off light switch':
864
- return new DeconzDevice.TypeAttributes(Cats.SWITCH, 'Outlet')
865
- case 'On/Off light':
866
- return new DeconzDevice.TypeAttributes(Cats.LIGHTBULB, 'Outlet')
867
- case 'On/Off output':
868
- return new DeconzDevice.TypeAttributes(Cats.OUTLET, 'Outlet')
869
- case 'On/Off plug-in unit':
870
- return new DeconzDevice.TypeAttributes(Cats.OUTLET, 'Outlet')
871
- case 'Smart plug':
872
- return new DeconzDevice.TypeAttributes(Cats.OUTLET, 'Outlet')
873
- case 'Configuration tool':
874
- return new DeconzDevice.TypeAttributes(Cats.BRIDGE)
875
- case 'Range extender':
876
- return new DeconzDevice.TypeAttributes(Cats.RANGE_EXTENDER)
877
- case 'Warning device':
878
- return new DeconzDevice.TypeAttributes(Cats.SECURITY_SYSTEM, 'WarningDevice')
879
- case 'Window covering controller':
880
- return new DeconzDevice.TypeAttributes(Cats.WINDOW_COVERING, 'WindowCovering')
881
- case 'Window covering device':
882
- return new DeconzDevice.TypeAttributes(Cats.WINDOW_COVERING, 'WindowCovering')
883
- default:
884
- return new DeconzDevice.TypeAttributes()
885
- }
886
- }
887
-
888
- /** Derivce the attributes for a `/sensors` resource type.
889
- *
890
- * @params {string} type - The `type` attribute of the `/sensors` resource.
891
- * @return {DeconzDevice.TypeAttributes} - The derived type
892
- * attributes.
893
- */
894
- sensorsTypeAttributes (type) {
895
- const Cats = this.Accessory.Categories
896
- switch (type) {
897
- case 'ZHAAirPurifier':
898
- case 'CLIPAirPurifier':
899
- return new DeconzDevice.TypeAttributes(Cats.AIR_PURIFIER)
900
- case 'ZHAAirQuality':
901
- case 'CLIPAirQuality':
902
- return new DeconzDevice.TypeAttributes(Cats.SENSOR, 'AirQuality', 3)
903
- case 'ZHAAlarm':
904
- case 'CLIPAlarm':
905
- return new DeconzDevice.TypeAttributes(Cats.SECURITY_SYSTEM, 'Alarm')
906
- // case 'ZHAAncillaryControl':
907
- // case 'CLIPAncillaryControl':
908
- // return new DeconzDevice.TypeAttributes(Cats.SECURITY_SYSTEM)
909
- case 'ZHABattery':
910
- case 'CLIPBattery':
911
- return new DeconzDevice.TypeAttributes(Cats.SENSOR, 'Battery')
912
- case 'ZHACarbonMonoxide':
913
- case 'CLIPCarbonMonoxide':
914
- return new DeconzDevice.TypeAttributes(Cats.SENSOR, 'CarbonMonoxide')
915
- case 'ZHAConsumption':
916
- case 'CLIPConsumption':
917
- return new DeconzDevice.TypeAttributes(Cats.SENSOR, 'Consumption')
918
- // case 'ZHADoorLock':
919
- // case 'CLIPDoorLock':
920
- // return new DeconzDevice.TypeAttributes(Cats.DOOR_LOCK)
921
- case 'Daylight':
922
- return new DeconzDevice.TypeAttributes(Cats.SENSOR, 'Daylight')
923
- case 'ZHAFire':
924
- case 'CLIPFire':
925
- return new DeconzDevice.TypeAttributes(Cats.SENSOR, 'Fire')
926
- case 'CLIPGenericFlag':
927
- return new DeconzDevice.TypeAttributes(Cats.SWITCH, 'Flag')
928
- case 'CLIPGenericStatus':
929
- return new DeconzDevice.TypeAttributes(Cats.OTHER, 'Status')
930
- case 'ZHAHumidity':
931
- case 'CLIPHumidity':
932
- return new DeconzDevice.TypeAttributes(Cats.SENSOR, 'Humidity', 4)
933
- case 'ZHALightLevel':
934
- case 'CLIPLightLevel':
935
- return new DeconzDevice.TypeAttributes(Cats.SENSOR, 'LightLevel', 6)
936
- // case 'ZHAMoisture':
937
- // case 'CLIPMoisture':
938
- // return new DeconzDevice.TypeAttributes(Cats.SENSOR)
939
- case 'ZHAOpenClose':
940
- case 'CLIPOpenClose':
941
- return new DeconzDevice.TypeAttributes(Cats.SENSOR, 'OpenClose', 8)
942
- case 'ZHAPower':
943
- case 'CLIPPower':
944
- return new DeconzDevice.TypeAttributes(Cats.SENSOR, 'Power', 1)
945
- case 'ZHAPresence':
946
- case 'CLIPPresence':
947
- return new DeconzDevice.TypeAttributes(Cats.SENSOR, 'Presence', 7)
948
- case 'ZHAPressure':
949
- case 'CLIPPressure':
950
- return new DeconzDevice.TypeAttributes(Cats.SENSOR, 'Pressure', 3)
951
- // case 'ZHASpectral':
952
- // return new DeconzDevice.TypeAttributes(Cats.SENSOR)
953
- case 'ZGPSwitch':
954
- case 'ZHASwitch':
955
- return new DeconzDevice.TypeAttributes(Cats.PROGRAMMABLE_SWITCH, 'Switch')
956
- case 'CLIPSwitch':
957
- return new DeconzDevice.TypeAttributes(Cats.PROGRAMMABLE_SWITCH)
958
- case 'ZHATemperature':
959
- case 'CLIPTemperature':
960
- return new DeconzDevice.TypeAttributes(Cats.SENSOR, 'Temperature', 5)
961
- case 'ZHAThermostat':
962
- case 'CLIPThermostat':
963
- return new DeconzDevice.TypeAttributes(Cats.THERMOSTAT, 'Thermostat')
964
- case 'ZHATime':
965
- case 'CLIPTime':
966
- return new DeconzDevice.TypeAttributes(Cats.OTHER)
967
- case 'ZHAVibration':
968
- case 'CLIPVibration':
969
- return new DeconzDevice.TypeAttributes(Cats.SENSOR, 'Vibration')
970
- case 'ZHAWater':
971
- case 'CLIPWater':
972
- return new DeconzDevice.TypeAttributes(Cats.SENSOR, 'Water')
973
- default:
974
- return new DeconzDevice.TypeAttributes()
975
- }
917
+ this.vdebug('%s: %s: device resource', id, rpath)
976
918
  }
977
919
  }
978
920