homebridge-deconz 0.0.17 → 0.0.21

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>
@@ -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
  }
@@ -339,6 +339,7 @@ class Resource {
339
339
  case 'CLIPPresence': return 'Motion'
340
340
  case 'ZHAPressure':
341
341
  case 'CLIPPressure': return 'AirPressure'
342
+ case 'ZHARelativeRotary': return 'Switch'
342
343
  case 'ZHASpectral': return ''
343
344
  case 'ZGPSwitch':
344
345
  case 'ZHASwitch':
@@ -882,6 +883,18 @@ class Resource {
882
883
  break
883
884
  }
884
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
885
898
  case 'ROM001': // Hue smart button
886
899
  buttons.push([1, 'Button', SINGLE | LONG, true])
887
900
  break
@@ -935,6 +948,7 @@ class Resource {
935
948
  break
936
949
  case 'Sunricher':
937
950
  switch (this.model) {
951
+ case 'ZG2833K4_EU06': // Sunricher 4-button remote
938
952
  case 'ZG2833K8_EU05': // Sunricher 8-button remote, see #529.
939
953
  if (this.endpoint === '01') {
940
954
  buttons.push([1, 'On 1', SINGLE | LONG])
@@ -70,7 +70,10 @@ class Gateway extends homebridgeLib.AccessoryDelegate {
70
70
  // end migration
71
71
  }
72
72
  if (this.context.fullState != null) {
73
- this.analyseFullState(this.context.fullState, { analyseOnly: true })
73
+ this.analyseFullState(this.context.fullState, {
74
+ analyseOnly: true,
75
+ logUnsupported: true
76
+ })
74
77
  }
75
78
 
76
79
  this.addPropertyDelegate({
@@ -774,7 +777,7 @@ class Gateway extends homebridgeLib.AccessoryDelegate {
774
777
  * @param {Object} fullState - The gateway full state, as returned by
775
778
  * {@link DeconzAccessory.Gateway#poll poll()}.
776
779
  * @param {Object} params - Parameters
777
- * @param {boolean} [params.logUnsupportedResources=false] - Issue debug
780
+ * @param {boolean} [params.logUnsupported=false] - Issue debug
778
781
  * messsages for unsupported resources.
779
782
  * @param {boolean} [params.analyseOnly=false]
780
783
  */
@@ -144,13 +144,15 @@ class DeconzAccessory extends homebridgeLib.AccessoryDelegate {
144
144
  if (params.serviceName === 'Battery') {
145
145
  service = this.serviceByServiceName.Battery
146
146
  } else if (params.serviceName === 'Consumption') {
147
- service = this.serviceByServiceName.Light ||
147
+ service = this.serviceByServiceName.Outlet ||
148
+ this.serviceByServiceName.Light ||
148
149
  this.serviceByServiceName.Power
149
150
  if (service != null) {
150
151
  service.addResource(resource)
151
152
  }
152
153
  } else if (params.serviceName === 'Power') {
153
- service = this.serviceByServiceName.Light ||
154
+ service = this.serviceByServiceName.Outlet ||
155
+ this.serviceByServiceName.Light ||
154
156
  this.serviceByServiceName.Consumption
155
157
  if (service != null) {
156
158
  service.addResource(resource)
@@ -187,8 +189,8 @@ class DeconzAccessory extends homebridgeLib.AccessoryDelegate {
187
189
  }
188
190
  this.serviceBySubtype[resource.subtype] = service
189
191
  this.serviceByRpath[resource.rpath] = service
190
- if (this.serviceByServiceName[resource.serviceName] == null) {
191
- this.serviceByServiceName[resource.serviceName] = service
192
+ if (this.serviceByServiceName[params.serviceName] == null) {
193
+ this.serviceByServiceName[params.serviceName] = service
192
194
  }
193
195
  if (
194
196
  resource.body.config != null &&
@@ -25,6 +25,7 @@ class DeconzPlatform extends homebridgeLib.Platform {
25
25
 
26
26
  parseConfigJson (configJson) {
27
27
  this.config = {
28
+ brightnessAdjustment: 100,
28
29
  forceHttp: false,
29
30
  hosts: [],
30
31
  noResponse: false,
@@ -44,6 +45,7 @@ class DeconzPlatform extends homebridgeLib.Platform {
44
45
  })
45
46
  .stringKey('name')
46
47
  .stringKey('platform')
48
+ .intKey('brightnessAdjustment', 10, 100)
47
49
  .boolKey('forceHttp')
48
50
  .stringKey('host')
49
51
  .arrayKey('hosts')
@@ -61,6 +63,7 @@ class DeconzPlatform extends homebridgeLib.Platform {
61
63
 
62
64
  try {
63
65
  optionParser.parse(configJson)
66
+ this.config.brightnessAdjustment /= 100
64
67
  if (this.config.host != null) {
65
68
  this.config.hosts.push(this.config.host)
66
69
  }
@@ -170,7 +173,8 @@ class DeconzPlatform extends homebridgeLib.Platform {
170
173
  }
171
174
  for (const id in this.gatewayMap) {
172
175
  const gateway = this.gatewayMap[id]
173
- dumpInfo.gatewayMap[id] = gateway.context
176
+ dumpInfo.gatewayMap[id] = Object.assign({}, gateway.context)
177
+ dumpInfo.gatewayMap[id].deviceById = gateway.deviceById
174
178
  }
175
179
  await this.createDumpFile(dumpInfo)
176
180
  } catch (error) { this.error(error) }
@@ -47,7 +47,7 @@ class FilterMaintenance extends DeconzService.SensorsResource {
47
47
  )
48
48
  }
49
49
  if (state.replacefilter != null) {
50
- this.values.filterChange = state.filterChange
50
+ this.values.filterChange = state.replacefilter
51
51
  ? this.Characteristics.hap.FilterChangeIndication.CHANGE_FILTER
52
52
  : this.Characteristics.hap.FilterChangeIndication.FILTER_OK
53
53
  }
@@ -158,16 +158,6 @@ class AirPurifier extends DeconzService.SensorsResource {
158
158
  }
159
159
 
160
160
  updateState (state) {
161
- if (this.values.filterLifeTime != null && state.filterruntime != null) {
162
- this.values.filterLifeLevel = 100 - Math.round(
163
- 100 * state.filterruntime / this.values.filterLifeTime
164
- )
165
- }
166
- if (state.replacefilter != null) {
167
- this.values.filterChange = state.filterChange
168
- ? this.Characteristics.hap.FilterChangeIndication.CHANGE_FILTER
169
- : this.Characteristics.hap.FilterChangeIndication.FILTER_OK
170
- }
171
161
  if (state.speed != null) {
172
162
  this.values.active = state.speed > 0
173
163
  ? this.Characteristics.hap.Active.ACTIVE
@@ -167,6 +167,10 @@ class Button extends homebridgeLib.ServiceDelegate {
167
167
  this.values.event = event
168
168
  }
169
169
  }
170
+
171
+ updateRotation () {
172
+ this.values.event = homeKitEvent.SINGLE_PRESS
173
+ }
170
174
  }
171
175
 
172
176
  module.exports = Button