homebridge-deconz 1.3.4 → 1.3.6-0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cli/deconz.js CHANGED
@@ -5,10 +5,9 @@
5
5
  //
6
6
  // Command line interface to deCONZ gateway.
7
7
 
8
- import { createRequire } from 'node:module'
9
-
10
8
  import { DeconzTool } from 'hb-deconz-tools/DeconzTool'
11
9
 
10
+ import { createRequire } from 'node:module'
12
11
  const require = createRequire(import.meta.url)
13
12
  const packageJson = require('../package.json')
14
13
 
package/cli/otau.js CHANGED
@@ -5,10 +5,9 @@
5
5
  //
6
6
  // Command line interface to deCONZ gateway.
7
7
 
8
- import { createRequire } from 'node:module'
9
-
10
8
  import { OtauTool } from 'hb-deconz-tools/OtauTool'
11
9
 
10
+ import { createRequire } from 'node:module'
12
11
  const require = createRequire(import.meta.url)
13
12
  const packageJson = require('../package.json')
14
13
 
package/cli/ui.js CHANGED
@@ -1,24 +1,21 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // homebridge-deconz/cli/deconz.js
3
+ // homebridge-deconz/cli/ui.js
4
4
  // Copyright © 2018-2026 Erik Baauw. All rights reserved.
5
5
  //
6
6
  // Command line interface to Homebridge deCONZ UI Server.
7
7
 
8
8
  import { readFile } from 'node:fs/promises'
9
- import { createRequire } from 'node:module'
10
9
 
11
- import { CommandLineParser } from 'hb-lib-tools/CommandLineParser'
12
- import { CommandLineTool } from 'hb-lib-tools/CommandLineTool'
10
+ import { CommandLineTool, CommandLineParser, UsageError, b, u } from 'hb-lib-tools/CommandLineTool'
13
11
  import { HttpClient } from 'hb-lib-tools/HttpClient'
14
12
  import { JsonFormatter } from 'hb-lib-tools/JsonFormatter'
15
- import { OptionParser } from 'hb-lib-tools/OptionParser'
13
+ import { patterns, toInt, toPath, toString } from 'hb-lib-tools/OptionParser'
16
14
 
15
+ import { createRequire } from 'node:module'
17
16
  const require = createRequire(import.meta.url)
18
- const packageJson = require('../package.json')
19
-
20
- const { b, u } = CommandLineTool
21
- const { UsageError } = CommandLineParser
17
+ const defaultPackageJson = require('../package.json')
18
+ // import defaultPackageJson from '../package.json' with { type: 'json' }
22
19
 
23
20
  const usage = {
24
21
  ui: `${b('ui')} [${b('-hVD')}] [${b('-U')} ${u('username')}] [${b('-G')} ${u('gateway')}] [${b('-t')} ${u('timeout')}] ${u('command')} [${u('argument')} ...]`,
@@ -181,38 +178,38 @@ Parameters:
181
178
  }
182
179
 
183
180
  class Main extends CommandLineTool {
184
- constructor () {
185
- super({ mode: 'command', debug: false })
181
+ constructor (packageJson) {
182
+ super(packageJson ?? defaultPackageJson, { mode: 'command', debug: false })
186
183
  this.usage = usage.deconz
187
184
  }
188
185
 
189
186
  parseArguments () {
190
- const parser = new CommandLineParser(packageJson)
187
+ const parser = new CommandLineParser(this)
191
188
  const clargs = {
192
189
  options: {
193
190
  timeout: 5
194
191
  }
195
192
  }
196
193
  parser
197
- .help('h', 'help', help.ui)
198
- .version('V', 'version')
199
- .debug('D', 'debug', this)
194
+ .helpFlag('h', 'help', help.ui)
195
+ .versionFlag('V', 'version')
196
+ .debugFlag('D', 'debug')
200
197
  .option('U', 'username', (value) => {
201
- clargs.username = OptionParser.toString(
202
- 'username', value, true, true
198
+ clargs.username = toString(
199
+ value, { key: 'username', nonEmpty: true, userInput: true }
203
200
  ).toUpperCase()
204
- if (!OptionParser.patterns.mac.test(clargs.username)) {
201
+ if (!patterns.mac.test(clargs.username)) {
205
202
  throw new UsageError(`${clargs.username}: invalid username`)
206
203
  }
207
204
  })
208
205
  .option('G', 'gateway', (value) => {
209
- clargs.gateway = OptionParser.toString(
210
- 'gateway', value, true, true
206
+ clargs.gateway = toString(
207
+ value, { key: 'gateway', nonEmpty: true, userInput: true }
211
208
  ).toUpperCase()
212
209
  })
213
210
  .option('t', 'timeout', (value) => {
214
- clargs.options.timeout = OptionParser.toInt(
215
- 'timeout', value, 1, 60, true
211
+ clargs.options.timeout = toInt(
212
+ value, { key: 'timeout', min: 1, max: 60, userInput: true }
216
213
  )
217
214
  })
218
215
  .parameter('command', (value) => {
@@ -342,7 +339,7 @@ class Main extends CommandLineTool {
342
339
  timeout: this.clargs.options.timeout
343
340
  })
344
341
  const response = await client.get('/ping')
345
- if (response.body !== 'pong') {
342
+ if (response.jsonBody !== 'pong') {
346
343
  throw new Error(`${host}: cannot ping`)
347
344
  }
348
345
  return client
@@ -382,12 +379,12 @@ class Main extends CommandLineTool {
382
379
  }
383
380
 
384
381
  async discover (platforms, ...args) {
385
- const parser = new CommandLineParser(packageJson)
382
+ const parser = new CommandLineParser(this)
386
383
  const clargs = {
387
384
  options: {}
388
385
  }
389
386
  parser
390
- .help('h', 'help', help.get)
387
+ .helpFlag('h', 'help', help.get)
391
388
  .flag('s', 'sortKeys', () => { clargs.options.sortKeys = true })
392
389
  .flag('n', 'noWhiteSpace', () => {
393
390
  clargs.options.noWhiteSpace = true
@@ -405,12 +402,12 @@ class Main extends CommandLineTool {
405
402
  }
406
403
 
407
404
  async get (...args) {
408
- const parser = new CommandLineParser(packageJson)
405
+ const parser = new CommandLineParser(this)
409
406
  const clargs = {
410
407
  options: {}
411
408
  }
412
409
  parser
413
- .help('h', 'help', help.get)
410
+ .helpFlag('h', 'help', help.get)
414
411
  .flag('s', 'sortKeys', () => { clargs.options.sortKeys = true })
415
412
  .flag('n', 'noWhiteSpace', () => {
416
413
  clargs.options.noWhiteSpace = true
@@ -423,7 +420,7 @@ class Main extends CommandLineTool {
423
420
  .flag('k', 'keysOnly', () => { clargs.options.keysOnly = true })
424
421
  .flag('v', 'valuesOnly', () => { clargs.options.valuesOnly = true })
425
422
  .parameter('resource', (value) => {
426
- clargs.resource = OptionParser.toPath('resource', value)
423
+ clargs.resource = toPath(value, { key: 'resource', userInput: true })
427
424
  })
428
425
  .parse(...args)
429
426
  if (clargs.resource === '/') {
@@ -434,23 +431,23 @@ class Main extends CommandLineTool {
434
431
  ) {
435
432
  clargs.resource = '/gateways/' + this.gid + clargs.resource
436
433
  }
437
- const { body } = await this.client.get(clargs.resource)
434
+ const { jsonBody } = await this.client.get(clargs.resource)
438
435
  const jsonFormatter = new JsonFormatter(clargs.options)
439
- this.print(jsonFormatter.stringify(body))
436
+ this.print(jsonFormatter.stringify(jsonBody))
440
437
  }
441
438
 
442
439
  async put (...args) {
443
- const parser = new CommandLineParser(packageJson)
440
+ const parser = new CommandLineParser(this)
444
441
  const clargs = {
445
442
  options: {}
446
443
  }
447
444
  parser
448
- .help('h', 'help', help.put)
445
+ .helpFlag('h', 'help', help.put)
449
446
  .parameter('resource', (value) => {
450
- clargs.resource = OptionParser.toPath('resource', value)
447
+ clargs.resource = toPath(value, { key: 'resource', userInput: true })
451
448
  })
452
449
  .parameter('body', (value) => {
453
- value = OptionParser.toString('body', value, true, true)
450
+ value = toString(value, { key: 'body', nonEmpty: true, userInput: true })
454
451
  try {
455
452
  clargs.body = JSON.parse(value)
456
453
  } catch (error) {
@@ -468,8 +465,11 @@ class Main extends CommandLineTool {
468
465
  }
469
466
  clargs.resource += '/settings'
470
467
  const jsonFormatter = new JsonFormatter(clargs.options)
471
- const { body } = await this.client.put(clargs.resource, clargs.body)
472
- this.print(jsonFormatter.stringify(body))
468
+ const { jsonBody } = await this.client.put(clargs.resource, clargs.body)
469
+ for (const error of jsonBody.errors || []) {
470
+ this.warn(error) // TODO: include request details.
471
+ }
472
+ this.print(jsonFormatter.stringify(jsonBody.success))
473
473
  }
474
474
  }
475
475
 
package/jsdoc.json CHANGED
@@ -11,7 +11,7 @@
11
11
  "cli",
12
12
  "node_modules/hb-deconz-tools/index.js",
13
13
  "node_modules/hb-deconz-tools/lib",
14
- "node_modules/hb-lib-tools/lib/HttpClient.js"
14
+ "node_modules/hb-lib-tools/dist/HttpClient.js"
15
15
  ]
16
16
  },
17
17
  "opts": {
@@ -3,7 +3,7 @@
3
3
  //
4
4
  // Homebridge plugin for deCONZ.
5
5
 
6
- import { OptionParser } from 'homebridge-lib/OptionParser'
6
+ import { toInstance, toInt, toObject, toString } from 'homebridge-lib/OptionParser'
7
7
 
8
8
  import { ApiClient } from 'hb-deconz-tools/ApiClient'
9
9
 
@@ -13,7 +13,6 @@ import '../DeconzAccessory/Gateway.js'
13
13
  import { DeconzService } from '../DeconzService/index.js'
14
14
  import '../DeconzService/Button.js'
15
15
 
16
- const { toInstance, toInt, toObject, toString } = OptionParser
17
16
  const { buttonEvent } = ApiClient
18
17
  const { SINGLE, DOUBLE, LONG } = DeconzService.Button
19
18
  const rtypes = ['lights', 'sensors', 'groups', 'alarmsystems']
@@ -91,7 +90,7 @@ class Resource {
91
90
  * @return {object} The MultiCLIP `id`, `endpoint`, and `cluster`.
92
91
  */
93
92
  static parseClipId (uniqueid) {
94
- toString('uniqueid', uniqueid, true)
93
+ toString(uniqueid, { key: 'uniqueid', nonEmpty: true })
95
94
  const a = patterns.clipId.exec(uniqueid.replace(/:/g, '').toUpperCase())
96
95
  return {
97
96
  id: a?.[1],
@@ -124,12 +123,12 @@ class Resource {
124
123
  * @param {object} body - The body of the resource.
125
124
  */
126
125
  constructor (gateway, rtype, rid, body) {
127
- toInstance('gateway', gateway, DeconzAccessory.Gateway)
126
+ toInstance(gateway, { key: 'gateway', class: DeconzAccessory.Gateway })
128
127
 
129
128
  /** The resource type of the resource: `groups`, `lights`, or `sensors`.
130
129
  * @type {string}
131
130
  */
132
- this.rtype = toString('rtype', rtype, true)
131
+ this.rtype = toString(rtype, { key: 'rtype', nonEmpty: true })
133
132
  if (!(rtypes.includes(rtype))) {
134
133
  throw new RangeError(`rtype: ${rtype}: not a valid resource type`)
135
134
  }
@@ -137,20 +136,20 @@ class Resource {
137
136
  /** The resource ID of the resource.
138
137
  * @type {integer}
139
138
  */
140
- this.rid = toInt('rid', rid)
139
+ this.rid = toInt(rid, { key: 'rid' })
141
140
 
142
141
  /** The body of the resource.
143
142
  * @type {object}
144
143
  */
145
- this.body = toObject('body', body)
146
- toString('body.name', body.name, true)
144
+ this.body = toObject(body, { key: 'body' })
145
+ toString(body.name, { key: 'body.name', nonEmpty: true })
147
146
  body.name = body.name.replace(/[^\p{L}\p{N} ']/ug, ' ')
148
147
  .replace(/^[ ']*/, '')
149
148
  .replace(/[ ']*$/, '')
150
149
  if (rtype === 'alarmsystems') {
151
150
  body.type = 'AlarmSystem'
152
151
  }
153
- toString('body.type', body.type, true)
152
+ toString(body.type, { key: 'body.type', nonEmpty: true })
154
153
 
155
154
  let realDevice = false
156
155
 
@@ -5,7 +5,7 @@
5
5
 
6
6
  import { timeout } from 'homebridge-lib'
7
7
  import { AccessoryDelegate } from 'homebridge-lib/AccessoryDelegate'
8
- import { OptionParser } from 'homebridge-lib/OptionParser'
8
+ import { OptionParser, toBool } from 'homebridge-lib/OptionParser'
9
9
 
10
10
  import { ApiClient } from 'hb-deconz-tools/ApiClient'
11
11
  import { ApiError } from 'hb-deconz-tools/ApiError'
@@ -882,12 +882,17 @@ class Gateway extends AccessoryDelegate {
882
882
  if (path.length === 0) {
883
883
  return { status: 405 } // Method Not Allowed
884
884
  }
885
+ const responseBody = {
886
+ success: {},
887
+ errors: []
888
+ }
885
889
  if (path[0] === 'settings') {
886
890
  const settings = {}
887
891
  const optionParser = new OptionParser(settings, true)
888
892
  optionParser
889
893
  .on('userInputError', (error) => {
890
- this.warn(error)
894
+ this.warn('ui error: %s', error)
895
+ responseBody.errors.push(error.message)
891
896
  })
892
897
  .boolKey('autoExpose')
893
898
  .boolKey('expose')
@@ -904,12 +909,11 @@ class Gateway extends AccessoryDelegate {
904
909
  }
905
910
  optionParser.parse(body)
906
911
 
907
- const responseBody = {}
908
912
  for (const key in settings) {
909
913
  switch (key) {
910
914
  case 'brightnessAdjustment':
911
915
  this.values[key] = settings[key] / 100
912
- responseBody[key] = this.values[key]
916
+ responseBody.success[key] = this.values[key]
913
917
  break
914
918
  case 'autoExpose':
915
919
  case 'expose':
@@ -921,7 +925,7 @@ class Gateway extends AccessoryDelegate {
921
925
  case 'search':
922
926
  case 'unlock':
923
927
  this.values[key] = settings[key]
924
- responseBody[key] = this.values[key]
928
+ responseBody.success[key] = this.values[key]
925
929
  break
926
930
  default:
927
931
  break
@@ -951,10 +955,16 @@ class Gateway extends AccessoryDelegate {
951
955
  return { status: 404 } // Not Found
952
956
  }
953
957
  if (body.expose != null) {
954
- this.exposeDevice(id, body.expose)
955
- return { status: 200, body: { expose: body.expose } }
958
+ try {
959
+ toBool(body.expose, { key: 'expose', userInput: true })
960
+ this.exposeDevice(id, body.expose)
961
+ responseBody.success.expose = body.expose
962
+ } catch (error) {
963
+ this.warn('ui error: %s', error)
964
+ responseBody.errors.push(error.message)
965
+ }
956
966
  }
957
- return { status: 200 }
967
+ return { status: 200, body: responseBody }
958
968
  }
959
969
  }
960
970
  return { status: 403 } // Forbidden
@@ -4,7 +4,7 @@
4
4
  // Homebridge plugin for deCONZ.
5
5
 
6
6
  import { AccessoryDelegate } from 'homebridge-lib/AccessoryDelegate'
7
- import { OptionParser } from 'homebridge-lib/OptionParser'
7
+ import { toBool, toInt, toString, UserInputError } from 'homebridge-lib/OptionParser'
8
8
 
9
9
  import { ApiClient } from 'hb-deconz-tools/ApiClient'
10
10
 
@@ -262,21 +262,25 @@ class DeconzAccessory extends AccessoryDelegate {
262
262
  return { status: 200, body }
263
263
  }
264
264
 
265
+ // TODO: userInputErrors are logged in Homebridge log, but not returned in the UI response.
265
266
  onUiPut (body) {
266
267
  let reExpose = false
267
- const responseBody = {}
268
+ const responseBody = {
269
+ success: {},
270
+ errors: []
271
+ }
268
272
  for (const key in body) {
269
273
  try {
270
274
  let value
271
275
  switch (key) {
272
276
  case 'expose':
273
- value = OptionParser.toBool(key, body[key])
277
+ value = toBool(body[key], { key, userInput: true })
274
278
  if (value) {
275
279
  reExpose = true
276
280
  } else {
277
281
  this.gateway.exposeDevice(this.id, value)
278
282
  }
279
- responseBody[key] = value
283
+ responseBody.success[key] = value
280
284
  continue
281
285
  // Settings for the primary service.
282
286
  case 'anyOn':
@@ -284,52 +288,52 @@ class DeconzAccessory extends AccessoryDelegate {
284
288
  case 'exposeScenes':
285
289
  case 'venetianBlind':
286
290
  if (this.service.values[key] != null) {
287
- value = OptionParser.toBool(key, body[key])
291
+ value = toBool(body[key], { key, userInput: true })
288
292
  this.service.values[key] = value
289
293
  reExpose = true
290
- responseBody[key] = value
294
+ responseBody.success[key] = value
291
295
  continue
292
296
  }
293
297
  break
294
298
  case 'logLevel':
295
- value = OptionParser.toInt(key, body[key], 0, 3)
299
+ value = toInt(body[key], { key, min: 0, max: 3, userInput: true })
296
300
  this.values[key] = value
297
- responseBody[key] = value
301
+ responseBody.success[key] = value
298
302
  continue
299
303
  case 'lowBatteryThreshold':
300
304
  if (this.servicesByServiceName.Battery?.[0] != null) {
301
- value = OptionParser.toInt(key, body[key], 10, 100)
305
+ value = toInt(body[key], { key, min: 10, max: 100, userInput: true })
302
306
  this.servicesByServiceName.Battery[0].values[key] = value
303
- responseBody[key] = value
307
+ responseBody.success[key] = value
304
308
  continue
305
309
  }
306
310
  break
307
311
  case 'pin':
308
312
  if (this.service.values[key] != null) {
309
- value = OptionParser.toString(key, body[key])
313
+ value = toString(body[key], { key, userInput: true })
310
314
  this.service.values[key] = value
311
- responseBody[key] = value
315
+ responseBody.success[key] = value
312
316
  continue
313
317
  }
314
318
  break
315
319
  case 'serviceName':
316
320
  if (this.values.serviceName != null) {
317
- value = OptionParser.toString(key, body[key])
318
- if (['Light', 'Outlet', 'Switch', 'Valve'].includes(value) == null) {
319
- throw new Error(`${value}: illegal serviceName`)
321
+ value = toString(body[key], { key, nonEmpty: true, userInput: true })
322
+ if (!['Light', 'Outlet', 'Switch', 'Valve'].includes(value)) {
323
+ throw new UserInputError(`${value}: not a valid serviceName`)
320
324
  }
321
325
  this.values.serviceName = value
322
326
  reExpose = true
323
- responseBody[key] = value
327
+ responseBody.success[key] = value
324
328
  continue
325
329
  }
326
330
  break
327
331
  case 'useExternalTemperature':
328
332
  case 'wallSwitch':
329
333
  if (this.service.values[key] != null) {
330
- value = OptionParser.toBool(key, body[key])
334
+ value = toBool(body[key], { key, userInput: true })
331
335
  this.service.values[key] = value
332
- responseBody[key] = value
336
+ responseBody.success[key] = value
333
337
  continue
334
338
  }
335
339
  break
@@ -337,7 +341,11 @@ class DeconzAccessory extends AccessoryDelegate {
337
341
  break
338
342
  }
339
343
  this.warn('ui error: %s: invalid key', key)
340
- } catch (error) { this.warn('ui error: %s', error) }
344
+ responseBody.errors.push(`${key}: invalid key`)
345
+ } catch (error) {
346
+ this.warn('ui error: %s', error)
347
+ responseBody.errors.push(error.message)
348
+ }
341
349
  }
342
350
  if (reExpose) {
343
351
  this.gateway.reExposeAccessory(this.id)
@@ -5,14 +5,12 @@
5
5
 
6
6
  import { timeout } from 'homebridge-lib'
7
7
  import { AdaptiveLighting } from 'homebridge-lib/AdaptiveLighting'
8
- import { Colour } from 'homebridge-lib/Colour'
8
+ import { defaultGamut, xyToHsv, hsvToXy, ctToXy, hsvToRgb } from 'homebridge-lib/Colour'
9
9
  import { ServiceDelegate } from 'homebridge-lib/ServiceDelegate'
10
10
 
11
11
  import { DeconzService } from '../DeconzService/index.js'
12
12
  import '../DeconzService/LightsResource.js'
13
13
 
14
- const { defaultGamut, xyToHsv, hsvToXy, ctToXy, hsvToRgb } = Colour
15
-
16
14
  class Light extends DeconzService.LightsResource {
17
15
  constructor (accessory, resource, params = {}) {
18
16
  params.Service = accessory.Services.hap.Lightbulb
package/package.json CHANGED
@@ -7,7 +7,7 @@
7
7
  "ebaauw"
8
8
  ],
9
9
  "license": "Apache-2.0",
10
- "version": "1.3.4",
10
+ "version": "1.3.6-0",
11
11
  "keywords": [
12
12
  "homebridge-plugin",
13
13
  "homekit",
@@ -27,12 +27,12 @@
27
27
  },
28
28
  "engines": {
29
29
  "deCONZ": "2.33.2",
30
- "homebridge": "^2.2.1",
31
- "node": "^24||^22"
30
+ "homebridge": "^2.4.0",
31
+ "node": "^26||^24||^22"
32
32
  },
33
33
  "dependencies": {
34
- "hb-deconz-tools": "~3.0.6",
35
- "homebridge-lib": "~8.1.4"
34
+ "hb-deconz-tools": "~3.0.8-0",
35
+ "homebridge-lib": "~9.0.0-0"
36
36
  },
37
37
  "scripts": {
38
38
  "prepare": "standard && rm -rf out && jsdoc -c jsdoc.json",