homebridge-deconz 0.0.12 → 0.0.15

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/README.md CHANGED
@@ -1,11 +1,13 @@
1
1
  <p align="center">
2
- <img src="homebridge-deconz.png" height="200px">
3
- </p><span align="center">
2
+ <img src="homebridge-ui/public/homebridge-deconz.png" height="200px">
3
+ </p>
4
+ <span align="center">
4
5
 
5
6
  # Homebridge deCONZ
6
7
  [![Downloads](https://img.shields.io/npm/dt/homebridge-deconz)](https://www.npmjs.com/package/homebridge-deconz)
7
8
  [![Version](https://img.shields.io/npm/v/homebridge-deconz)](https://www.npmjs.com/package/homebridge-deconz)
8
- [![Homebridge Discord](https://img.shields.io/discord/432663330281226270?color=728ED5&logo=discord&label=discord)](https://discord.gg/hZubhrz)
9
+ [![Homebridge Discord](https://img.shields.io/discord/432663330281226270?color=728ED5&logo=discord&label=discord)](https://discord.gg/zUhSZSNb4P)
10
+ [![verified-by-homebridge](https://badgen.net/badge/homebridge/verified/purple)](https://github.com/homebridge/homebridge/wiki/Verified-Plugins)
9
11
 
10
12
  [![GitHub issues](https://img.shields.io/github/issues/ebaauw/homebridge-deconz)](https://github.com/ebaauw/homebridge-deconz/issues)
11
13
  [![GitHub pull requests](https://img.shields.io/github/issues-pr/ebaauw/homebridge-deconz)](https://github.com/ebaauw/homebridge-deconz/pulls)
@@ -22,7 +24,7 @@ See [Future Development of Homebridge Hue](https://github.com/ebaauw/homebridge-
22
24
  Homebridge deCONZ is still under development.
23
25
  See [Releases](https://github.com/ebaauw/homebridge-deconz/releases) for more details.
24
26
 
25
- If you have a question, please post a message to the **#hue** channel of the Homebridge community on [Discord](https://discord.gg/hZubhrz).
27
+ If you have a question, please post a message to the **#deconz** channel of the Homebridge community on [Discord](https://discord.gg/zUhSZSNb4P).
26
28
 
27
29
  ### Introduction
28
30
  This [Homebridge](https://github.com/homebridge/homebridge) plugin exposes to Apple's [HomeKit](http://www.apple.com/ios/home/) ZigBee devices (lights, plugs, sensors, switches, ...) and virtual devices on a deCONZ gateway by dresden elektronik.
@@ -2,7 +2,7 @@
2
2
  "pluginAlias": "deCONZ",
3
3
  "pluginType": "platform",
4
4
  "singular": true,
5
- "customUi": true,
5
+ "customUi": false,
6
6
  "headerDisplay": "Homebridge plugin for deCONZ",
7
7
  "footerDisplay": "For a detailed description, see the [wiki](https://github.com/ebaauw/homebridge-deconz/wiki/Configuration).",
8
8
  "schema": {
@@ -77,7 +77,7 @@
77
77
  }
78
78
  }
79
79
  },
80
- "form": [
80
+ "layout": [
81
81
  "name",
82
82
  {
83
83
  "key": "hosts",
@@ -6,19 +6,499 @@ Copyright © 2022 Erik Baauw. All rights reserved.
6
6
  -->
7
7
 
8
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>
9
14
 
10
15
  <script>
11
- (async () => {
12
- try {
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) {
13
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
+ }, {
14
320
 
15
- // const pluginConfig = await homebridge.getPluginConfig()
16
- // const pluginConfigSchema = await homebridge.getPluginConfigSchema()
17
- const cachedAccessories = await homebridge.getCachedAccessories()
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
+ }
18
470
 
19
- const result = await homebridge.request('/cachedAccessories', cachedAccessories)
20
- const nGateways = Object.keys(result).length
21
- homebridge.toast.success(`${nGateways} gateways`)
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()
22
502
  } catch (error) {
23
503
  console.error(error)
24
504
  }
@@ -5,39 +5,54 @@
5
5
 
6
6
  'use strict'
7
7
 
8
- const {
9
- HomebridgePluginUiServer, RequestError
10
- } = require('@homebridge/plugin-ui-utils')
8
+ const { UiServer } = require('homebridge-lib')
9
+ const Deconz = require('../lib/Deconz')
11
10
 
12
- class UiServer extends HomebridgePluginUiServer {
11
+ class DeconzUiServer extends UiServer {
13
12
  constructor () {
14
13
  super()
15
14
 
16
- this.onRequest('/cachedAccessories', async (cachedAccessories) => {
17
- try {
18
- // console.log('%d accessories', cachedAccessories.length)
19
- const gateways = cachedAccessories.filter((accessory) => {
20
- return accessory.plugin === 'homebridge-deconz' &&
21
- accessory.context != null &&
22
- accessory.context.className === 'Gateway'
15
+ this.onRequest('discover', async (params) => {
16
+ if (this.discovery == null) {
17
+ this.discovery = new Deconz.Discovery({
18
+ // forceHttp: this.config.forceHttp,
19
+ // timeout: this.config.timeout
23
20
  })
24
- // console.log('%d gateways', gateways.length)
25
- const result = {}
26
- for (const gateway of gateways) {
27
- const { host, apiKey } = gateway.context
28
- if (apiKey != null) {
29
- result[host] = apiKey
30
- }
31
- }
32
- console.log('%d gateways: %j', Object.keys(result).length, result)
33
- return result
34
- } catch (error) {
35
- throw new RequestError(error)
21
+ this.discovery
22
+ .on('error', (error) => {
23
+ this.log(
24
+ '%s: request %d: %s %s', error.request.name,
25
+ error.request.id, error.request.method, error.request.resource
26
+ )
27
+ this.warn(
28
+ '%s: request %d: %s', error.request.name, error.request.id, error
29
+ )
30
+ })
31
+ .on('request', (request) => {
32
+ this.debug(
33
+ '%s: request %d: %s %s', request.name,
34
+ request.id, request.method, request.resource
35
+ )
36
+ })
37
+ .on('response', (response) => {
38
+ this.debug(
39
+ '%s: request %d: %d %s', response.request.name,
40
+ response.request.id, response.statusCode, response.statusMessage
41
+ )
42
+ })
43
+ .on('found', (name, id, address) => {
44
+ this.debug('%s: found %s at %s', name, id, address)
45
+ })
46
+ .on('searching', (host) => {
47
+ this.debug('upnp: listening on %s', host)
48
+ })
49
+ .on('searchDone', () => { this.debug('upnp: search done') })
36
50
  }
51
+ const configs = await this.discovery.discover()
52
+ return configs
37
53
  })
38
-
39
54
  this.ready()
40
55
  }
41
56
  }
42
57
 
43
- new UiServer() // eslint-disable-line no-new
58
+ new DeconzUiServer() // eslint-disable-line no-new
@@ -656,7 +656,7 @@ class Resource {
656
656
  buttons.push([5, 'Next', SINGLE | LONG])
657
657
  break
658
658
  case 'TRADFRI wireless dimmer':
659
- if (this.obj.mode === 1) {
659
+ if (this.body.mode === 1) {
660
660
  buttons.push([1, 'Turn Right', SINGLE | LONG])
661
661
  buttons.push([2, 'Turn Left', SINGLE | LONG])
662
662
  } else {
@@ -1187,9 +1187,9 @@ class Resource {
1187
1187
  */
1188
1188
  patchThermostat (gateway) {
1189
1189
  if (this.manufacturer === 'ELKO' && this.model === 'Super TR') {
1190
- this.capbilities.heatValue = 'heat'
1190
+ this.capabilities.heatValue = 'heat'
1191
1191
  } else {
1192
- this.capbilities.heatValue = 'auto'
1192
+ this.capabilities.heatValue = 'auto'
1193
1193
  }
1194
1194
  }
1195
1195
 
@@ -1197,7 +1197,8 @@ class Resource {
1197
1197
  * @param {DeconzAccessory.Gateway} gateway - The gateway.
1198
1198
  */
1199
1199
  patchWindowCovering (gateway) {
1200
- if (this.manufacturer === 'ELKO' && this.model === 'Super TR') {
1200
+ if (this.manufacturer === 'LUMI' && this.model === 'lumi.curtain.acn002') {
1201
+ this.capabilities.maxSpeed = 2
1201
1202
  this.capabilities.positionChange = true
1202
1203
  }
1203
1204
  }
@@ -157,7 +157,7 @@ class Gateway extends homebridgeLib.AccessoryDelegate {
157
157
  this.heartbeatEnabled = true
158
158
  this
159
159
  .on('identify', this.identify)
160
- .once('heartbeat', this.init)
160
+ .once('heartbeat', (beat) => { this.initialBeat = beat })
161
161
  .on('heartbeat', this.heartbeat)
162
162
  .on('shutdown', this.shutdown)
163
163
  }
@@ -182,6 +182,12 @@ class Gateway extends homebridgeLib.AccessoryDelegate {
182
182
  this.values.manufacturer, this.values.model, this.values.software,
183
183
  this.nAccessories, this.nDevices, this.nResourcesMonitored
184
184
  )
185
+ if (this.context.migration != null) {
186
+ this.log(
187
+ 'migration: %s: %d resources',
188
+ this.context.migration, this.nResourcesMonitored
189
+ )
190
+ }
185
191
  if (this.logLevel > 2) {
186
192
  this.vdebug(
187
193
  '%d gateway resouces: %j', this.nResources,
@@ -210,28 +216,24 @@ class Gateway extends homebridgeLib.AccessoryDelegate {
210
216
  }
211
217
  }
212
218
 
213
- /** Initialise the gateway delegate.
214
- */
215
- async init (beat) {
216
- try {
217
- this.debug('initialising...')
218
- this.initialBeat = beat
219
- await this.connect()
220
- this.initialised = true
221
- this.debug('initialised')
222
- this.emit('initialised')
223
- } catch (error) { this.error(error) }
224
- }
225
-
226
219
  /** Update properties from gateway announcement.
227
220
  * @param {string} host - The gateway hostname or IP address and port.
228
221
  * @param {Object} config - The response body of an unauthenticated
229
222
  * GET `/config` (from {@link DeconzDiscovery#config config()}.
230
223
  */
231
- found (host, config) {
232
- this.values.host = host
233
- this.context.config = config
234
- this.values.software = config.swversion
224
+ async found (host, config) {
225
+ try {
226
+ this.context.host = host
227
+ this.values.host = host
228
+ this.context.config = config
229
+ this.values.software = config.swversion
230
+ if (!this.initialised) {
231
+ this.debug('initialising...')
232
+ await this.connect()
233
+ }
234
+ } catch (error) {
235
+ this.error(error)
236
+ }
235
237
  }
236
238
 
237
239
  async shutdown () {
@@ -412,8 +414,8 @@ class Gateway extends homebridgeLib.AccessoryDelegate {
412
414
  for (const id in this.exposeErrorById) {
413
415
  this.resetExposeError(id)
414
416
  }
415
- this.context.fullState = null
416
417
  this.pollNext = true
418
+ this.pollFullState = true
417
419
  } catch (error) {
418
420
  if (
419
421
  error instanceof Deconz.ApiError && error.type === 101 && retry < 8
@@ -627,6 +629,45 @@ class Gateway extends homebridgeLib.AccessoryDelegate {
627
629
 
628
630
  // ===========================================================================
629
631
 
632
+ async onUiGet (a) {
633
+ this.debug('ui request: GET %s', a.join('/'))
634
+ if (a.length === 0) {
635
+ return {
636
+ status: 200,
637
+ body: {
638
+ expose: this.service.values.expose,
639
+ groups: this.service.values.groups,
640
+ heartrate: this.service.values.heartrate,
641
+ lights: this.service.values.lights,
642
+ logLevel: this.service.values.logLevel,
643
+ schedules: this.service.values.schedules,
644
+ sensors: this.service.values.sensors
645
+ // deviceByRidByRtype: this.deviceByRidByRtype
646
+ }
647
+ }
648
+ }
649
+ if (a[0] !== 'accessories') {
650
+ return { status: 403 } // Forbidden
651
+ }
652
+ if (a.length === 1) {
653
+ return { status: 200, body: this.deviceByRidByRtype }
654
+ }
655
+ if (this.deviceById[a[1]] == null) {
656
+ return { status: 404 } // Not Found
657
+ }
658
+ if (a.length === 2) {
659
+ return { status: 200, body: this.deviceById[a[1]] }
660
+ }
661
+ return { status: 403 } // Forbidden
662
+ }
663
+
664
+ async onUiPut (a, body) {
665
+ this.debug('ui request: PUT %s %j', a.join('/'), body)
666
+ return { status: 501 } // Not Implented
667
+ }
668
+
669
+ // ===========================================================================
670
+
630
671
  /** Poll the gateway.
631
672
  *
632
673
  * Periodically get the gateway full state and call
@@ -639,9 +680,11 @@ class Gateway extends homebridgeLib.AccessoryDelegate {
639
680
  try {
640
681
  this.polling = true
641
682
  this.vdebug('%spolling...', this.pollNext ? 'priority ' : '')
642
- if (this.context.fullState == null) {
643
- this.context.fullState = await this.client.get('/')
644
- this.context.fullState.groups[0] = await this.client.get('/groups/0')
683
+ if (this.context.fullState == null || this.pollFullState) {
684
+ const fullState = await this.client.get('/')
685
+ fullState.groups[0] = await this.client.get('/groups/0')
686
+ this.context.fullState = fullState
687
+ this.pollFullState = false
645
688
  } else {
646
689
  const config = await this.client.get('/config')
647
690
  if (config.bridgeid === this.id && config.UTC == null) {
@@ -673,6 +716,11 @@ class Gateway extends homebridgeLib.AccessoryDelegate {
673
716
  this.vdebug('polling done')
674
717
  this.pollNext = false
675
718
  this.polling = false
719
+ if (!this.initialised) {
720
+ this.initialised = true
721
+ this.debug('initialised')
722
+ this.emit('initialised')
723
+ }
676
724
  }
677
725
  }
678
726
 
@@ -857,6 +905,7 @@ class Gateway extends homebridgeLib.AccessoryDelegate {
857
905
  }
858
906
 
859
907
  this.nAccessories = Object.keys(this.accessoryById).length
908
+ this.nResourcesMonitored = Object.keys(this.accessoryByRpath).length
860
909
  this.nExposeErrors = Object.keys(this.exposeErrorById).length
861
910
  if (this.nExposeErrors === 0) {
862
911
  this.vdebug('%d accessories', this.nAccessories)
@@ -867,24 +916,20 @@ class Gateway extends homebridgeLib.AccessoryDelegate {
867
916
  }
868
917
 
869
918
  if (changed) {
870
- this.nResourcesMonitored = Object.keys(this.accessoryByRpath).length
871
- this.identify()
872
919
  if (this.context.migration == null) {
873
920
  const response = await this.client.post('/resourcelinks', {
874
921
  name: 'homebridge-deconz',
875
922
  description: 'migration',
876
923
  classid: 1,
877
- links: Object.keys(this.resourceByRpath).sort()
924
+ links: Object.keys(this.accessoryByRpath).sort()
878
925
  })
879
926
  this.context.migration = '/resourcelinks/' + response.success.id
880
927
  } else {
881
928
  await this.client.put(this.context.migration, {
882
- links: Object.keys(this.resourceByRpath).sort()
929
+ links: Object.keys(this.accessoryByRpath).sort()
883
930
  })
884
931
  }
885
- this.log(
886
- 'migration: %s: %d resources', this.context.migration, this.nResources
887
- )
932
+ this.identify()
888
933
  }
889
934
  }
890
935
 
@@ -45,6 +45,7 @@ class DeconzPlatform extends homebridgeLib.Platform {
45
45
  .stringKey('name')
46
46
  .stringKey('platform')
47
47
  .boolKey('forceHttp')
48
+ .stringKey('host')
48
49
  .arrayKey('hosts')
49
50
  .boolKey('noResponse')
50
51
  .intKey('parallelRequests', 1, 30)
@@ -60,6 +61,9 @@ class DeconzPlatform extends homebridgeLib.Platform {
60
61
 
61
62
  try {
62
63
  optionParser.parse(configJson)
64
+ if (this.config.host != null) {
65
+ this.config.hosts.push(this.config.host)
66
+ }
63
67
  this.discovery = new Deconz.Discovery({
64
68
  forceHttp: this.config.forceHttp,
65
69
  timeout: this.config.timeout
@@ -106,7 +110,8 @@ class DeconzPlatform extends homebridgeLib.Platform {
106
110
  host: host
107
111
  })
108
112
  }
109
- this.gatewayMap[id].found(host, config)
113
+ await this.gatewayMap[id].found(host, config)
114
+ await events.once(this.gatewayMap[id], 'initialised')
110
115
  this.emit('found')
111
116
  }
112
117
 
@@ -124,17 +129,25 @@ class DeconzPlatform extends homebridgeLib.Platform {
124
129
  async init () {
125
130
  try {
126
131
  const jobs = []
127
- this.debug('job %d: find at least one gateway', jobs.length)
128
- jobs.push(events.once(this, 'found'))
129
132
  if (this.config.hosts.length > 0) {
130
133
  for (const host of this.config.hosts) {
131
134
  this.debug('job %d: find gateway at %s', jobs.length, host)
132
135
  jobs.push(this.findHost(host))
133
136
  }
134
137
  } else {
138
+ this.debug('job %d: find at least one gateway', jobs.length)
139
+ jobs.push(events.once(this, 'found'))
135
140
  for (const id in this.gatewayMap) {
141
+ const gateway = this.gatewayMap[id]
142
+ const host = gateway.context.host
136
143
  this.debug('job %d: find gateway %s', jobs.length, id)
137
- jobs.push(this.gatewayMap[id].init())
144
+ jobs.push(events.once(gateway, 'initialised'))
145
+ try {
146
+ const config = await this.discovery.config(host)
147
+ await this.foundGateway(host, config)
148
+ } catch (error) {
149
+ this.warn('%s: %s', id, error)
150
+ }
138
151
  }
139
152
  }
140
153
 
@@ -150,9 +163,61 @@ class DeconzPlatform extends homebridgeLib.Platform {
150
163
 
151
164
  this.log('%d gateways', Object.keys(this.gatewayMap).length)
152
165
  this.emit('initialised')
166
+ const dumpInfo = {
167
+ config: this.config,
168
+ gatewayMap: {}
169
+ }
170
+ for (const id in this.gatewayMap) {
171
+ const gateway = this.gatewayMap[id]
172
+ dumpInfo.gatewayMap[id] = gateway.context
173
+ }
174
+ await this.createDumpFile(dumpInfo)
153
175
  } catch (error) { this.error(error) }
154
176
  }
155
177
 
178
+ async onUiRequest (method, url, body) {
179
+ const a = url.split('/').slice(1)
180
+ if (a.length < 1) {
181
+ return { status: 403 } // Forbidden
182
+ }
183
+ if (a[0] === 'gateways') {
184
+ if (a.length === 1) {
185
+ if (method === 'GET') {
186
+ // const gatewayByHost = await this.discovery.discover()
187
+ const gatewayByHost = {}
188
+ for (const id in this.gatewayMap) {
189
+ const gateway = this.gatewayMap[id]
190
+ gatewayByHost[gateway.context.host] = {
191
+ config: gateway.context.config,
192
+ host: gateway.context.host,
193
+ id: id
194
+ }
195
+ }
196
+ return {
197
+ status: 200,
198
+ body: Object.keys(gatewayByHost).sort().map((host) => {
199
+ return gatewayByHost[host]
200
+ })
201
+ }
202
+ }
203
+ return { status: 405 } // Method Not Allowed
204
+ }
205
+ const gateway = this.gatewayMap[a[1]]
206
+ const path = a.slice(2)
207
+ if (gateway == null) {
208
+ return { status: 404 } // Not Found
209
+ }
210
+ if (method === 'GET') {
211
+ return gateway.onUiGet(path)
212
+ }
213
+ if (method === 'PUT') {
214
+ return gateway.onUiPut(path, body)
215
+ }
216
+ return { status: 405 } // Method Not Allowed
217
+ }
218
+ return { status: 403 } // Forbidden
219
+ }
220
+
156
221
  async heartbeat (beat) {
157
222
  try {
158
223
  if (beat % 300 === 5 && this.config.hosts.length === 0) {
@@ -140,7 +140,7 @@ class Light extends DeconzService.LightsResource {
140
140
  unit: '°'
141
141
  }).on('didSet', (value, fromHomeKit) => {
142
142
  if (fromHomeKit) {
143
- const hue = Math.round(this.hk.hue * 65535.0 / 360.0)
143
+ const hue = Math.round(this.values.hue * 65535.0 / 360.0)
144
144
  this.put({ hue: hue })
145
145
  this.values.colormode = 'hs'
146
146
  }
@@ -151,7 +151,7 @@ class Light extends DeconzService.LightsResource {
151
151
  unit: '%'
152
152
  }).on('didSet', (value, fromHomeKit) => {
153
153
  if (fromHomeKit) {
154
- const sat = Math.round(this.hk.sat * 254.0 / 100.0)
154
+ const sat = Math.round(this.values.saturation * 254.0 / 100.0)
155
155
  this.put({ sat: sat })
156
156
  this.values.colormode = 'hs'
157
157
  }
@@ -236,31 +236,8 @@ class Light extends DeconzService.LightsResource {
236
236
  }
237
237
 
238
238
  if (this.resource.rtype === 'groups') {
239
- this.sceneServices = []
240
- for (const scene of this.resource.body.scenes) {
241
- const service = new homebridgeLib.ServiceDelegate(accessory, {
242
- name: this.resource.body.name + ' ' + scene.name,
243
- Service: this.Services.hap.Switch,
244
- subtype: this.subtype + '-S' + scene.id
245
- })
246
- service.addCharacteristicDelegate({
247
- key: 'on',
248
- Characteristic: this.Characteristics.hap.On,
249
- value: false
250
- }).on('didSet', async (value, fromHomeKit) => {
251
- this.checkAdaptiveLighting()
252
- if (fromHomeKit && value) {
253
- try {
254
- const path = this.resource.rpath + '/scenes/' + scene.id + '/recall'
255
- this.debug('PUT %s', path)
256
- await this.client.put(path)
257
- } catch (error) { this.error(error) }
258
- await timeout(this.platform.config.waitTimeReset)
259
- service.values.on = false
260
- }
261
- })
262
- this.sceneServices.push(service)
263
- }
239
+ this.sceneServices = {}
240
+ this.updateScenes(this.resource.body.scenes)
264
241
  }
265
242
 
266
243
  if (this.capabilities.effects != null) {
@@ -412,6 +389,43 @@ class Light extends DeconzService.LightsResource {
412
389
  super.updateState(state)
413
390
  }
414
391
 
392
+ updateScenes (scenes) {
393
+ const sceneById = {}
394
+ for (const scene of scenes) {
395
+ sceneById[scene.id] = scene
396
+ if (this.sceneServices[scene.id] == null) {
397
+ const service = new homebridgeLib.ServiceDelegate(this.accessoryDelegate, {
398
+ name: this.resource.body.name + ' ' + scene.name,
399
+ Service: this.Services.hap.Switch,
400
+ subtype: this.subtype + '-S' + scene.id
401
+ })
402
+ service.addCharacteristicDelegate({
403
+ key: 'on',
404
+ Characteristic: this.Characteristics.hap.On,
405
+ value: false
406
+ }).on('didSet', async (value, fromHomeKit) => {
407
+ this.checkAdaptiveLighting()
408
+ if (fromHomeKit && value) {
409
+ try {
410
+ const path = this.resource.rpath + '/scenes/' + scene.id + '/recall'
411
+ this.debug('PUT %s', path)
412
+ await this.client.put(path)
413
+ } catch (error) { this.error(error) }
414
+ await timeout(this.platform.config.waitTimeReset)
415
+ service.values.on = false
416
+ }
417
+ })
418
+ this.sceneServices[scene.id] = service
419
+ }
420
+ }
421
+ for (const id in this.scenesServices) {
422
+ if (sceneById[id] == null) {
423
+ this.scenesSerices[id].destroy()
424
+ delete this.scenesService[id]
425
+ }
426
+ }
427
+ }
428
+
415
429
  initAdaptiveLighting () {
416
430
  if (this.adaptiveLighting == null) {
417
431
  this.adaptiveLighting = new homebridgeLib.AdaptiveLighting(
@@ -12,7 +12,7 @@ const DeconzService = require('../DeconzService')
12
12
  */
13
13
  class Thermostat extends DeconzService.SensorsResource {
14
14
  constructor (accessory, resource, params = {}) {
15
- params.Service = accessory.Services.hap.TemperatureSensor
15
+ params.Service = accessory.Services.hap.Thermostat
16
16
  super(accessory, resource, params)
17
17
 
18
18
  this.addCharacteristicDelegate({
@@ -65,7 +65,7 @@ class Thermostat extends DeconzService.SensorsResource {
65
65
  }
66
66
  }).on('didSet', async (value, fromHomeKit) => {
67
67
  if (fromHomeKit) {
68
- await this.put('/mode', {
68
+ await this.put('/config', {
69
69
  mode: value === this.Characteristics.hap.TargetHeatingCoolingState.OFF
70
70
  ? 'off'
71
71
  : this.capabilities.heatValue
@@ -37,10 +37,6 @@ class WindowCovering extends DeconzService.LightsResource {
37
37
  key: 'positionState',
38
38
  Characteristic: this.Characteristics.hap.PositionState,
39
39
  value: this.Characteristics.hap.PositionState.STOPPED
40
- }).on('didSet', (value) => {
41
- if (value === this.Characteristics.hap.PositionState.STOPPED) {
42
- this.values.targetPosition = this.values.currentPosition
43
- }
44
40
  })
45
41
 
46
42
  this.addCharacteristicDelegate({
@@ -63,6 +59,25 @@ class WindowCovering extends DeconzService.LightsResource {
63
59
  })
64
60
  }
65
61
 
62
+ if (resource.capabilities.maxSpeed != null) {
63
+ this.addCharacteristicDelegate({
64
+ key: 'motorSpeed',
65
+ Characteristic: this.Characteristics.my.MotorSpeed,
66
+ unit: '',
67
+ props: {
68
+ unit: '',
69
+ minValue: 0,
70
+ maxValue: resource.capabilities.maxSpeed,
71
+ minStep: 1
72
+ }
73
+ }).on('didSet', async (value, fromHomeKit) => {
74
+ if (!fromHomeKit) {
75
+ return
76
+ }
77
+ await this.put({ speed: value })
78
+ })
79
+ }
80
+
66
81
  if (resource.capabilities.positionChange) {
67
82
  this.addCharacteristicDelegate({
68
83
  key: 'positionChange',
@@ -80,13 +95,10 @@ class WindowCovering extends DeconzService.LightsResource {
80
95
  this.addCharacteristicDelegates()
81
96
 
82
97
  this.update(resource.body, resource.rpath)
98
+ this.values.targetPosition = this.values.currentPosition
83
99
  }
84
100
 
85
- setPosition () {
86
- if (this.timer != null) {
87
- clearTimeout(this.timer)
88
- delete this.timer
89
- }
101
+ async setPosition () {
90
102
  let lift = 100 - this.values.targetPosition // % closed --> % open
91
103
  if (this.venetianBlind) {
92
104
  if (this.values.closeUpwards) {
@@ -101,14 +113,11 @@ class WindowCovering extends DeconzService.LightsResource {
101
113
  this.values.targetPosition > this.values.currentPosition
102
114
  ? this.Characteristics.hap.PositionState.INCREASING
103
115
  : this.Characteristics.hap.PositionState.DECREASING
104
- this.put({ lift: lift })
105
- this.timer = setTimeout(() => {
106
- this.values.positionState =
107
- this.Characteristics.hap.PositionState.STOPPED
108
- }, 15000)
116
+ this.moving = new Date()
117
+ await this.put({ lift: lift })
109
118
  }
110
119
 
111
- updateState (state, rpath) {
120
+ updateState (state) {
112
121
  if (state.lift != null) {
113
122
  let position = Math.round(state.lift / 5) * 5
114
123
  let closeUpwards
@@ -121,16 +130,23 @@ class WindowCovering extends DeconzService.LightsResource {
121
130
  }
122
131
  }
123
132
  position = 100 - position // % open -> % closed
124
- if (
125
- position === this.values.targetPosition &&
126
- (closeUpwards == null || closeUpwards === this.targetCloseUpwards)
127
- ) {
128
- this.values.positionState = this.Characteristics.hap.PositionState.STOPPED
129
- }
130
133
  this.values.currentPosition = position
131
134
  if (closeUpwards != null) {
132
135
  this.values.closeUpwards = closeUpwards
133
136
  }
137
+ if (
138
+ this.moving == null || new Date() - this.moving >= 30000 || (
139
+ position === this.values.targetPosition &&
140
+ (closeUpwards == null || closeUpwards === this.targetCloseUpwards)
141
+ )
142
+ ) {
143
+ this.moving = null
144
+ this.values.targetPosition = position
145
+ this.values.positionState = this.Characteristics.hap.PositionState.STOPPED
146
+ }
147
+ }
148
+ if (state.speed != null) {
149
+ this.values.motorSpeed = state.speed
134
150
  }
135
151
  super.updateState(state)
136
152
  }
@@ -91,6 +91,9 @@ class DeconzService extends homebridgeLib.ServiceDelegate {
91
91
  if (body.state != null) {
92
92
  this.updateState(body.state, rpath)
93
93
  }
94
+ if (body.scenes != null && this.rtype === 'groups') {
95
+ this.updateScenes(body.scenes)
96
+ }
94
97
  }
95
98
  }
96
99
 
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "displayName": "Homebridge deCONZ",
5
5
  "author": "Erik Baauw",
6
6
  "license": "Apache-2.0",
7
- "version": "0.0.12",
7
+ "version": "0.0.15",
8
8
  "keywords": [
9
9
  "homebridge-plugin",
10
10
  "homekit",
@@ -22,13 +22,12 @@
22
22
  "engines": {
23
23
  "deCONZ": "2.14.1",
24
24
  "homebridge": "^1.4.0",
25
- "node": "^16.14.0"
25
+ "node": "^16.14.2"
26
26
  },
27
27
  "dependencies": {
28
- "@homebridge/plugin-ui-utils": "~0.0.19",
29
- "homebridge-lib": "~5.2.3",
30
- "semver": "^7.3.5",
31
- "ws": "^8.4.2",
28
+ "homebridge-lib": "~5.4.0",
29
+ "semver": "^7.3.7",
30
+ "ws": "^8.5.0",
32
31
  "xml2js": "~0.4.23"
33
32
  },
34
33
  "scripts": {