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 +6 -4
- package/config.schema.json +2 -2
- package/{homebridge-deconz.png → homebridge-ui/public/homebridge-deconz.png} +0 -0
- package/homebridge-ui/public/index.html +488 -8
- package/homebridge-ui/server.js +40 -25
- package/lib/Deconz/Resource.js +1 -1
- package/lib/DeconzAccessory/Gateway.js +39 -0
- package/lib/DeconzPlatform.js +47 -0
- package/lib/DeconzService/Light.js +41 -27
- package/lib/DeconzService/Thermostat.js +1 -1
- package/lib/DeconzService/WindowCovering.js +7 -14
- package/lib/DeconzService/index.js +3 -0
- package/package.json +2 -3
package/README.md
CHANGED
@@ -1,11 +1,13 @@
|
|
1
1
|
<p align="center">
|
2
|
-
<img src="homebridge-deconz.png" height="200px">
|
3
|
-
</p
|
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
|
[](https://www.npmjs.com/package/homebridge-deconz)
|
7
8
|
[](https://www.npmjs.com/package/homebridge-deconz)
|
8
|
-
[](https://discord.gg/
|
9
|
+
[](https://discord.gg/zUhSZSNb4P)
|
10
|
+
[](https://github.com/homebridge/homebridge/wiki/Verified-Plugins)
|
9
11
|
|
10
12
|
[](https://github.com/ebaauw/homebridge-deconz/issues)
|
11
13
|
[](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 **#
|
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.
|
package/config.schema.json
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
"pluginAlias": "deCONZ",
|
3
3
|
"pluginType": "platform",
|
4
4
|
"singular": true,
|
5
|
-
"customUi":
|
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
|
-
"
|
80
|
+
"layout": [
|
81
81
|
"name",
|
82
82
|
{
|
83
83
|
"key": "hosts",
|
File without changes
|
@@ -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
|
-
|
12
|
-
|
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
|
-
|
16
|
-
|
17
|
-
|
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
|
-
|
20
|
-
|
21
|
-
|
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
|
}
|
package/homebridge-ui/server.js
CHANGED
@@ -5,39 +5,54 @@
|
|
5
5
|
|
6
6
|
'use strict'
|
7
7
|
|
8
|
-
const {
|
9
|
-
|
10
|
-
} = require('@homebridge/plugin-ui-utils')
|
8
|
+
const { UiServer } = require('homebridge-lib')
|
9
|
+
const Deconz = require('../lib/Deconz')
|
11
10
|
|
12
|
-
class
|
11
|
+
class DeconzUiServer extends UiServer {
|
13
12
|
constructor () {
|
14
13
|
super()
|
15
14
|
|
16
|
-
this.onRequest('
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
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
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
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
|
58
|
+
new DeconzUiServer() // eslint-disable-line no-new
|
package/lib/Deconz/Resource.js
CHANGED
@@ -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.
|
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
|
package/lib/DeconzPlatform.js
CHANGED
@@ -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.
|
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.
|
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
|
-
|
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('/
|
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
|
-
|
150
|
-
|
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
|
}
|
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.
|
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
|
-
"
|
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"
|