homebridge-deconz 0.0.15 → 0.0.18

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.
@@ -0,0 +1,506 @@
1
+ <!--
2
+ homebridge-deconz/homebridge-ui/public/index.html
3
+
4
+ Homebridge plug-in for deCONZ.
5
+ Copyright © 2022 Erik Baauw. All rights reserved.
6
+ -->
7
+
8
+ <link rel="stylesheet" href="style.css">
9
+ <p align="center">
10
+ <a href="https://github.com/ebaauw/homebridge-deconz/wiki/Configuration" target="_blank">
11
+ <img src="homebridge-deconz.png" height="200px">
12
+ </a>
13
+ </p>
14
+
15
+ <script>
16
+
17
+ async function showFormPluginConfig () {
18
+ homebridge.showSpinner()
19
+ const pluginConfig = await homebridge.getPluginConfig()
20
+ console.log('pluginConfig: %o', pluginConfig)
21
+ // const pluginConfigSchema = await homebridge.getPluginConfigSchema()
22
+ // console.log('pluginConfigSchema: %o', pluginConfigSchema)
23
+ // const cachedAccessories = await homebridge.getCachedAccessories()
24
+ // console.log('cachedAccessories: %o', cachedAccessories)
25
+ // const discoveredGateways = await homebridge.request('discover')
26
+ // console.log('discovered gateways: %o', discoveredGateways)
27
+ for (const config of pluginConfig) {
28
+ if (config._bridge != null) {
29
+ const cachedAccessories = await homebridge.request('cachedAccessories', {
30
+ username: config._bridge.username
31
+ })
32
+ console.log('%s: cachedAccessories: %o', config.name, cachedAccessories)
33
+ const cachedGateways = cachedAccessories.filter((accessory) => {
34
+ return accessory.plugin === 'homebridge-deconz' &&
35
+ accessory.context != null &&
36
+ accessory.context.className === 'Gateway'
37
+ })
38
+ const result = {}
39
+ for (const gateway of cachedGateways) {
40
+ if (gateway.context.uiPort == null) {
41
+ continue
42
+ }
43
+ const pong = await homebridge.request(
44
+ 'get', { uiPort: gateway.context.uiPort, path: '/ping' }
45
+ )
46
+ if (pong === 'pong') {
47
+ result[gateway.context.host] = gateway.context
48
+ }
49
+ }
50
+ const gateways = Object.keys(result).sort()
51
+ console.log('%s: gateways: %j',config.name, gateways)
52
+ }
53
+ }
54
+ homebridge.hideSpinner()
55
+
56
+ const form = homebridge.createForm(
57
+ {
58
+ schema: {
59
+ type: 'object',
60
+ properties: {
61
+ config: {
62
+ title: 'Gateways',
63
+ description: 'Configure a child bridge per deCONZ gateway. See <a href="https://github.com/ebaauw/homebridge-deconz/wiki/Configuration" target="_blank">wiki</a> for details.',
64
+ type: 'array',
65
+ disabled: true,
66
+ items: {
67
+ type: 'object',
68
+ properties: {
69
+ host: {
70
+ description: 'Gateway hostname and port.',
71
+ default: 'localhost:80',
72
+ type: 'string',
73
+ required: true
74
+ },
75
+ name: {
76
+ description: 'Homebridge log plugin name.',
77
+ default: 'deCONZ',
78
+ type: 'string'
79
+ },
80
+ _bridge: {
81
+ type: 'object',
82
+ required: true,
83
+ properties: {
84
+ name: {
85
+ type: 'string',
86
+ required: true
87
+ },
88
+ username: {
89
+ type: 'string',
90
+ pattern: '^([A-F0-9]{2}:){5}[A-F0-9]{2}$',
91
+ placeholder: 'AA:BB:CC:DD:EE:FF',
92
+ required: true
93
+ },
94
+ port: {
95
+ type: 'integer',
96
+ minimum: 1025,
97
+ // maximum: 65535,
98
+ required: true
99
+ },
100
+ manufacturer: {
101
+ type: 'string',
102
+ enabled: false,
103
+ required: true
104
+ },
105
+ model: {
106
+ type: 'string',
107
+ required: true
108
+ }
109
+ }
110
+ }
111
+ }
112
+ }
113
+ }
114
+ }
115
+ },
116
+ // layout: null
117
+ layout: [
118
+ {
119
+ type: 'tabarray',
120
+ title: '{{ value.name }}',
121
+ items: [
122
+ {
123
+ type: 'fieldset',
124
+ title: 'Gateway Settings',
125
+ key: 'config[]',
126
+ items: [
127
+ {
128
+ type: 'flex',
129
+ 'flex-flow': 'row',
130
+ items: [
131
+ 'config[].host',
132
+ 'config[].name',
133
+ ]
134
+ }
135
+ ]
136
+ },
137
+ {
138
+ type: 'flex',
139
+ 'flex-flow': 'row',
140
+ key: 'config[]',
141
+ items: [
142
+ {
143
+ type: 'button',
144
+ title: 'Connect',
145
+ key: 'config[].connect'
146
+ },
147
+ {
148
+ type: 'button',
149
+ title: 'Get API Key',
150
+ key: 'config[].getApiKey'
151
+ },
152
+ {
153
+ type: 'submit',
154
+ title: 'Configure',
155
+ key: 'config[].configure'
156
+ }
157
+ ]
158
+ },
159
+ {
160
+ type: 'fieldset',
161
+ key: 'config[]._bridge',
162
+ // expandable: true,
163
+ title: 'Child Bridge Accessory Settings',
164
+ items: [
165
+ {
166
+ type: 'flex',
167
+ 'flex-flow': 'row',
168
+ items: [
169
+ 'config[]._bridge.username',
170
+ 'config[]._bridge.port'
171
+ ]
172
+ },
173
+ 'config[]._bridge.name',
174
+ {
175
+ type: 'flex',
176
+ 'flex-flow': 'row',
177
+ items: [
178
+ 'config[]._bridge.manufacturer',
179
+ 'config[]._bridge.model'
180
+ ]
181
+ }
182
+ ]
183
+ }
184
+ ]
185
+ }
186
+ ]
187
+ }, {
188
+ config: pluginConfig,
189
+ },
190
+ 'Gateway Settings',
191
+ 'Homebridge Settings'
192
+ )
193
+ form.onChange(async (form) => {
194
+ console.log('change: %o', form)
195
+ })
196
+ form.onSubmit(async (form) => {
197
+ console.log('submit: %o', form)
198
+ })
199
+ form.onCancel(async (form) => {
200
+ console.log('cancel: %o', form)
201
+ })
202
+
203
+ }
204
+
205
+ async function showFormGateways (gateway) {
206
+ homebridge.showSpinner()
207
+ const cachedAccessories = await homebridge.getCachedAccessories()
208
+ const cachedGateways = cachedAccessories.filter((accessory) => {
209
+ return accessory.plugin === 'homebridge-deconz' &&
210
+ accessory.context != null &&
211
+ accessory.context.className === 'Gateway'
212
+ })
213
+ const result = {}
214
+ for (const gateway of cachedGateways) {
215
+ if (gateway.context.uiPort == null) {
216
+ continue
217
+ }
218
+ const pong = await homebridge.request(
219
+ 'get', { uiPort: gateway.context.uiPort, path: '/ping' }
220
+ )
221
+ if (pong === 'pong') {
222
+ result[gateway.context.host] = gateway.context
223
+ }
224
+ }
225
+ const gateways = Object.keys(result).sort()
226
+ homebridge.hideSpinner()
227
+ if (gateways.length === 0) {
228
+ homebridge.showSchemaForm()
229
+ return
230
+ }
231
+ // const form = homebridge.createForm({
232
+ // schema: {
233
+ // type: 'object',
234
+ // properties: {
235
+ // gateway: {
236
+ // title: 'Connected Gateways',
237
+ // type: 'string',
238
+ // oneOf: gateways.map((name) => {
239
+ // const config = result[name].context.config
240
+ // return {
241
+ // title: `${name}: dresden elektronik ${config.modelid} gateway v${config.swversion} / ${config.devicename} ${config.bridgeid}`,
242
+ // enum: [name]
243
+ // }
244
+ // }),
245
+ // required: true
246
+ // }
247
+ // }
248
+ // },
249
+ // layout: null,
250
+ // form: null
251
+ // }, {
252
+ // gateway: gateway != null ? gateway : gateways[0]
253
+ // }, 'Gateway Settings', 'Homebridge Settings')
254
+ const form = homebridge.createForm({
255
+ footerDisplay: 'For a detailed description, see the [wiki](https://github.com/ebaauw/homebridge-deconz/wiki/Configuration).',
256
+ schema: {
257
+ type: 'object',
258
+ properties: {
259
+ name: {
260
+ description: 'Plugin name as displayed in the Homebridge log.',
261
+ type: 'string',
262
+ required: true,
263
+ default: 'deCONZ'
264
+ },
265
+ gateways: {
266
+ title: 'Gateways',
267
+ type: 'array',
268
+ disabled: true,
269
+ items: {
270
+ type: 'object',
271
+ properties: {
272
+ host: {
273
+ description: 'Hostname and port of the deCONZ gateway.',
274
+ type: 'string'
275
+ },
276
+ expose: {
277
+ description: 'Expose gateway to HomeKit.',
278
+ type: 'boolean'
279
+ }
280
+ }
281
+ }
282
+
283
+ }
284
+ }
285
+ } //,
286
+ // layout: [
287
+ // 'name',
288
+ // {
289
+ // key: 'gateways',
290
+ // type: 'array',
291
+ // buttonText: 'Add Gateway',
292
+ // items: [
293
+ // {
294
+ // type: 'section',
295
+ // htmlClass: 'row',
296
+ // items: [
297
+ // {
298
+ // type: 'section',
299
+ // htmlClass: 'col',
300
+ // items: [
301
+ // 'gateways[].host'
302
+ // ]
303
+ // },
304
+ // {
305
+ // type: 'section',
306
+ // htmlClass: 'col',
307
+ // items: [
308
+ // {
309
+ // key: 'gateways[].expose',
310
+ // disabled: true
311
+ // }
312
+ // ]
313
+ // }
314
+ // ]
315
+ // }
316
+ // ]
317
+ // }
318
+ // ]
319
+ }, {
320
+
321
+ }, 'Gateway Settings', 'Homebridge Settings')
322
+ form.onChange(async (form) => {
323
+ // showFormGatewaySettings(result[form.gateway])
324
+ })
325
+ form.onSubmit(async (form) => {
326
+ await showFormGatewaySettings(result[form.gateways])
327
+ })
328
+ form.onCancel(() => { homebridge.showSchemaForm() })
329
+ }
330
+
331
+ async function showFormGatewaySettings (gateway, device) {
332
+ homebridge.showSpinner()
333
+ const data = await homebridge.request(
334
+ 'get', {
335
+ uiPort: gateway.uiPort,
336
+ path: '/gateways/' + gateway.id
337
+ }
338
+ )
339
+ const values = {}
340
+ for (const rtype in data.deviceByRidByRtype) {
341
+ values[rtype] = []
342
+ for (const rid in data.deviceByRidByRtype[rtype]) {
343
+ const device = data.deviceByRidByRtype[rtype][rid]
344
+ values[rtype].push({
345
+ title: ['', rtype, rid].join('/') + ': ' +
346
+ device.resourceBySubtype[device.primary].body.name,
347
+ enum: [device.id]
348
+ })
349
+ }
350
+ }
351
+ data.lightsDevice = values.lights[0].enum[0]
352
+ data.sensorsDevice = values.sensors[0].enum[0]
353
+ data.groupsDevice = values.groups[0].enum[0]
354
+ homebridge.hideSpinner()
355
+ const form = homebridge.createForm({
356
+ schema: {
357
+ type: 'object',
358
+ properties: {
359
+ expose: {
360
+ title: 'Expose',
361
+ type: 'boolean'
362
+ },
363
+ lights: {
364
+ title: 'Lights',
365
+ type: 'boolean',
366
+ },
367
+ sensors: {
368
+ title: 'Sensors',
369
+ type: 'boolean',
370
+ },
371
+ groups: {
372
+ title: 'Groups',
373
+ type: 'boolean',
374
+ },
375
+ schedules: {
376
+ title: 'Schedules',
377
+ type: 'boolean',
378
+ },
379
+ logLevel: {
380
+ title: 'Log Level',
381
+ type: 'string',
382
+ oneOf: ['0', '1', '2', '3'].map((level) => { return { title: level, enum: [level] } }),
383
+ required: true,
384
+ condition: {
385
+ functionBody: 'return model.expose'
386
+ }
387
+ },
388
+ lightsDevice: {
389
+ title: 'Device',
390
+ type: 'string',
391
+ oneOf: values.lights,
392
+ required: true
393
+ },
394
+ sensorsDevice: {
395
+ title: 'Device',
396
+ type: 'string',
397
+ oneOf: values.sensors,
398
+ required: true
399
+ },
400
+ groupsDevice: {
401
+ title: 'Device',
402
+ type: 'string',
403
+ oneOf: values.groups,
404
+ required: true
405
+ }
406
+ }
407
+ },
408
+ layout: [
409
+ {
410
+ type: 'fieldset',
411
+ title: `${gateway.context.host} Gateway Settings`
412
+ },
413
+ 'expose',
414
+ 'logLevel',
415
+ {
416
+ type: 'flex',
417
+ 'flex-flow': 'row',
418
+ title: 'Automatically Expose New',
419
+ items: [
420
+ 'lights',
421
+ 'sensors',
422
+ 'groups',
423
+ 'schedules'
424
+ ],
425
+ condition: {
426
+ functionBody: 'return model.expose'
427
+ }
428
+ },
429
+ {
430
+ type: 'fieldset',
431
+ items: [
432
+ {
433
+ type: 'tabs',
434
+ tabs: [
435
+ {
436
+ title: 'Lights',
437
+ items: [
438
+ 'lightsDevice'
439
+ ]
440
+ },
441
+ {
442
+ title: 'Sensors',
443
+ items: [
444
+ 'sensorsDevice'
445
+ ]
446
+ },
447
+ {
448
+ title: 'Groups',
449
+ items: [
450
+ 'groupsDevice'
451
+ ]
452
+ }
453
+ ]
454
+ }
455
+ ],
456
+ condition: {
457
+ functionBody: 'return model.expose'
458
+ }
459
+ }
460
+ ]
461
+ }, data, 'Device Settings', 'Done')
462
+ form.onChange((form) => {})
463
+ form.onSubmit((form) => {
464
+ showFormDeviceSettings(gateway, form.lightsDevice)
465
+ })
466
+ form.onCancel((form) => {
467
+ showFormGateways(gateway.context.host)
468
+ })
469
+ }
470
+
471
+ async function showFormDeviceSettings (gateway, device) {
472
+ homebridge.showSpinner()
473
+ homebridge.hideSpinner()
474
+ const form = homebridge.createForm({
475
+ schema: {
476
+ type: 'object',
477
+ properties: {
478
+ gateway: {
479
+ type: 'string'
480
+ },
481
+ device: {
482
+ type: 'string'
483
+ }
484
+ }
485
+ }
486
+ }, {
487
+ gateway: gateway.context.host,
488
+ device: device
489
+ }, 'OK', 'Cancel')
490
+ form.onChange((form) => {})
491
+ form.onSubmit((form) => {
492
+ showFormGatewaySettings(gateway)
493
+ })
494
+ form.onCancel((form) => {
495
+ showFormGatewaySettings(gateway)
496
+ })
497
+ }
498
+
499
+ (async () => {
500
+ try {
501
+ await showFormPluginConfig()
502
+ } catch (error) {
503
+ console.error(error)
504
+ }
505
+ })()
506
+ </script>
@@ -81,9 +81,8 @@ class ApiClient extends homebridgeLib.HttpClient {
81
81
  * @return {integer} lux - The value in lux.
82
82
  */
83
83
  static lightLevelToLux (v) {
84
- let lux = v ? Math.pow(10, (v - 1) / 10000) : 0.0001
85
- lux = Math.round(lux * 10000) / 10000
86
- return Math.max(0.0001, Math.min(lux, 100000))
84
+ v = Math.max(0, Math.min(v, 60001))
85
+ return v ? Math.round(Math.pow(10, (v - 1) / 10000) * 10) / 10 : 0.0001
87
86
  }
88
87
 
89
88
  /** Create a new instance of a Deconz.Client.
@@ -149,7 +148,7 @@ class ApiClient extends homebridgeLib.HttpClient {
149
148
  maxSockets: _options.maxSockets,
150
149
  path: '/api',
151
150
  timeout: _options.timeout,
152
- validStatusCodes: [200, 400, 403, 404]
151
+ validStatusCodes: [200, 400, 403] //, 404]
153
152
  }
154
153
  if (_options.phoscon) {
155
154
  // options.headers = { Accept: 'application/vnd.ddel.v1' }
@@ -44,7 +44,7 @@ class Discovery extends events.EventEmitter {
44
44
  */
45
45
  async config (host) {
46
46
  const client = new homebridgeLib.HttpClient({
47
- host: host,
47
+ host,
48
48
  json: true,
49
49
  path: '/api',
50
50
  timeout: this._options.timeout
@@ -78,12 +78,15 @@ class Discovery extends events.EventEmitter {
78
78
  if (
79
79
  body != null && typeof body === 'object' &&
80
80
  typeof body.apiversion === 'string' &&
81
- /00212E[0-9A-Fa-f]{10}/.test(body.bridgeid) &&
81
+ /[0-9A-Fa-f]{16}/.test(body.bridgeid) &&
82
82
  typeof body.devicename === 'string' &&
83
83
  typeof body.name === 'string' &&
84
84
  typeof body.swversion === 'string'
85
85
  ) {
86
- return body
86
+ if (body.bridgeid.startsWith('00212E')) {
87
+ return body
88
+ }
89
+ throw new Error(`${body.bridgeid}: not a RaspBee/ConBee mac address`)
87
90
  }
88
91
  throw new Error('not a deCONZ gateway')
89
92
  }
@@ -96,7 +99,7 @@ class Discovery extends events.EventEmitter {
96
99
  */
97
100
  async description (host) {
98
101
  const options = {
99
- host: host,
102
+ host,
100
103
  timeout: this._options.timeout
101
104
  }
102
105
  const client = new homebridgeLib.HttpClient(options)
@@ -21,8 +21,9 @@ const sensorsPrios = [
21
21
  'Power',
22
22
  'Consumption',
23
23
  'Temperature',
24
- 'Presence',
24
+ 'Motion',
25
25
  'OpenClose',
26
+ 'AirPurifier',
26
27
  'Thermostat'
27
28
  ]
28
29
 
@@ -256,7 +257,7 @@ class Resource {
256
257
  get prio () {
257
258
  if (this.rtype === 'groups') return -1
258
259
  if (this.rtype === 'lights') return this.endpoint
259
- return sensorsPrios.indexOf(this.type)
260
+ return sensorsPrios.indexOf(this.serviceName)
260
261
  }
261
262
 
262
263
  /** The resource path of the resource, e.g. `/lights/1`.
@@ -270,7 +271,7 @@ class Resource {
270
271
  * corresponding HomeKit service, or `null` for unsupported and unknown
271
272
  * resources.
272
273
  *
273
- * This is derived from the resource type and`type` in the resource body.
274
+ * This is derived from the resource type and `type` in the resource body.
274
275
  * @type {string}
275
276
  */
276
277
  get serviceName () {
@@ -305,6 +306,8 @@ class Resource {
305
306
  }
306
307
  } else { // (this.rtype === 'sensors')
307
308
  switch (this.body.type) {
309
+ case 'CLIPAirPurifier':
310
+ case 'ZHAAirPurifier': return 'AirPurifier'
308
311
  case 'ZHAAirQuality':
309
312
  case 'CLIPAirQuality': return 'AirQuality'
310
313
  case 'ZHAAlarm':
@@ -336,6 +339,7 @@ class Resource {
336
339
  case 'CLIPPresence': return 'Motion'
337
340
  case 'ZHAPressure':
338
341
  case 'CLIPPressure': return 'AirPressure'
342
+ case 'ZHARelativeRotary': return 'Switch'
339
343
  case 'ZHASpectral': return ''
340
344
  case 'ZGPSwitch':
341
345
  case 'ZHASwitch':
@@ -879,6 +883,18 @@ class Resource {
879
883
  break
880
884
  }
881
885
  break
886
+ case 'RDM002': // Hue tap dial switch
887
+ dots = true
888
+ if (this.endpoint === '01') {
889
+ buttons.push([1, '1', SINGLE | LONG, true])
890
+ buttons.push([2, '2', SINGLE | LONG, true])
891
+ buttons.push([3, '3', SINGLE | LONG, true])
892
+ buttons.push([4, '4', SINGLE | LONG, true])
893
+ } else if (this.endpoint === '14') {
894
+ buttons.push([5, 'Right Turn', SINGLE])
895
+ buttons.push([6, 'Left Turn', SINGLE])
896
+ }
897
+ break
882
898
  case 'ROM001': // Hue smart button
883
899
  buttons.push([1, 'Button', SINGLE | LONG, true])
884
900
  break
@@ -932,6 +948,7 @@ class Resource {
932
948
  break
933
949
  case 'Sunricher':
934
950
  switch (this.model) {
951
+ case 'ZG2833K4_EU06': // Sunricher 4-button remote
935
952
  case 'ZG2833K8_EU05': // Sunricher 8-button remote, see #529.
936
953
  if (this.endpoint === '01') {
937
954
  buttons.push([1, 'On 1', SINGLE | LONG])
@@ -0,0 +1,38 @@
1
+ // homebridge-deconz/lib/DeconzAccessory/Thermostat.js
2
+ // Copyright © 2022 Erik Baauw. All rights reserved.
3
+ //
4
+ // Homebridge plugin for deCONZ.
5
+
6
+ 'use strict'
7
+
8
+ const DeconzAccessory = require('../DeconzAccessory')
9
+
10
+ class AirPurifier extends DeconzAccessory {
11
+ /** Instantiate a delegate for an accessory corresponding to a device.
12
+ * @param {DeconzAccessory.Gateway} gateway - The gateway.
13
+ * @param {Deconz.Device} device - The device.
14
+ */
15
+ constructor (gateway, device, settings = {}) {
16
+ super(gateway, device, gateway.Accessory.Categories.AIR_PURIFIER)
17
+ this.identify()
18
+
19
+ this.service = this.createService(device.resource, { primaryService: true })
20
+
21
+ for (const subtype in device.resourceBySubtype) {
22
+ const resource = device.resourceBySubtype[subtype]
23
+ if (subtype === device.primary) {
24
+ continue
25
+ }
26
+ this.createService(resource)
27
+ }
28
+
29
+ this.createSettingsService()
30
+
31
+ setImmediate(() => {
32
+ this.debug('initialised')
33
+ this.emit('initialised')
34
+ })
35
+ }
36
+ }
37
+
38
+ module.exports = AirPurifier