homebridge-deconz 0.0.13 → 0.0.14

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 {
@@ -627,6 +627,45 @@ class Gateway extends homebridgeLib.AccessoryDelegate {
627
627
 
628
628
  // ===========================================================================
629
629
 
630
+ async onUiGet (a) {
631
+ this.debug('ui request: GET %s', a.join('/'))
632
+ if (a.length === 0) {
633
+ return {
634
+ status: 200,
635
+ body: {
636
+ expose: this.service.values.expose,
637
+ groups: this.service.values.groups,
638
+ heartrate: this.service.values.heartrate,
639
+ lights: this.service.values.lights,
640
+ logLevel: this.service.values.logLevel,
641
+ schedules: this.service.values.schedules,
642
+ sensors: this.service.values.sensors
643
+ // deviceByRidByRtype: this.deviceByRidByRtype
644
+ }
645
+ }
646
+ }
647
+ if (a[0] !== 'accessories') {
648
+ return { status: 403 } // Forbidden
649
+ }
650
+ if (a.length === 1) {
651
+ return { status: 200, body: this.deviceByRidByRtype }
652
+ }
653
+ if (this.deviceById[a[1]] == null) {
654
+ return { status: 404 } // Not Found
655
+ }
656
+ if (a.length === 2) {
657
+ return { status: 200, body: this.deviceById[a[1]] }
658
+ }
659
+ return { status: 403 } // Forbidden
660
+ }
661
+
662
+ async onUiPut (a, body) {
663
+ this.debug('ui request: PUT %s %j', a.join('/'), body)
664
+ return { status: 501 } // Not Implented
665
+ }
666
+
667
+ // ===========================================================================
668
+
630
669
  /** Poll the gateway.
631
670
  *
632
671
  * Periodically get the gateway full state and call
@@ -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
@@ -153,6 +157,49 @@ class DeconzPlatform extends homebridgeLib.Platform {
153
157
  } catch (error) { this.error(error) }
154
158
  }
155
159
 
160
+ async onUiRequest (method, url, body) {
161
+ const a = url.split('/').slice(1)
162
+ if (a.length < 1) {
163
+ return { status: 403 } // Forbidden
164
+ }
165
+ if (a[0] === 'gateways') {
166
+ if (a.length === 1) {
167
+ if (method === 'GET') {
168
+ // const gatewayByHost = await this.discovery.discover()
169
+ const gatewayByHost = {}
170
+ for (const id in this.gatewayMap) {
171
+ const gateway = this.gatewayMap[id]
172
+ gatewayByHost[gateway.context.host] = {
173
+ config: gateway.context.config,
174
+ host: gateway.context.host,
175
+ id: id
176
+ }
177
+ }
178
+ return {
179
+ status: 200,
180
+ body: Object.keys(gatewayByHost).sort().map((host) => {
181
+ return gatewayByHost[host]
182
+ })
183
+ }
184
+ }
185
+ return { status: 405 } // Method Not Allowed
186
+ }
187
+ const gateway = this.gatewayMap[a[1]]
188
+ const path = a.slice(2)
189
+ if (gateway == null) {
190
+ return { status: 404 } // Not Found
191
+ }
192
+ if (method === 'GET') {
193
+ return gateway.onUiGet(path)
194
+ }
195
+ if (method === 'PUT') {
196
+ return gateway.onUiPut(path, body)
197
+ }
198
+ return { status: 405 } // Method Not Allowed
199
+ }
200
+ return { status: 403 } // Forbidden
201
+ }
202
+
156
203
  async heartbeat (beat) {
157
204
  try {
158
205
  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(
@@ -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({
@@ -103,10 +99,6 @@ class WindowCovering extends DeconzService.LightsResource {
103
99
  }
104
100
 
105
101
  async setPosition () {
106
- if (this.timer != null) {
107
- clearTimeout(this.timer)
108
- delete this.timer
109
- }
110
102
  let lift = 100 - this.values.targetPosition // % closed --> % open
111
103
  if (this.venetianBlind) {
112
104
  if (this.values.closeUpwards) {
@@ -121,11 +113,8 @@ class WindowCovering extends DeconzService.LightsResource {
121
113
  this.values.targetPosition > this.values.currentPosition
122
114
  ? this.Characteristics.hap.PositionState.INCREASING
123
115
  : this.Characteristics.hap.PositionState.DECREASING
116
+ this.moving = new Date()
124
117
  await this.put({ lift: lift })
125
- this.timer = setTimeout(() => {
126
- this.values.positionState =
127
- this.Characteristics.hap.PositionState.STOPPED
128
- }, 15000)
129
118
  }
130
119
 
131
120
  updateState (state) {
@@ -146,9 +135,13 @@ class WindowCovering extends DeconzService.LightsResource {
146
135
  this.values.closeUpwards = closeUpwards
147
136
  }
148
137
  if (
149
- position === this.values.targetPosition &&
150
- (closeUpwards == null || closeUpwards === this.targetCloseUpwards)
138
+ this.moving == null || new Date() - this.moving >= 15000 || (
139
+ position === this.values.targetPosition &&
140
+ (closeUpwards == null || closeUpwards === this.targetCloseUpwards)
141
+ )
151
142
  ) {
143
+ this.moving = null
144
+ this.values.targetPosition = position
152
145
  this.values.positionState = this.Characteristics.hap.PositionState.STOPPED
153
146
  }
154
147
  }
@@ -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.13",
7
+ "version": "0.0.14",
8
8
  "keywords": [
9
9
  "homebridge-plugin",
10
10
  "homekit",
@@ -25,8 +25,7 @@
25
25
  "node": "^16.14.0"
26
26
  },
27
27
  "dependencies": {
28
- "@homebridge/plugin-ui-utils": "~0.0.19",
29
- "homebridge-lib": "~5.2.3",
28
+ "homebridge-lib": "~5.3.0",
30
29
  "semver": "^7.3.5",
31
30
  "ws": "^8.5.0",
32
31
  "xml2js": "~0.4.23"