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
package/cli/deconz.js ADDED
@@ -0,0 +1,980 @@
1
+ #!/usr/bin/env node
2
+
3
+ // homebridge-deconz/cli/deconz.js
4
+ // Copyright © 2018-2022 Erik Baauw. All rights reserved.
5
+ //
6
+ // Command line interface to deCONZ gateway.
7
+
8
+ 'use strict'
9
+
10
+ const fs = require('fs')
11
+ const Deconz = require('../lib/Deconz')
12
+ const homebridgeLib = require('homebridge-lib')
13
+ const packageJson = require('../package.json')
14
+
15
+ const { b, u } = homebridgeLib.CommandLineTool
16
+ const { UsageError } = homebridgeLib.CommandLineParser
17
+
18
+ const usage = {
19
+ deconz: `${b('deconz')} [${b('-hVDp')}] [${b('-H')} ${u('hostname')}[${b(':')}${u('port')}]] [${b('-K')} ${u('api key')}] [${b('-t')} ${u('timeout')}] ${u('command')} [${u('argument')} ...]`,
20
+
21
+ get: `${b('get')} [${b('-hsnjuatlkv')}] [${u('path')}]`,
22
+ put: `${b('put')} [${b('-hv')}] ${u('resource')} [${u('body')}]`,
23
+ post: `${b('post')} [${b('-hv')}] ${u('resource')} [${u('body')}]`,
24
+ delete: `${b('delete')} [${b('-hv')}] ${u('resource')} [${u('body')}]`,
25
+
26
+ eventlog: `${b('eventlog')} [${b('-hnrs')}]`,
27
+
28
+ discover: `${b('discover')} [${b('-hS')}]`,
29
+ config: `${b('config')} [${b('-hs')}]`,
30
+ description: `${b('description')} [${b('-hs')}]`,
31
+ getApiKey: `${b('getApiKey')} [${b('-hv')}]`,
32
+ unlock: `${b('unlock')} [${b('-hv')}]`,
33
+ search: `${b('search')} [${b('-hv')}]`,
34
+
35
+ probe: `${b('probe')} [${b('-hv')}] [${b('-t')} ${u('timeout')}] ${u('light')}`,
36
+ restart: `${b('restart')} [${b('-hv')}]`
37
+ }
38
+ const description = {
39
+ deconz: 'Command line interface to deCONZ gateway.',
40
+
41
+ get: `Retrieve ${u('path')} from gateway.`,
42
+ put: `Update ${u('resource')} on gateway with ${u('body')}.`,
43
+ post: `Create ${u('resource')} on gateway with ${u('body')}.`,
44
+ delete: `Delete ${u('resource')} from gateway with ${u('body')}.`,
45
+
46
+ eventlog: 'Log web socket notifications by the gateway.',
47
+
48
+ discover: 'Discover gateways.',
49
+ config: 'Retrieve gateway configuration (unauthenticated).',
50
+ description: 'Retrieve gateway description.',
51
+
52
+ getApiKey: 'Obtain an API key for the gateway.',
53
+ unlock: 'Unlock the gateway so new clients can obtain an API key.',
54
+ search: 'Initiate a seach for new devices.',
55
+
56
+ probe: `Probe ${u('light')} for supported colour (temperature) range.`,
57
+ restart: 'Restart the gateway.'
58
+ }
59
+ const help = {
60
+ deconz: `${description.deconz}
61
+
62
+ Usage: ${usage.deconz}
63
+
64
+ Parameters:
65
+ ${b('-h')}, ${b('--help')}
66
+ Print this help and exit.
67
+
68
+ ${b('-V')}, ${b('--version')}
69
+ Print version and exit.
70
+
71
+ ${b('-D')}, ${b('--debug')}
72
+ Print debug messages for communication with the gateway.
73
+
74
+ ${b('-p')}, ${b('--phoscon')}
75
+ Imitate the Phoscon app.
76
+
77
+ ${b('-H')} ${u('hostname')}[${b(':')}${u('port')}], ${b('--host=')}${u('hostname')}[${b(':')}${u('port')}]
78
+ Connect to ${u('hostname')}${b(':80')} or ${u('hostname')}${b(':')}${u('port')} instead of the default ${b('localhost:80')}.
79
+ The hostname and port can also be specified by setting ${b('DECONZ_HOST')}.
80
+
81
+ ${b('-K')} ${u('API key')}, ${b('--apiKey=')}${u('API key')}
82
+ Use ${u('API key')} instead of the API key saved in ${b('~/.deconz')}.
83
+ The API key can also be specified by setting ${b('DECONZ_API_KEY')}.
84
+
85
+ ${b('-t')} ${u('timeout')}, ${b('--timeout=')}${u('timeout')}
86
+ Set timeout to ${u('timeout')} seconds instead of default ${b(5)}.
87
+
88
+ Commands:
89
+ ${usage.get}
90
+ ${description.get}
91
+
92
+ ${usage.put}
93
+ ${description.put}
94
+
95
+ ${usage.post}
96
+ ${description.post}
97
+
98
+ ${usage.delete}
99
+ ${description.delete}
100
+
101
+ ${usage.eventlog}
102
+ ${description.eventlog}
103
+
104
+ ${usage.discover}
105
+ ${description.discover}
106
+
107
+ ${usage.config}
108
+ ${description.config}
109
+
110
+ ${usage.description}
111
+ ${description.description}
112
+
113
+ ${usage.getApiKey}
114
+ ${description.getApiKey}
115
+
116
+ ${usage.unlock}
117
+ ${description.unlock}
118
+
119
+ ${usage.search}
120
+ ${description.search}
121
+
122
+ ${usage.probe}
123
+ ${description.probe}
124
+
125
+ ${usage.restart}
126
+ ${description.restart}
127
+
128
+ For more help, issue: ${b('deconz')} ${u('command')} ${b('-h')}`,
129
+ get: `${description.deconz}
130
+
131
+ Usage: ${b('deconz')} ${usage.get}
132
+
133
+ ${description.get}
134
+
135
+ Parameters:
136
+ ${b('-h')}, ${b('--help')}
137
+ Print this help and exit.
138
+
139
+ ${b('-s')}, ${b('--sortKeys')}
140
+ Sort object key/value pairs alphabetically on key.
141
+
142
+ ${b('-n')}, ${b('-noWhiteSpace')}
143
+ Do not include spaces nor newlines in the output.
144
+
145
+ ${b('-j')}, ${b('--jsonArray')}
146
+ Output a JSON array of objects for each key/value pair.
147
+ Each object contains two key/value pairs: key "keys" with an array
148
+ of keys as value and key "value" with the value as value.
149
+
150
+ ${b('-u')}, ${b('--joinKeys')}
151
+ Output JSON array of objects for each key/value pair.
152
+ Each object contains one key/value pair: the path (concatenated
153
+ keys separated by '/') as key and the value as value.
154
+
155
+ ${b('-a')}, ${b('--ascii')}
156
+ Output path:value in plain text instead of JSON.
157
+
158
+ ${b('-t')}, ${b('--topOnly')}
159
+ Limit output to top-level key/values.
160
+
161
+ ${b('-l')}, ${b('--leavesOnly')}
162
+ Limit output to leaf (non-array, non-object) key/values.
163
+
164
+ ${b('-k')}, ${b('--keysOnly')}
165
+ Limit output to keys. With ${b('-u')}, output a JSON array of paths.
166
+
167
+ ${b('-v')}, ${b('--valuesOnly')}
168
+ Limit output to values. With ${b('-u')}, output a JSON array of values.
169
+
170
+ ${u('path')}
171
+ Path to retrieve from the gateway.`,
172
+ put: `${description.deconz}
173
+
174
+ Usage: ${b('deconz')} ${usage.put}
175
+
176
+ ${description.put}
177
+
178
+ Parameters:
179
+ ${b('-h')}, ${b('--help')}
180
+ Print this help and exit.
181
+
182
+ ${b('-v')}, ${b('--verbose')}
183
+ Print full API output.
184
+
185
+ ${u('resource')}
186
+ Resource to update.
187
+
188
+ ${u('body')}
189
+ Body in JSON.`,
190
+ post: `${description.deconz}
191
+
192
+ Usage: ${b('deconz')} ${usage.post}
193
+
194
+ ${description.post}
195
+
196
+ Parameters:
197
+ ${b('-h')}, ${b('--help')}
198
+ Print this help and exit.
199
+
200
+ ${b('-v')}, ${b('--verbose')}
201
+ Print full API output.
202
+
203
+ ${u('resource')}
204
+ Resource to create.
205
+
206
+ ${u('body')}
207
+ Body in JSON.`,
208
+ delete: `${description.deconz}
209
+
210
+ Usage: ${b('deconz')} ${usage.delete}
211
+
212
+ ${description.delete}
213
+
214
+ Parameters:
215
+ ${b('-h')}, ${b('--help')}
216
+ Print this help and exit.
217
+
218
+ ${b('-v')}, ${b('--verbose')}
219
+ Print full API output.
220
+
221
+ ${u('resource')}
222
+ Resource to delete.
223
+
224
+ ${u('body')}
225
+ Body in JSON.`,
226
+ eventlog: `${description.deconz}
227
+
228
+ Usage: ${b('deconz')} ${usage.eventlog}
229
+
230
+ ${description.eventlog}
231
+
232
+ Parameters:
233
+ ${b('-h')}, ${b('--help')}
234
+ Print this help and exit.
235
+
236
+ ${b('-v')}, ${b('--verbose')}
237
+ Print full API output.
238
+
239
+ ${b('-n')}, ${b('--noRetry')}
240
+ Do not retry when connection is closed.
241
+
242
+ ${b('-r')}, ${b('--raw')}
243
+ Do not parse events, output raw event data.
244
+
245
+ ${b('-s')}, ${b('--service')}
246
+ Do not output timestamps (useful when running as service).`,
247
+ discover: `${description.deconz}
248
+
249
+ Usage: ${b('deconz')} ${usage.discover}
250
+
251
+ ${description.discover}
252
+
253
+ Parameters:
254
+ ${b('-h')}, ${b('--help')}
255
+ Print this help and exit.
256
+
257
+ ${b('-S')}, ${b('--stealth')}
258
+ Stealth mode, only use local discovery.`,
259
+ config: `${description.deconz}
260
+
261
+ Usage: ${b('deconz')} ${usage.config}
262
+
263
+ ${description.config}
264
+
265
+ Parameters:
266
+ ${b('-h')}, ${b('--help')}
267
+ Print this help and exit.
268
+
269
+ ${b('-s')}, ${b('--sortKeys')}
270
+ Sort object key/value pairs alphabetically on key.`,
271
+ description: `${description.deconz}
272
+
273
+ Usage: ${b('deconz')} ${usage.description}
274
+
275
+ ${description.description}
276
+
277
+ Parameters:
278
+ ${b('-h')}, ${b('--help')}
279
+ Print this help and exit.
280
+
281
+ ${b('-s')}, ${b('--sortKeys')}
282
+ Sort object key/value pairs alphabetically on key.`,
283
+ getApiKey: `${description.deconz}
284
+
285
+ Usage: ${b('deconz')} ${usage.getApiKey}
286
+
287
+ ${description.getApiKey}
288
+ You need to unlock the deCONZ gateway prior to issuing this command,
289
+ unless you're running it on the gateway's local host.
290
+ The API key is saved to ${b('~/.deconz')}.
291
+
292
+ Parameters:
293
+ ${b('-h')}, ${b('--help')}
294
+ Print this help and exit.
295
+
296
+ ${b('-v')}, ${b('--verbose')}
297
+ Print full API output.`,
298
+ unlock: `${description.deconz}
299
+
300
+ Usage: ${b('deconz')} ${usage.unlock}
301
+
302
+ ${description.unlock}
303
+
304
+ Parameters:
305
+ ${b('-h')}, ${b('--help')}
306
+ Print this help and exit.
307
+
308
+ ${b('-v')}, ${b('--verbose')}
309
+ Print full API output.`,
310
+ search: `${description.search}
311
+
312
+ Usage: ${b('deconz')} ${usage.search}
313
+
314
+ ${description.search}
315
+
316
+ Parameters:
317
+ ${b('-h')}, ${b('--help')}
318
+ Print this help and exit.
319
+
320
+ ${b('-v')}, ${b('--verbose')}
321
+ Print full API output.`,
322
+ probe: `${description.deconz}
323
+
324
+ Usage: ${b('deconz')} ${usage.probe}
325
+
326
+ ${description.probe}
327
+
328
+ Parameters:
329
+ ${b('-h')}, ${b('--help')}
330
+ Print this help and exit.
331
+
332
+ ${b('-v')}, ${b('--verbose')}
333
+ Print full API output.
334
+
335
+ ${b('-t')} ${u('timeout')}, ${b('--timeout=')}${u('timeout')}
336
+ Timeout after ${u('timeout')} minutes (default: 5).
337
+
338
+ ${u('light')}
339
+ Lights resource to probe.`,
340
+ restart: `${description.deconz}
341
+
342
+ Usage: ${b('deconz')} ${usage.restart}
343
+
344
+ ${description.restart}
345
+
346
+ Parameters:
347
+ ${b('-h')}, ${b('--help')}
348
+ Print this help and exit.
349
+
350
+ ${b('-v')}, ${b('--verbose')}
351
+ Print full API output.`
352
+ }
353
+
354
+ class Main extends homebridgeLib.CommandLineTool {
355
+ constructor () {
356
+ super({ mode: 'command', debug: false })
357
+ this.usage = usage.deconz
358
+ try {
359
+ this.readGateways()
360
+ } catch (error) {
361
+ if (error.code !== 'ENOENT') {
362
+ this.error(error)
363
+ }
364
+ this.gateways = {}
365
+ }
366
+ }
367
+
368
+ // ===========================================================================
369
+
370
+ readGateways () {
371
+ const text = fs.readFileSync(process.env.HOME + '/.deconz')
372
+ try {
373
+ this.gateways = JSON.parse(text)
374
+ } catch (error) {
375
+ this.warn('%s/.deconz: file corrupted', process.env.HOME)
376
+ this.gateways = {}
377
+ }
378
+ }
379
+
380
+ writeGateways () {
381
+ const jsonFormatter = new homebridgeLib.JsonFormatter(
382
+ { noWhiteSpace: true, sortKeys: true }
383
+ )
384
+ const text = jsonFormatter.stringify(this.gateways)
385
+ fs.writeFileSync(process.env.HOME + '/.deconz', text, { mode: 0o600 })
386
+ }
387
+
388
+ parseArguments () {
389
+ const parser = new homebridgeLib.CommandLineParser(packageJson)
390
+ const clargs = {
391
+ options: {
392
+ host: process.env.DECONZ_HOST || 'localhost',
393
+ timeout: 5
394
+ }
395
+ }
396
+ parser
397
+ .help('h', 'help', help.deconz)
398
+ .version('V', 'version')
399
+ .option('H', 'host', (value) => {
400
+ homebridgeLib.OptionParser.toHost('host', value, false, true)
401
+ clargs.options.host = value
402
+ })
403
+ .option('K', 'apiKey', (value) => {
404
+ clargs.options.apiKey = homebridgeLib.OptionParser.toString(
405
+ 'apiKey', value, true, true
406
+ )
407
+ })
408
+ .flag('p', 'phoscon', () => {
409
+ clargs.options.phoscon = true
410
+ })
411
+ .flag('D', 'debug', () => {
412
+ if (this.debugEnabled) {
413
+ this.setOptions({ vdebug: true })
414
+ } else {
415
+ this.setOptions({ debug: true, chalk: true })
416
+ }
417
+ })
418
+ .option('t', 'timeout', (value) => {
419
+ clargs.options.timeout = homebridgeLib.OptionParser.toInt(
420
+ 'timeout', value, 1, 60, true
421
+ )
422
+ })
423
+ .parameter('command', (value) => {
424
+ if (usage[value] == null || typeof this[value] !== 'function') {
425
+ throw new UsageError(`${value}: unknown command`)
426
+ }
427
+ clargs.command = value
428
+ })
429
+ .remaining((list) => { clargs.args = list })
430
+ parser
431
+ .parse()
432
+ return clargs
433
+ }
434
+
435
+ async main () {
436
+ try {
437
+ await this._main()
438
+ } catch (error) {
439
+ if (error.request == null) {
440
+ this.error(error)
441
+ }
442
+ }
443
+ }
444
+
445
+ async _main () {
446
+ this.clargs = this.parseArguments()
447
+ this.deconzDiscovery = new Deconz.Discovery({
448
+ timeout: this.clargs.options.timeout
449
+ })
450
+ this.deconzDiscovery
451
+ .on('error', (error) => {
452
+ this.log(
453
+ '%s: request %d: %s %s', error.request.name,
454
+ error.request.id, error.request.method, error.request.resource
455
+ )
456
+ this.warn(
457
+ '%s: request %d: %s', error.request.name, error.request.id, error
458
+ )
459
+ })
460
+ .on('request', (request) => {
461
+ this.debug(
462
+ '%s: request %d: %s %s', request.name,
463
+ request.id, request.method, request.resource
464
+ )
465
+ this.vdebug(
466
+ '%s: request %d: %s %s', request.name,
467
+ request.id, request.method, request.url
468
+ )
469
+ })
470
+ .on('response', (response) => {
471
+ this.vdebug(
472
+ '%s: request %d: response: %j', response.request.name,
473
+ response.request.id, response.body
474
+ )
475
+ this.debug(
476
+ '%s: request %d: %d %s', response.request.name,
477
+ response.request.id, response.statusCode, response.statusMessage
478
+ )
479
+ })
480
+ .on('found', (name, id, address) => {
481
+ this.debug('%s: found %s at %s', name, id, address)
482
+ })
483
+ .on('searching', (host) => {
484
+ this.debug('upnp: listening on %s', host)
485
+ })
486
+ .on('searchDone', () => { this.debug('upnp: search done') })
487
+
488
+ if (this.clargs.command === 'discover') {
489
+ return this.discover(this.clargs.args)
490
+ }
491
+ try {
492
+ this.gatewayConfig = await this.deconzDiscovery.config(
493
+ this.clargs.options.host
494
+ )
495
+ } catch (error) {
496
+ if (error.request == null) {
497
+ await this.fatal('%s: %s', this.clargs.options.host, error)
498
+ }
499
+ await this.fatal('%s: deCONZ gateway not found', this.clargs.options.host)
500
+ }
501
+
502
+ this.name = 'deconz ' + this.clargs.command
503
+ this.usage = `${b('deconz')} ${usage[this.clargs.command]}`
504
+
505
+ if (this.clargs.command === 'config' || this.clargs.command === 'description') {
506
+ return this[this.clargs.command](this.clargs.args)
507
+ }
508
+
509
+ this.bridgeid = this.gatewayConfig.bridgeid
510
+ if (this.clargs.options.apiKey == null) {
511
+ if (
512
+ this.gateways[this.bridgeid] != null &&
513
+ this.gateways[this.bridgeid].apiKey != null
514
+ ) {
515
+ this.clargs.options.apiKey = this.gateways[this.bridgeid].apiKey
516
+ } else if (process.env.DECONZ_API_KEY != null) {
517
+ this.clargs.options.apiKey = process.env.DECONZ_API_KEY
518
+ }
519
+ }
520
+ if (this.clargs.options.apiKey == null && this.clargs.command !== 'getApiKey') {
521
+ let args = ''
522
+ if (
523
+ this.clargs.options.host !== 'localhost' &&
524
+ this.clargs.options.host !== process.env.DECONZ_HOST
525
+ ) {
526
+ args += ' -H ' + this.clargs.options.host
527
+ }
528
+ await this.fatal(
529
+ 'missing API key - unlock gateway and run "deconz%s getApiKey"', args
530
+ )
531
+ }
532
+ this.client = new Deconz.ApiClient(this.clargs.options)
533
+ this.client
534
+ .on('error', (error) => {
535
+ if (error.request.id !== this.requestId) {
536
+ if (error.request.body == null) {
537
+ this.log(
538
+ 'request %d: %s %s', error.request.id,
539
+ error.request.method, error.request.resource
540
+ )
541
+ } else {
542
+ this.log(
543
+ 'request %d: %s %s %s', error.request.id,
544
+ error.request.method, error.request.resource, error.request.body
545
+ )
546
+ }
547
+ this.requestId = error.request.id
548
+ }
549
+ if (error.nonCritical) {
550
+ this.warn('request %d: %s', error.request.id, error)
551
+ } else {
552
+ this.error('request %d: %s', error.request.id, error)
553
+ }
554
+ })
555
+ .on('request', (request) => {
556
+ if (request.body == null) {
557
+ this.debug(
558
+ 'request %d: %s %s', request.id, request.method, request.resource
559
+ )
560
+ this.vdebug(
561
+ 'request %d: %s %s', request.id, request.method, request.url
562
+ )
563
+ } else {
564
+ this.debug(
565
+ 'request %d: %s %s %s', request.id,
566
+ request.method, request.resource, request.body
567
+ )
568
+ this.vdebug(
569
+ 'request %d: %s %s %s', request.id,
570
+ request.method, request.url, request.body
571
+ )
572
+ }
573
+ })
574
+ .on('response', (response) => {
575
+ this.vdebug(
576
+ 'request %d: response: %j', response.request.id, response.body
577
+ )
578
+ this.debug(
579
+ 'request %d: %d %s', response.request.id,
580
+ response.statusCode, response.statusMessage
581
+ )
582
+ })
583
+ return this[this.clargs.command](this.clargs.args)
584
+ }
585
+
586
+ // ===== GET =================================================================
587
+
588
+ async get (...args) {
589
+ const parser = new homebridgeLib.CommandLineParser(packageJson)
590
+ const clargs = {
591
+ options: {}
592
+ }
593
+ parser
594
+ .help('h', 'help', help.get)
595
+ .flag('s', 'sortKeys', () => { clargs.options.sortKeys = true })
596
+ .flag('n', 'noWhiteSpace', () => {
597
+ clargs.options.noWhiteSpace = true
598
+ })
599
+ .flag('j', 'jsonArray', () => { clargs.options.noWhiteSpace = true })
600
+ .flag('u', 'joinKeys', () => { clargs.options.joinKeys = true })
601
+ .flag('a', 'ascii', () => { clargs.options.ascii = true })
602
+ .flag('t', 'topOnly', () => { clargs.options.topOnly = true })
603
+ .flag('l', 'leavesOnly', () => { clargs.options.leavesOnly = true })
604
+ .flag('k', 'keysOnly', () => { clargs.options.keysOnly = true })
605
+ .flag('v', 'valuesOnly', () => { clargs.options.valuesOnly = true })
606
+ .remaining((list) => {
607
+ if (list.length > 1) {
608
+ throw new UsageError('too many paramters')
609
+ }
610
+ clargs.resource = list.length === 0
611
+ ? '/'
612
+ : homebridgeLib.OptionParser.toPath('resource', list[0])
613
+ })
614
+ .parse(...args)
615
+ const jsonFormatter = new homebridgeLib.JsonFormatter(clargs.options)
616
+ const response = await this.client.get(clargs.resource)
617
+ this.print(jsonFormatter.stringify(response))
618
+ }
619
+
620
+ // ===== PUT, POST, DELETE ===================================================
621
+
622
+ async resourceCommand (command, ...args) {
623
+ const parser = new homebridgeLib.CommandLineParser(packageJson)
624
+ const clargs = {
625
+ options: {}
626
+ }
627
+ parser
628
+ .help('h', 'help', help[command])
629
+ .flag('v', 'verbose', () => { clargs.options.verbose = true })
630
+ .parameter('resource', (resource) => {
631
+ clargs.resource = homebridgeLib.OptionParser.toPath('resource', resource)
632
+ if (clargs.resource === '/') {
633
+ // deCONZ will crash otherwise, see deconz-rest-plugin#2520.
634
+ throw new UsageError(`/: invalid resource for ${command}`)
635
+ }
636
+ })
637
+ .remaining((list) => {
638
+ if (list.length > 1) {
639
+ throw new Error('too many paramters')
640
+ }
641
+ if (list.length === 1) {
642
+ try {
643
+ clargs.body = JSON.parse(list[0])
644
+ } catch (error) {
645
+ throw new Error(error.message) // Covert TypeError to Error.
646
+ }
647
+ }
648
+ })
649
+ .parse(...args)
650
+ const response = await this.client[command](clargs.resource, clargs.body)
651
+ const jsonFormatter = new homebridgeLib.JsonFormatter()
652
+ if (clargs.options.verbose || response.success == null) {
653
+ this.print(jsonFormatter.stringify(response.body))
654
+ return
655
+ }
656
+ if (command !== 'put') {
657
+ if (response.success.id != null) {
658
+ this.print(jsonFormatter.stringify(response.success.id))
659
+ } else {
660
+ this.print(jsonFormatter.stringify(response.success))
661
+ }
662
+ return
663
+ }
664
+ this.print(jsonFormatter.stringify(response.success))
665
+ }
666
+
667
+ async put (...args) {
668
+ return this.resourceCommand('put', ...args)
669
+ }
670
+
671
+ async post (...args) {
672
+ return this.resourceCommand('post', ...args)
673
+ }
674
+
675
+ async delete (...args) {
676
+ return this.resourceCommand('delete', ...args)
677
+ }
678
+
679
+ // ===========================================================================
680
+
681
+ async destroy () {
682
+ if (this.wsMonitor != null) {
683
+ await this.wsMonitor.close()
684
+ }
685
+ if (this.eventStream != null) {
686
+ await this.eventStream.close()
687
+ }
688
+ }
689
+
690
+ async eventlog (...args) {
691
+ const parser = new homebridgeLib.CommandLineParser(packageJson)
692
+ let mode = 'daemon'
693
+ const options = {}
694
+ parser
695
+ .help('h', 'help', help.eventlog)
696
+ .flag('n', 'noRetry', () => { options.retryTime = 0 })
697
+ .flag('r', 'raw', () => { options.raw = true })
698
+ .flag('s', 'service', () => { mode = 'service' })
699
+ .parse(...args)
700
+ this.jsonFormatter = new homebridgeLib.JsonFormatter(
701
+ mode === 'service' ? { noWhiteSpace: true } : {}
702
+ )
703
+ const { websocketport } = await this.client.get('/config')
704
+ options.host = this.client.host + ':' + websocketport
705
+ this.wsMonitor = new Deconz.WsClient(options)
706
+ this.setOptions({ mode: mode })
707
+ this.wsMonitor
708
+ .on('error', (error) => { this.error(error) })
709
+ .on('listening', (url) => { this.log('listening on %s', url) })
710
+ .on('closed', (url) => { this.log('connection to %s closed', url) })
711
+ .on('changed', (rtype, rid, body) => {
712
+ let resource = '/' + rtype + '/' + rid
713
+ if (Object.keys(body).length === 1) {
714
+ if (body.state != null) {
715
+ resource += '/state'
716
+ body = body.state
717
+ } else if (body.config != null) {
718
+ resource += '/config'
719
+ body = body.config
720
+ }
721
+ }
722
+ this.log('%s: %s', resource, this.jsonFormatter.stringify(body))
723
+ })
724
+ .on('added', (rtype, rid, body) => {
725
+ this.log(
726
+ '/%s/%d: added: %s', rtype, rid, this.jsonFormatter.stringify(body)
727
+ )
728
+ })
729
+ .on('sceneRecall', (resource) => {
730
+ this.log('%s: recall', resource)
731
+ })
732
+ .on('notification', (body) => {
733
+ this.log(this.jsonFormatter.stringify(body))
734
+ })
735
+ .listen()
736
+ }
737
+
738
+ // ===========================================================================
739
+
740
+ async simpleCommand (command, ...args) {
741
+ const parser = new homebridgeLib.CommandLineParser(packageJson)
742
+ const clargs = {
743
+ options: {}
744
+ }
745
+ parser
746
+ .help('h', 'help', help[command])
747
+ .flag('v', 'verbose', () => { clargs.options.verbose = true })
748
+ .parse(...args)
749
+ const response = await this.client[command]()
750
+ const jsonFormatter = new homebridgeLib.JsonFormatter()
751
+ for (const error of response.errors) {
752
+ this.warn('api error %d: %s', error.type, error.description)
753
+ }
754
+ if (clargs.options.verbose || response.success == null) {
755
+ this.print(jsonFormatter.stringify(response.body))
756
+ return
757
+ }
758
+ if (response.success.id != null) {
759
+ this.print(jsonFormatter.stringify(response.success.id))
760
+ return
761
+ }
762
+ if (response.success != null) {
763
+ this.print(jsonFormatter.stringify(response.success))
764
+ return
765
+ }
766
+ this.print(jsonFormatter.stringify(response.body))
767
+ }
768
+
769
+ async discover (...args) {
770
+ const parser = new homebridgeLib.CommandLineParser(packageJson)
771
+ let stealth = false
772
+ parser
773
+ .help('h', 'help', help.discover)
774
+ .flag('S', 'stealth', () => { stealth = true })
775
+ .parse(...args)
776
+ const jsonFormatter = new homebridgeLib.JsonFormatter({ sortKeys: true })
777
+ const bridges = await this.deconzDiscovery.discover(stealth)
778
+ this.print(jsonFormatter.stringify(bridges))
779
+ }
780
+
781
+ async config (...args) {
782
+ const parser = new homebridgeLib.CommandLineParser(packageJson)
783
+ const options = {}
784
+ parser
785
+ .help('h', 'help', help.config)
786
+ .flag('s', 'sortKeys', () => { options.sortKeys = true })
787
+ .parse(...args)
788
+ const jsonFormatter = new homebridgeLib.JsonFormatter(options)
789
+ const json = jsonFormatter.stringify(this.gatewayConfig)
790
+ this.print(json)
791
+ }
792
+
793
+ async description (...args) {
794
+ const parser = new homebridgeLib.CommandLineParser(packageJson)
795
+ const options = {}
796
+ parser
797
+ .help('h', 'help', help.description)
798
+ .flag('s', 'sortKeys', () => { options.sortKeys = true })
799
+ .parse(...args)
800
+ const response = await this.deconzDiscovery.description(this.clargs.options.host)
801
+ const jsonFormatter = new homebridgeLib.JsonFormatter(options)
802
+ const json = jsonFormatter.stringify(response)
803
+ this.print(json)
804
+ }
805
+
806
+ async getApiKey (...args) {
807
+ const parser = new homebridgeLib.CommandLineParser(packageJson)
808
+ const jsonFormatter = new homebridgeLib.JsonFormatter(
809
+ { noWhiteSpace: true, sortKeys: true }
810
+ )
811
+ parser
812
+ .help('h', 'help', help.getApiKey)
813
+ .parse(...args)
814
+ const apiKey = await this.client.getApiKey('deconz')
815
+ this.print(jsonFormatter.stringify(apiKey))
816
+ this.gateways[this.bridgeid] = { apiKey: apiKey }
817
+ if (this.client.fingerprint != null) {
818
+ this.gateways[this.bridgeid].fingerprint = this.client.fingerprint
819
+ }
820
+ this.writeGateways()
821
+ }
822
+
823
+ async unlock (...args) {
824
+ return this.simpleCommand('unlock', ...args)
825
+ }
826
+
827
+ async search (...args) {
828
+ return this.simpleCommand('search', ...args)
829
+ }
830
+
831
+ // ===== LIGHTVALUES =========================================================
832
+
833
+ async probe (...args) {
834
+ const parser = new homebridgeLib.CommandLineParser(packageJson)
835
+ const clargs = {
836
+ maxCount: 60
837
+ }
838
+ parser
839
+ .help('h', 'help', help.probe)
840
+ .flag('v', 'verbose', () => { clargs.verbose = true })
841
+ .option('t', 'timeout', (value, key) => {
842
+ homebridgeLib.OptionParser.toInt(
843
+ 'timeout', value, 1, 10, true
844
+ )
845
+ clargs.maxCount = value * 12
846
+ })
847
+ .parameter('light', (value) => {
848
+ if (value.substring(0, 8) !== '/lights/') {
849
+ throw new UsageError(`${value}: invalid light`)
850
+ }
851
+ clargs.light = value
852
+ })
853
+ .parse(...args)
854
+ const light = await this.client.get(clargs.light)
855
+
856
+ async function probeCt (name, value) {
857
+ clargs.verbose && this.log(`${clargs.light}: ${name} ...\\c`)
858
+ await this.client.put(clargs.light + '/state', { ct: value })
859
+ let count = 0
860
+ return new Promise((resolve, reject) => {
861
+ const interval = setInterval(async () => {
862
+ const ct = await this.client.get(clargs.light + '/state/ct')
863
+ if (ct !== value || ++count > clargs.maxCount) {
864
+ clearInterval(interval)
865
+ clargs.verbose && this.logc(
866
+ count > clargs.maxCount ? ' timeout' : ' done'
867
+ )
868
+ return resolve(ct)
869
+ }
870
+ clargs.verbose && this.logc('.\\c')
871
+ }, 5000)
872
+ })
873
+ }
874
+
875
+ function round (f) {
876
+ return Math.round(f * 10000) / 10000
877
+ }
878
+
879
+ async function probeXy (name, value) {
880
+ clargs.verbose && this.log(`${clargs.light}: ${name} ...\\c`)
881
+ await this.client.put(clargs.light + '/state', { xy: value })
882
+ let count = 0
883
+ return new Promise((resolve, reject) => {
884
+ const interval = setInterval(async () => {
885
+ let xy = await this.client.get(clargs.light + '/state/xy')
886
+ if (this.client.isDeconz) {
887
+ xy = [round(xy[0]), round(xy[1])]
888
+ }
889
+ if (
890
+ xy[0] !== value[0] || xy[1] !== value[1] ||
891
+ ++count > clargs.maxCount
892
+ ) {
893
+ clearInterval(interval)
894
+ clargs.verbose && this.logc(
895
+ count > clargs.maxCount ? ' timeout' : ' done'
896
+ )
897
+ return resolve(xy)
898
+ }
899
+ clargs.verbose && this.logc('.\\c')
900
+ }, 5000)
901
+ })
902
+ }
903
+
904
+ this.verbose && this.log(
905
+ '%s: %s %s %s "%s"', clargs.light, light.manufacturername,
906
+ light.modelid, light.type, light.name
907
+ )
908
+ const response = {
909
+ manufacturername: light.manufacturername,
910
+ modelid: light.modelid,
911
+ type: light.type,
912
+ bri: light.state.bri != null
913
+ }
914
+ await this.client.put(clargs.light + '/state', { on: true })
915
+ if (light.state.ct != null) {
916
+ response.ct = {}
917
+ response.ct.min = await probeCt.call(this, 'cool', 1)
918
+ response.ct.max = await probeCt.call(this, 'warm', 1000)
919
+ }
920
+ if (light.state.xy != null) {
921
+ const zero = 0.0001
922
+ const one = 0.9961
923
+ response.xy = {}
924
+ response.xy.r = await probeXy.call(this, 'red', [one, zero])
925
+ response.xy.g = await probeXy.call(this, 'green', [zero, one])
926
+ response.xy.b = await probeXy.call(this, 'blue', [zero, zero])
927
+ }
928
+ await this.client.put(clargs.light + '/state', { on: light.state.on })
929
+ this.jsonFormatter = new homebridgeLib.JsonFormatter()
930
+ const json = this.jsonFormatter.stringify(response)
931
+ this.print(json)
932
+ }
933
+
934
+ // ===== BRIDGE/GATEWAY DISCOVERY ==============================================
935
+
936
+ async restart (...args) {
937
+ const parser = new homebridgeLib.CommandLineParser(packageJson)
938
+ const clargs = {}
939
+ parser
940
+ .help('h', 'help', help.restart)
941
+ .flag('v', 'verbose', () => { clargs.verbose = true })
942
+ .parse(...args)
943
+ if (this.client.isHue) {
944
+ const response = await this.client.put('/config', { reboot: true })
945
+ if (!response.success.reboot) {
946
+ return false
947
+ }
948
+ } else if (this.client.isDeconz) {
949
+ const response = await this.client.post('/config/restartapp')
950
+ if (!response.success.restartapp) {
951
+ return false
952
+ }
953
+ } else {
954
+ await this.fatal('restart: only supported for Hue bridge or deCONZ gateway')
955
+ }
956
+ clargs.verbose && this.log('restarting ...\\c')
957
+ return new Promise((resolve, reject) => {
958
+ let busy = false
959
+ const interval = setInterval(async () => {
960
+ try {
961
+ if (!busy) {
962
+ busy = true
963
+ const bridgeid = await this.client.get('/config/bridgeid')
964
+ if (bridgeid === this.bridgeid) {
965
+ clearInterval(interval)
966
+ clargs.verbose && this.logc(' done')
967
+ return resolve(true)
968
+ }
969
+ busy = false
970
+ }
971
+ } catch (error) {
972
+ busy = false
973
+ }
974
+ clargs.verbose && this.logc('.\\c')
975
+ }, 2500)
976
+ })
977
+ }
978
+ }
979
+
980
+ new Main().main()