homebridge-deconz 0.0.8 → 0.0.9
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/cli/deconz.js +2 -3
- package/lib/Deconz/Resource.js +260 -6
- package/lib/DeconzAccessory/Gateway.js +16 -2
- package/lib/DeconzAccessory/Light.js +65 -0
- package/lib/DeconzAccessory/index.js +14 -11
- package/lib/DeconzService/GatewaySettings.js +3 -2
- package/lib/DeconzService/Light.js +469 -0
- package/lib/DeconzService/Sensor.js +2 -2
- package/lib/DeconzService/index.js +1 -1
- package/package.json +2 -2
package/cli/deconz.js
CHANGED
@@ -517,7 +517,7 @@ class Main extends homebridgeLib.CommandLineTool {
|
|
517
517
|
'missing API key - unlock gateway and run "deconz%s getApiKey"', args
|
518
518
|
)
|
519
519
|
}
|
520
|
-
this.client = new Deconz.
|
520
|
+
this.client = new Deconz.ApiClient(this.clargs.options)
|
521
521
|
this.client
|
522
522
|
.on('error', (error) => {
|
523
523
|
if (error.request.id !== this.requestId) {
|
@@ -688,10 +688,9 @@ class Main extends homebridgeLib.CommandLineTool {
|
|
688
688
|
this.jsonFormatter = new homebridgeLib.JsonFormatter(
|
689
689
|
mode === 'service' ? { noWhiteSpace: true } : {}
|
690
690
|
)
|
691
|
-
const WsMonitor = require('../lib/DeconzWsClient')
|
692
691
|
const { websocketport } = await this.client.get('/config')
|
693
692
|
options.host = this.client.host + ':' + websocketport
|
694
|
-
this.wsMonitor = new
|
693
|
+
this.wsMonitor = new Deconz.WsClient(options)
|
695
694
|
this.setOptions({ mode: mode })
|
696
695
|
this.wsMonitor
|
697
696
|
.on('error', (error) => { this.error(error) })
|
package/lib/Deconz/Resource.js
CHANGED
@@ -7,7 +7,10 @@
|
|
7
7
|
|
8
8
|
const homebridgeLib = require('homebridge-lib')
|
9
9
|
|
10
|
-
const { toInt, toObject, toString } = homebridgeLib.OptionParser
|
10
|
+
const { toInstance, toInt, toObject, toString } = homebridgeLib.OptionParser
|
11
|
+
const { defaultGamut } = homebridgeLib.Colour
|
12
|
+
|
13
|
+
const DeconzAccessory = require('../DeconzAccessory')
|
11
14
|
|
12
15
|
const rtypes = ['lights', 'sensors', 'groups']
|
13
16
|
|
@@ -26,6 +29,53 @@ const sensorsPrios = [
|
|
26
29
|
'Thermostat'
|
27
30
|
]
|
28
31
|
|
32
|
+
// =============================================================================
|
33
|
+
|
34
|
+
// See: http://www.developers.meethue.com/documentation/supported-lights
|
35
|
+
|
36
|
+
const hueGamutType = { // Color gamut per light model.
|
37
|
+
A: { // Color Lights
|
38
|
+
r: [0.7040, 0.2960],
|
39
|
+
g: [0.2151, 0.7106],
|
40
|
+
b: [0.1380, 0.0800]
|
41
|
+
},
|
42
|
+
B: { // Extended Color Lights
|
43
|
+
r: [0.6750, 0.3220],
|
44
|
+
g: [0.4090, 0.5180],
|
45
|
+
b: [0.1670, 0.0400]
|
46
|
+
},
|
47
|
+
C: { // next gen Extended Color Lights
|
48
|
+
r: [0.6920, 0.3080],
|
49
|
+
g: [0.1700, 0.7000],
|
50
|
+
b: [0.1530, 0.0480]
|
51
|
+
}
|
52
|
+
}
|
53
|
+
|
54
|
+
const hueGamutTypeByModel = {
|
55
|
+
LCT001: 'B', // Hue bulb A19
|
56
|
+
LCT002: 'B', // Hue Spot BR30
|
57
|
+
LCT003: 'B', // Hue Spot GU10
|
58
|
+
LCT007: 'B', // Hue bulb A19
|
59
|
+
LCT010: 'C', // Hue bulb A19
|
60
|
+
LCT011: 'C', // Hue BR30
|
61
|
+
LCT012: 'C', // Hue Color Candle
|
62
|
+
LCT014: 'C', // Hue bulb A19
|
63
|
+
LCT015: 'C', // Hue bulb A19
|
64
|
+
LCT016: 'C', // Hue bulb A19
|
65
|
+
LLC005: 'A', // Living Colors Gen3 Bloom, Aura
|
66
|
+
LLC006: 'A', // Living Colors Gen3 Iris
|
67
|
+
LLC007: 'A', // Living Colors Gen3 Bloom, Aura
|
68
|
+
LLC010: 'A', // Hue Living Colors Iris
|
69
|
+
LLC011: 'A', // Hue Living Colors Bloom
|
70
|
+
LLC012: 'A', // Hue Living Colors Bloom
|
71
|
+
LLC013: 'A', // Disney Living Colors
|
72
|
+
LLC014: 'A', // Living Colors Gen3 Bloom, Aura
|
73
|
+
LLC020: 'C', // Hue Go
|
74
|
+
LLM001: 'B', // Color Light Module
|
75
|
+
LST001: 'A', // Hue LightStrips
|
76
|
+
LST002: 'C' // Hue LightStrips Plus
|
77
|
+
}
|
78
|
+
|
29
79
|
/** Delegate class for a resource on a deCONZ gateway.
|
30
80
|
*
|
31
81
|
* @memberof Deconz
|
@@ -47,14 +97,14 @@ class Resource {
|
|
47
97
|
|
48
98
|
/** Create a new instance of a delegate of a resource.
|
49
99
|
*
|
50
|
-
* @param {
|
100
|
+
* @param {DeconzAccessory.Gateway} gateway - The gateway.
|
51
101
|
* @param {string} rtype - The resource type of the resource:
|
52
102
|
* `groups`, `lights`, or `sensors`.
|
53
103
|
* @param {integer} rid - The resource ID of the resource.
|
54
104
|
* @param {object} body - The body of the resource.
|
55
105
|
*/
|
56
|
-
constructor (
|
57
|
-
|
106
|
+
constructor (gateway, rtype, rid, body) {
|
107
|
+
toInstance('gateway', gateway, DeconzAccessory.Gateway)
|
58
108
|
|
59
109
|
/** The resource type of the resource: `groups`, `lights`, or `sensors`.
|
60
110
|
* @type {string}
|
@@ -112,9 +162,78 @@ class Resource {
|
|
112
162
|
this.zigbee = true
|
113
163
|
} else {
|
114
164
|
this.subtype = '-' + rtype[0].toUpperCase() + rid
|
115
|
-
this.id =
|
165
|
+
this.id = gateway.id + this.subtype
|
116
166
|
this.zigbee = false
|
117
167
|
}
|
168
|
+
|
169
|
+
/** The associated Homekit _Manufacturer_.
|
170
|
+
*
|
171
|
+
* For Zigbee devices, this is the sanitised `manufacturername` in the
|
172
|
+
* resource body.
|
173
|
+
* For virtual devices, this is the _Manufacturer_ for the gateway.
|
174
|
+
* @type {string}
|
175
|
+
*/
|
176
|
+
this.manufacturer = this.zigbee
|
177
|
+
? body.manufacturername
|
178
|
+
: gateway.values.manufacturer
|
179
|
+
|
180
|
+
/** The associated HomeKit _Model_.
|
181
|
+
*
|
182
|
+
* For Zigbee devices, this is the sanitised `modelid` in the
|
183
|
+
* resource body.
|
184
|
+
* For virtual devices, this is the `type` in the resource body.
|
185
|
+
* @type {string}
|
186
|
+
*/
|
187
|
+
this.model = this.zigbee ? body.modelid : body.type
|
188
|
+
|
189
|
+
/** The associated HomeKit _Firmware Version_.
|
190
|
+
*
|
191
|
+
* For Zigbee devices, this is the sanitised `swversion` in the
|
192
|
+
* resource body.
|
193
|
+
* For virtual devices, this is the _Firmware Version_ for the gateway.
|
194
|
+
*/
|
195
|
+
this.firmware = this.zigbee
|
196
|
+
? body.swversion == null ? '0.0.0' : body.swversion
|
197
|
+
: gateway.values.software
|
198
|
+
|
199
|
+
switch (this.serviceName) {
|
200
|
+
case 'Light': {
|
201
|
+
if (body.action != null) {
|
202
|
+
Object.assign(body.state, body.action)
|
203
|
+
delete body.state.on
|
204
|
+
}
|
205
|
+
this.capabilities = {
|
206
|
+
on: body.state.on !== undefined,
|
207
|
+
bri: body.state.bri !== undefined,
|
208
|
+
ct: body.state.ct !== undefined,
|
209
|
+
ctMax: (body.ctmax != null && body.ctmax !== 0 && body.ctmax !== 65535)
|
210
|
+
? body.ctmax
|
211
|
+
: 500,
|
212
|
+
ctMin: (body.ctmin != null && body.ctmin !== 0)
|
213
|
+
? body.ctmin
|
214
|
+
: 153,
|
215
|
+
xy: body.state.xy !== undefined,
|
216
|
+
gamut: defaultGamut,
|
217
|
+
alert: body.state.alert !== undefined,
|
218
|
+
colorLoop: body.state.effect !== undefined
|
219
|
+
}
|
220
|
+
break
|
221
|
+
}
|
222
|
+
case 'WarningDevice':
|
223
|
+
this.capabilities = {}
|
224
|
+
break
|
225
|
+
case 'WindowCovering':
|
226
|
+
this.capabilities = {}
|
227
|
+
break
|
228
|
+
default:
|
229
|
+
this.capabilities = {}
|
230
|
+
break
|
231
|
+
}
|
232
|
+
|
233
|
+
const f = 'patch' + this.serviceName
|
234
|
+
if (typeof this[f] === 'function') {
|
235
|
+
this[f](gateway)
|
236
|
+
}
|
118
237
|
}
|
119
238
|
|
120
239
|
/** The priority of the resource, when determining the primary resource for a
|
@@ -143,7 +262,7 @@ class Resource {
|
|
143
262
|
*/
|
144
263
|
get serviceName () {
|
145
264
|
if (this.rtype === 'groups') {
|
146
|
-
return '
|
265
|
+
return 'Light'
|
147
266
|
} else if (this.rtype === 'lights') {
|
148
267
|
switch (this.body.type) {
|
149
268
|
case 'Color dimmable light': return 'Light'
|
@@ -222,6 +341,141 @@ class Resource {
|
|
222
341
|
}
|
223
342
|
}
|
224
343
|
}
|
344
|
+
|
345
|
+
/** Patch a resource corresponding to a `Light` service.
|
346
|
+
* @param {DeconzAccessory.Gateway} gateway - The gateway.
|
347
|
+
*/
|
348
|
+
patchLight (gateway) {
|
349
|
+
switch (this.manufacturer) {
|
350
|
+
case 'Busch-Jaeger':
|
351
|
+
// See: https://www.busch-jaeger.de/en/products/product-solutions/dimmer/busch-radio-controlled-dimmer-zigbee-light-link/
|
352
|
+
if (
|
353
|
+
this.model === 'RM01' && // 6715 U-500 with 6736-84.
|
354
|
+
this.capabilities.bri && this.body.type === 'On/Off light' // Issue #241
|
355
|
+
) {
|
356
|
+
gateway.debug(
|
357
|
+
'%s: ignoring state.bri for %s', this.rpath, this.body.type
|
358
|
+
)
|
359
|
+
this.capabilities.bri = false
|
360
|
+
}
|
361
|
+
break
|
362
|
+
case 'dresden elektronik':
|
363
|
+
// See: https://www.dresden-elektronik.de/funktechnik/solutions/wireless-light-control/wireless-ballasts/?L=1
|
364
|
+
this.capabilities.computesXy = true
|
365
|
+
break
|
366
|
+
case 'FeiBit':
|
367
|
+
if (this.model === 'FNB56-SKT1EHG1.2') { // issue #361
|
368
|
+
this.body.type = 'On/Off plug-in unit'
|
369
|
+
}
|
370
|
+
break
|
371
|
+
case 'GLEDOPTO':
|
372
|
+
// See: https://www.led-trading.de/zigbee-kompatibel-controller-led-lichtsteuerung
|
373
|
+
this.capabilities.gamut = {
|
374
|
+
r: [0.7006, 0.2993],
|
375
|
+
g: [0.1387, 0.8148],
|
376
|
+
b: [0.1510, 0.0227]
|
377
|
+
}
|
378
|
+
if (this.model === 'GLEDOPTO') { // Issue #244
|
379
|
+
if (
|
380
|
+
this.subtype === '0a' &&
|
381
|
+
this.body.type === 'Dimmable light' &&
|
382
|
+
this.firmware === '1.0.2'
|
383
|
+
) {
|
384
|
+
this.model = 'RGBW'
|
385
|
+
} else if (
|
386
|
+
this.subtype === '0b' &&
|
387
|
+
this.body.type === 'Color temperature light' &&
|
388
|
+
this.firmware === '1.3.002'
|
389
|
+
) {
|
390
|
+
this.model = 'WW/CW'
|
391
|
+
} else if (
|
392
|
+
this.subtype === '0b' &&
|
393
|
+
this.body.type === 'Extended color light' &&
|
394
|
+
this.firmware === '1.0.2'
|
395
|
+
) {
|
396
|
+
this.model = 'RGB+CCT'
|
397
|
+
const device = gateway.resourceById[this.id]
|
398
|
+
if (device != null && device.resourceBySubtype.length > 1) {
|
399
|
+
this.model = 'RGBW'
|
400
|
+
this.capabilities.ct = false
|
401
|
+
}
|
402
|
+
} else {
|
403
|
+
return
|
404
|
+
}
|
405
|
+
gateway.debug('%s: set model to %j', this.rpath, this.model)
|
406
|
+
}
|
407
|
+
break
|
408
|
+
case 'IKEA of Sweden':
|
409
|
+
// See: http://www.ikea.com/us/en/catalog/categories/departments/lighting/smart_lighting/
|
410
|
+
this.capabilities.gamut = defaultGamut // Issue #956
|
411
|
+
this.capabilities.noTransition = true
|
412
|
+
if (this.model === 'TRADFRI bulb E27 CWS 806lm') {
|
413
|
+
this.capabilities.computesXy = true
|
414
|
+
this.capabilities.gamut = {
|
415
|
+
r: [0.68, 0.31],
|
416
|
+
g: [0.11, 0.82],
|
417
|
+
b: [0.13, 0.04]
|
418
|
+
}
|
419
|
+
}
|
420
|
+
break
|
421
|
+
case 'innr':
|
422
|
+
// See: https://shop.innrlighting.com/en/shop
|
423
|
+
this.capabilities.gamut = { // Issue #152
|
424
|
+
r: [0.8817, 0.1033],
|
425
|
+
g: [0.2204, 0.7758],
|
426
|
+
b: [0.0551, 0.1940]
|
427
|
+
}
|
428
|
+
if (this.model === 'SP 120') { // smart plug
|
429
|
+
this.capabilities.bri = false
|
430
|
+
}
|
431
|
+
break
|
432
|
+
case 'LIDL Livarno Lux':
|
433
|
+
this.capabilities.ctMax = 454 // 2200 K
|
434
|
+
this.capabilities.ctMin = 153 // 6500 K
|
435
|
+
if (this.model === 'HG06467') { // Xmas light strip
|
436
|
+
this.capabilities.colorLoop = false
|
437
|
+
this.capabilities.hs = true
|
438
|
+
this.capabilities.effects = [
|
439
|
+
'Steady', 'Snow', 'Rainbow', 'Snake',
|
440
|
+
'Twinkle', 'Fireworks', 'Flag', 'Waves',
|
441
|
+
'Updown', 'Vintage', 'Fading', 'Collide',
|
442
|
+
'Strobe', 'Sparkles', 'Carnival', 'Glow'
|
443
|
+
]
|
444
|
+
}
|
445
|
+
break
|
446
|
+
case 'MLI': // Issue #439
|
447
|
+
this.capabilities.gamut = {
|
448
|
+
r: [0.68, 0.31],
|
449
|
+
g: [0.11, 0.82],
|
450
|
+
b: [0.13, 0.04]
|
451
|
+
}
|
452
|
+
if (this.capabilities.colorloop) {
|
453
|
+
this.capabilities.effects = [
|
454
|
+
'Sunset', 'Party', 'Worklight', 'Campfire', 'Romance', 'Nightlight'
|
455
|
+
]
|
456
|
+
}
|
457
|
+
break
|
458
|
+
case 'OSRAM':
|
459
|
+
this.capabilities.gamut = {
|
460
|
+
r: [0.6877, 0.3161],
|
461
|
+
g: [0.1807, 0.7282],
|
462
|
+
b: [0.1246, 0.0580]
|
463
|
+
}
|
464
|
+
break
|
465
|
+
case 'Philips':
|
466
|
+
case 'Signify Netherlands B.V.': {
|
467
|
+
// See: http://www.developers.meethue.com/documentation/supported-lights
|
468
|
+
this.manufacturer = 'Signify Netherlands B.V.'
|
469
|
+
this.capabilities.breathe = true
|
470
|
+
this.capabilities.computesXy = true
|
471
|
+
const gamut = hueGamutTypeByModel[this.model] || 'C'
|
472
|
+
this.capabilities.gamut = hueGamutType[gamut]
|
473
|
+
}
|
474
|
+
break
|
475
|
+
default:
|
476
|
+
break
|
477
|
+
}
|
478
|
+
}
|
225
479
|
}
|
226
480
|
|
227
481
|
module.exports = Resource
|
@@ -119,6 +119,8 @@ class Gateway extends homebridgeLib.AccessoryDelegate {
|
|
119
119
|
*/
|
120
120
|
this.accessoryByRpath = {}
|
121
121
|
|
122
|
+
this.defaultTransitionTime = 0.4
|
123
|
+
|
122
124
|
/** Map of errors by device ID trying to expose the corresponding accessory.
|
123
125
|
* @type {Object<string, Error>}
|
124
126
|
*/
|
@@ -158,6 +160,18 @@ class Gateway extends homebridgeLib.AccessoryDelegate {
|
|
158
160
|
.on('shutdown', this.shutdown)
|
159
161
|
}
|
160
162
|
|
163
|
+
get transitionTime () { return this.service.values.transitionTime }
|
164
|
+
|
165
|
+
async resetTransitionTime () {
|
166
|
+
if (this.resetting) {
|
167
|
+
return
|
168
|
+
}
|
169
|
+
this.resetting = true
|
170
|
+
await homebridgeLib.timeout(this.platform.config.waitTimeUpdate)
|
171
|
+
this.service.values.transitionTime = this.defaultTransitionTime
|
172
|
+
this.resetting = false
|
173
|
+
}
|
174
|
+
|
161
175
|
/** Log debug messages.
|
162
176
|
*/
|
163
177
|
identify () {
|
@@ -772,7 +786,7 @@ class Gateway extends homebridgeLib.AccessoryDelegate {
|
|
772
786
|
this.deleteService(id)
|
773
787
|
changed = true
|
774
788
|
} else {
|
775
|
-
/** Emitted when the
|
789
|
+
/** Emitted when the gateway has been polled.
|
776
790
|
* @event DeconzAccessory.Device#polled
|
777
791
|
* @param {Deconz.Device} device - The updated device.
|
778
792
|
*/
|
@@ -851,7 +865,7 @@ class Gateway extends homebridgeLib.AccessoryDelegate {
|
|
851
865
|
* unsupported resources.
|
852
866
|
*/
|
853
867
|
analyseResource (rtype, rid, body, logUnsupported) {
|
854
|
-
const resource = new Deconz.Resource(this
|
868
|
+
const resource = new Deconz.Resource(this, rtype, rid, body)
|
855
869
|
const { id, serviceName } = resource
|
856
870
|
if (id === this.id || serviceName === '') {
|
857
871
|
const debug = (logUnsupported ? this.debug : this.vdebug).bind(this)
|
@@ -0,0 +1,65 @@
|
|
1
|
+
// homebridge-deconz/lib/DeconzAccessory/Device.js
|
2
|
+
// Copyright © 2022 Erik Baauw. All rights reserved.
|
3
|
+
//
|
4
|
+
// Homebridge plugin for deCONZ.
|
5
|
+
|
6
|
+
'use strict'
|
7
|
+
|
8
|
+
const DeconzAccessory = require('.')
|
9
|
+
const DeconzService = require('../DeconzService')
|
10
|
+
|
11
|
+
/** Delegate class for a HomeKit accessory,
|
12
|
+
* corresponding to a Zigbee or virtual device on a deCONZ gateway,
|
13
|
+
* that is not yet supported.
|
14
|
+
* @extends DeconzAccessory
|
15
|
+
* @memberof DeconzAccessory
|
16
|
+
*/
|
17
|
+
class Light extends DeconzAccessory {
|
18
|
+
/** Instantiate a delegate for an accessory corresponding to a device.
|
19
|
+
* @param {DeconzAccessory.Gateway} gateway - The gateway.
|
20
|
+
* @param {Deconz.Device} device - The device.
|
21
|
+
*/
|
22
|
+
constructor (gateway, device) {
|
23
|
+
super(gateway, device)
|
24
|
+
|
25
|
+
this.identify()
|
26
|
+
|
27
|
+
this.service = new DeconzService.Light(this, device.resource)
|
28
|
+
|
29
|
+
this.settingsService = new DeconzService.DeviceSettings(this, {
|
30
|
+
name: this.name + ' Settings',
|
31
|
+
subtype: this.id,
|
32
|
+
resource: this.device.rpaths.join(', '),
|
33
|
+
expose: true,
|
34
|
+
logLevel: gateway.logLevel
|
35
|
+
})
|
36
|
+
|
37
|
+
this
|
38
|
+
.on('polled', (device) => {
|
39
|
+
this.debug('%s: polled: %j', device.resource.rpath, device.resource.body)
|
40
|
+
this.service.update(device.resource.body)
|
41
|
+
this.service.checkAdaptiveLighting()
|
42
|
+
})
|
43
|
+
.on('changed', (rpath, body) => {
|
44
|
+
if (rpath === device.resource.rpath) {
|
45
|
+
this.debug('%s: changed: %j', rpath, body)
|
46
|
+
this.service.update(body)
|
47
|
+
}
|
48
|
+
})
|
49
|
+
.on('identify', this.identify)
|
50
|
+
|
51
|
+
setImmediate(() => {
|
52
|
+
this.debug('initialised')
|
53
|
+
this.emit('initialised')
|
54
|
+
})
|
55
|
+
}
|
56
|
+
|
57
|
+
async identify () {
|
58
|
+
super.identify()
|
59
|
+
if (this.service != null) {
|
60
|
+
await this.service.identify()
|
61
|
+
}
|
62
|
+
}
|
63
|
+
}
|
64
|
+
|
65
|
+
module.exports = Light
|
@@ -11,25 +11,23 @@ const homebridgeLib = require('homebridge-lib')
|
|
11
11
|
* corresponding to a Zigbee or virtual device on a deCONZ gateway.
|
12
12
|
*/
|
13
13
|
class DeconzAccessory extends homebridgeLib.AccessoryDelegate {
|
14
|
-
static get Gateway () { return require('./Gateway') }
|
15
14
|
static get Device () { return require('./Device') }
|
15
|
+
static get Light () { return require('./Light') }
|
16
|
+
static get Gateway () { return require('./Gateway') }
|
16
17
|
|
17
18
|
/** Instantiate a delegate for an accessory corresponding to a device.
|
18
19
|
* @param {DeconzAccessory.Gateway} gateway - The gateway.
|
19
20
|
* @param {Deconz.Device} device - The device.
|
21
|
+
* @param {Accessory.Category} category - The HomeKit accessory category.
|
20
22
|
*/
|
21
|
-
constructor (gateway, device) {
|
22
|
-
|
23
|
+
constructor (gateway, device, category) {
|
24
|
+
// TODO device settings
|
23
25
|
super(gateway.platform, {
|
24
26
|
id: device.id,
|
25
|
-
name: body.name,
|
26
|
-
manufacturer: device.
|
27
|
-
|
28
|
-
|
29
|
-
model: device.zigbee ? body.modelid : body.type,
|
30
|
-
firmware: device.zigbee
|
31
|
-
? body.swversion == null ? '0.0.0' : body.swversion
|
32
|
-
: gateway.values.software,
|
27
|
+
name: device.resource.body.name,
|
28
|
+
manufacturer: device.resource.manufacturer,
|
29
|
+
model: device.resource.model,
|
30
|
+
firmware: device.resource.firmware,
|
33
31
|
category: category,
|
34
32
|
logLevel: gateway.logLevel
|
35
33
|
})
|
@@ -50,6 +48,11 @@ class DeconzAccessory extends homebridgeLib.AccessoryDelegate {
|
|
50
48
|
* @type {Deconz.Device}
|
51
49
|
*/
|
52
50
|
this.device = device
|
51
|
+
|
52
|
+
/** The API client instance for the gateway.
|
53
|
+
* @type {Deconz.ApiClient}
|
54
|
+
*/
|
55
|
+
this.client = gateway.client
|
53
56
|
}
|
54
57
|
|
55
58
|
/** The primary resource of the device.
|
@@ -129,10 +129,11 @@ class GatewaySettings extends homebridgeLib.ServiceDelegate {
|
|
129
129
|
})
|
130
130
|
|
131
131
|
this.addCharacteristicDelegate({
|
132
|
-
key: '
|
132
|
+
key: 'transitionTime',
|
133
133
|
Characteristic: this.Characteristics.my.TransitionTime,
|
134
|
-
value:
|
134
|
+
value: this.gateway.defaultTransitionTime
|
135
135
|
})
|
136
|
+
this.values.transitionTime = this.gateway.defaultTransitionTime
|
136
137
|
|
137
138
|
this.addCharacteristicDelegate({
|
138
139
|
key: 'unlock',
|
@@ -0,0 +1,469 @@
|
|
1
|
+
// homebridge-deconz/lib/DeconzService/Sensor.js
|
2
|
+
// Copyright © 2022 Erik Baauw. All rights reserved.
|
3
|
+
//
|
4
|
+
// Homebridge plugin for deCONZ.
|
5
|
+
|
6
|
+
'use strict'
|
7
|
+
|
8
|
+
const homebridgeLib = require('homebridge-lib')
|
9
|
+
const Deconz = require('../Deconz')
|
10
|
+
|
11
|
+
const { dateToString } = Deconz.ApiClient
|
12
|
+
const { timeout } = homebridgeLib
|
13
|
+
const { xyToHsv, hsvToXy, ctToXy } = homebridgeLib.Colour
|
14
|
+
|
15
|
+
class Light extends homebridgeLib.ServiceDelegate {
|
16
|
+
constructor (accessory, resource) {
|
17
|
+
super(accessory, {
|
18
|
+
id: resource.id,
|
19
|
+
name: resource.body.name,
|
20
|
+
subtype: resource.subtype,
|
21
|
+
Service: accessory.Services.hap.Lightbulb
|
22
|
+
})
|
23
|
+
this.id = resource.id
|
24
|
+
this.gateway = accessory.gateway
|
25
|
+
this.client = accessory.client
|
26
|
+
this.resource = resource
|
27
|
+
this.rtype = resource.rtype
|
28
|
+
this.rid = resource.rid
|
29
|
+
this.rpath = resource.rpath +
|
30
|
+
(resource.rtype === 'groups' ? '/action' : '/state')
|
31
|
+
this.capabilities = resource.capabilities
|
32
|
+
|
33
|
+
this.debug('%s: capabilities: %j', this.resource.rpath, this.capabilities)
|
34
|
+
|
35
|
+
this.targetState = {}
|
36
|
+
this.deferrals = []
|
37
|
+
|
38
|
+
this.addCharacteristicDelegate({
|
39
|
+
key: 'on',
|
40
|
+
Characteristic: this.Characteristics.hap.On,
|
41
|
+
value: this.capabilities.on
|
42
|
+
? this.resource.body.state.on
|
43
|
+
: this.resource.body.state.all_on,
|
44
|
+
setter: async (value) => { return this.put({ on: value }) }
|
45
|
+
}).on('didSet', (value, fromHomekit) => {
|
46
|
+
this.checkAdaptiveLighting()
|
47
|
+
})
|
48
|
+
|
49
|
+
if (!this.capabilities.on) {
|
50
|
+
this.addCharacteristicDelegate({
|
51
|
+
key: 'anyOn',
|
52
|
+
Characteristic: this.Characteristics.my.AnyOn,
|
53
|
+
value: this.resource.body.state.any_on,
|
54
|
+
setter: async (value) => { return this.put({ on: value }) }
|
55
|
+
}).on('didSet', (value, fromHomekit) => {
|
56
|
+
this.checkAdaptiveLighting()
|
57
|
+
})
|
58
|
+
}
|
59
|
+
|
60
|
+
if (this.capabilities.bri) {
|
61
|
+
this.brightnessDelegate = this.addCharacteristicDelegate({
|
62
|
+
key: 'brightness',
|
63
|
+
Characteristic: this.Characteristics.hap.Brightness,
|
64
|
+
unit: '%',
|
65
|
+
value: this.resource.body.state.bri,
|
66
|
+
setter: async (value) => {
|
67
|
+
const bri = Math.round(value * 254.0 / 100.0)
|
68
|
+
return this.put({ bri: bri })
|
69
|
+
}
|
70
|
+
}).on('didSet', (value, fromHomekit) => {
|
71
|
+
this.checkAdaptiveLighting()
|
72
|
+
})
|
73
|
+
|
74
|
+
this.addCharacteristicDelegate({
|
75
|
+
key: 'brightnessChange',
|
76
|
+
Characteristic: this.Characteristics.my.BrightnessChange,
|
77
|
+
value: 0,
|
78
|
+
setter: async (value) => {
|
79
|
+
return this.put({ bri_inc: Math.round(value * 254.0 / 100.0) })
|
80
|
+
}
|
81
|
+
}).on('didSet', async () => {
|
82
|
+
await timeout(this.platform.config.waitTimeReset)
|
83
|
+
this.values.brightnessChange = 0
|
84
|
+
})
|
85
|
+
this.values.brightnessChange = 0
|
86
|
+
}
|
87
|
+
|
88
|
+
if (this.capabilities.ct || this.capabilities.xy || this.capabilities.hs) {
|
89
|
+
this.addCharacteristicDelegate({
|
90
|
+
key: 'colormode',
|
91
|
+
value: this.resource.body.state.colormode
|
92
|
+
})
|
93
|
+
}
|
94
|
+
|
95
|
+
if (this.capabilities.ct) {
|
96
|
+
this.colorTemperatureDelegate = this.addCharacteristicDelegate({
|
97
|
+
key: 'colorTemperature',
|
98
|
+
Characteristic: this.Characteristics.hap.ColorTemperature,
|
99
|
+
unit: ' mired',
|
100
|
+
props: {
|
101
|
+
minValue: this.capabilities.ctMin,
|
102
|
+
maxValue: this.capabilities.ctMax
|
103
|
+
},
|
104
|
+
value: this.resource.body.state.ct,
|
105
|
+
setter: async (value) => {
|
106
|
+
const ct = Math.max(
|
107
|
+
this.capabilities.ctMin, Math.min(value, this.capabilities.ctMax)
|
108
|
+
)
|
109
|
+
return this.put({ ct: ct })
|
110
|
+
}
|
111
|
+
}).on('didSet', (value, fromHomeKit) => {
|
112
|
+
if (fromHomeKit) {
|
113
|
+
this.values.activeTransitionCount = 0
|
114
|
+
}
|
115
|
+
if (
|
116
|
+
this.capabilities.xy && !this.capabilities.computesXy &&
|
117
|
+
this.values.colormode === 'ct'
|
118
|
+
) {
|
119
|
+
const { h, s } = xyToHsv(ctToXy(value), this.capabilities.gamut)
|
120
|
+
this.values.hue = h
|
121
|
+
this.values.saturation = s
|
122
|
+
}
|
123
|
+
})
|
124
|
+
|
125
|
+
if (this.capabilities.bri) {
|
126
|
+
this.addCharacteristicDelegate({
|
127
|
+
key: 'supportedTransitionConfiguration',
|
128
|
+
Characteristic: this.Characteristics.hap
|
129
|
+
.SupportedCharacteristicValueTransitionConfiguration,
|
130
|
+
silent: true
|
131
|
+
})
|
132
|
+
this.addCharacteristicDelegate({
|
133
|
+
key: 'transitionControl',
|
134
|
+
Characteristic: this.Characteristics.hap
|
135
|
+
.CharacteristicValueTransitionControl,
|
136
|
+
silent: true,
|
137
|
+
getter: async () => {
|
138
|
+
await this.initAdaptiveLighting()
|
139
|
+
return this.adaptiveLighting.generateControl()
|
140
|
+
},
|
141
|
+
setter: async (value) => {
|
142
|
+
await this.initAdaptiveLighting()
|
143
|
+
this.adaptiveLighting.parseControl(value)
|
144
|
+
const response = this.adaptiveLighting.generateControlResponse()
|
145
|
+
this.values.activeTransitionCount = 1
|
146
|
+
return response
|
147
|
+
}
|
148
|
+
})
|
149
|
+
this.addCharacteristicDelegate({
|
150
|
+
key: 'activeTransitionCount',
|
151
|
+
Characteristic: this.Characteristics.hap
|
152
|
+
.CharacteristicValueActiveTransitionCount,
|
153
|
+
value: 0
|
154
|
+
}).on('didSet', (value) => {
|
155
|
+
if (!value) {
|
156
|
+
this.adaptiveLighting.deactivate()
|
157
|
+
} else {
|
158
|
+
this.checkAdaptiveLighting()
|
159
|
+
}
|
160
|
+
})
|
161
|
+
}
|
162
|
+
}
|
163
|
+
|
164
|
+
if (this.capabilities.xy) {
|
165
|
+
this.addCharacteristicDelegate({
|
166
|
+
key: 'hue',
|
167
|
+
Characteristic: this.Characteristics.hap.Hue,
|
168
|
+
unit: '°',
|
169
|
+
setter: async (value) => {
|
170
|
+
const xy = hsvToXy(value, this.values.saturation, this.capabilities.gamut)
|
171
|
+
return this.put({ xy: xy })
|
172
|
+
}
|
173
|
+
}).on('didSet', (value, fromHomekit) => {
|
174
|
+
if (fromHomekit) {
|
175
|
+
this.values.activeTransitionCount = 0
|
176
|
+
}
|
177
|
+
})
|
178
|
+
this.addCharacteristicDelegate({
|
179
|
+
key: 'saturation',
|
180
|
+
Characteristic: this.Characteristics.hap.Saturation,
|
181
|
+
unit: '%',
|
182
|
+
setter: async (value) => {
|
183
|
+
const xy = hsvToXy(this.values.hue, value, this.capabilities.gamut)
|
184
|
+
return this.put({ xy: xy })
|
185
|
+
}
|
186
|
+
}).on('didSet', (value, fromHomekit) => {
|
187
|
+
if (fromHomekit) {
|
188
|
+
this.values.activeTransitionCount = 0
|
189
|
+
}
|
190
|
+
})
|
191
|
+
} else if (this.capabilities.hs) {
|
192
|
+
this.addCharacteristicDelegate({
|
193
|
+
key: 'hue',
|
194
|
+
Characteristic: this.Characteristics.hap.Hue,
|
195
|
+
unit: '°',
|
196
|
+
setter: async (value) => {
|
197
|
+
const hue = Math.round(this.hk.hue * 65535.0 / 360.0)
|
198
|
+
return this.put({ hue: hue })
|
199
|
+
}
|
200
|
+
}).on('didSet', (value, fromHomekit) => {
|
201
|
+
if (fromHomekit) {
|
202
|
+
this.values.activeTransitionCount = 0
|
203
|
+
}
|
204
|
+
})
|
205
|
+
this.addCharacteristicDelegate({
|
206
|
+
key: 'saturation',
|
207
|
+
Characteristic: this.Characteristics.hap.Saturation,
|
208
|
+
unit: '%',
|
209
|
+
setter: async (value) => {
|
210
|
+
const sat = Math.round(this.hk.sat * 254.0 / 100.0)
|
211
|
+
return this.put({ sat: sat })
|
212
|
+
}
|
213
|
+
}).on('didSet', (value, fromHomekit) => {
|
214
|
+
if (fromHomekit) {
|
215
|
+
this.values.activeTransitionCount = 0
|
216
|
+
}
|
217
|
+
})
|
218
|
+
}
|
219
|
+
|
220
|
+
if (this.capabilities.colorLoop) {
|
221
|
+
this.addCharacteristicDelegate({
|
222
|
+
key: 'colorLoop',
|
223
|
+
Characteristic: this.Characteristics.my.ColorLoop,
|
224
|
+
setter: async (value) => {
|
225
|
+
const effect = value ? 'colorloop' : 'none'
|
226
|
+
const state = { effect: effect }
|
227
|
+
if (value) {
|
228
|
+
state.colorloopspeed = this.values.colorLoopSpeed
|
229
|
+
}
|
230
|
+
return this.put(state)
|
231
|
+
}
|
232
|
+
})
|
233
|
+
this.addCharacteristicDelegate({
|
234
|
+
key: 'colorLoopSpeed',
|
235
|
+
Characteristic: this.Characteristics.my.ColorLoopSpeed,
|
236
|
+
unit: 's',
|
237
|
+
value: 25,
|
238
|
+
setter: async (value) => {
|
239
|
+
return this.put({ effect: 'colorloop', colorloopspeed: value })
|
240
|
+
}
|
241
|
+
})
|
242
|
+
}
|
243
|
+
|
244
|
+
this.addCharacteristicDelegate({
|
245
|
+
key: 'lastSeen',
|
246
|
+
Characteristic: this.Characteristics.my.LastSeen
|
247
|
+
})
|
248
|
+
|
249
|
+
this.addCharacteristicDelegate({
|
250
|
+
key: 'statusFault',
|
251
|
+
Characteristic: this.Characteristics.hap.StatusFault
|
252
|
+
})
|
253
|
+
|
254
|
+
this.settings = {
|
255
|
+
brightnessAdjustment: 1,
|
256
|
+
resetTimeout: this.platform.config.resetTimeout,
|
257
|
+
waitTimeUpdate: this.platform.config.waitTimeUpdate,
|
258
|
+
wallSwitch: false
|
259
|
+
}
|
260
|
+
|
261
|
+
this.update(this.resource.body)
|
262
|
+
}
|
263
|
+
|
264
|
+
update (body) {
|
265
|
+
for (const key in body) {
|
266
|
+
const value = body[key]
|
267
|
+
switch (key) {
|
268
|
+
case 'action':
|
269
|
+
// Copied to `state` by `Resource` during polling.
|
270
|
+
break
|
271
|
+
case 'state':
|
272
|
+
this.updateState(value)
|
273
|
+
break
|
274
|
+
case 'lastannounced':
|
275
|
+
// this.values.lastBoot = dateToString(value)
|
276
|
+
break
|
277
|
+
case 'lastseen':
|
278
|
+
this.values.lastSeen = dateToString(value)
|
279
|
+
break
|
280
|
+
case 'colorcapabilities':
|
281
|
+
case 'ctmax':
|
282
|
+
case 'ctmin':
|
283
|
+
case 'devicemembership':
|
284
|
+
case 'etag':
|
285
|
+
case 'id':
|
286
|
+
case 'lights':
|
287
|
+
case 'hascolor':
|
288
|
+
case 'manufacturername':
|
289
|
+
case 'modelid':
|
290
|
+
case 'name':
|
291
|
+
case 'powerup':
|
292
|
+
case 'scenes':
|
293
|
+
case 'swversion':
|
294
|
+
case 'type':
|
295
|
+
case 'uniqueid':
|
296
|
+
break
|
297
|
+
default:
|
298
|
+
this.warn('%s: unknown %s attribute', key, this.rtype)
|
299
|
+
break
|
300
|
+
}
|
301
|
+
}
|
302
|
+
}
|
303
|
+
|
304
|
+
updateState (state) {
|
305
|
+
for (const key in state) {
|
306
|
+
const value = state[key]
|
307
|
+
switch (key) {
|
308
|
+
case 'all_on':
|
309
|
+
this.values.on = value
|
310
|
+
break
|
311
|
+
case 'any_on':
|
312
|
+
this.values.anyOn = value
|
313
|
+
break
|
314
|
+
case 'alert':
|
315
|
+
break
|
316
|
+
case 'bri':
|
317
|
+
this.values.brightness = Math.round(value * 100.0 / 254.0)
|
318
|
+
break
|
319
|
+
case 'colormode':
|
320
|
+
this.values.colormode = value
|
321
|
+
break
|
322
|
+
case 'ct':
|
323
|
+
this.values.colorTemperature = value
|
324
|
+
break
|
325
|
+
case 'effect':
|
326
|
+
break
|
327
|
+
case 'hue':
|
328
|
+
if (!this.capabilities.xy) {
|
329
|
+
this.values.hue = value
|
330
|
+
}
|
331
|
+
break
|
332
|
+
case 'on':
|
333
|
+
this.values.on = value
|
334
|
+
break
|
335
|
+
case 'reachable':
|
336
|
+
this.values.statusFault = value
|
337
|
+
? this.Characteristics.hap.StatusFault.NO_FAULT
|
338
|
+
: this.Characteristics.hap.StatusFault.GENERAL_FAULT
|
339
|
+
break
|
340
|
+
case 'sat':
|
341
|
+
if (!this.capabilities.xy) {
|
342
|
+
this.values.hue = value
|
343
|
+
}
|
344
|
+
break
|
345
|
+
case 'scene':
|
346
|
+
break
|
347
|
+
case 'xy':
|
348
|
+
if (this.values.colormode !== 'ct' || this.capabilities.computesXy) {
|
349
|
+
const { h, s } = xyToHsv(value, this.capabilities.gamut)
|
350
|
+
this.values.hue = h
|
351
|
+
this.values.saturation = s
|
352
|
+
}
|
353
|
+
break
|
354
|
+
default:
|
355
|
+
this.warn('state.%s: unknown %s attribute', key, this.rtype)
|
356
|
+
break
|
357
|
+
}
|
358
|
+
}
|
359
|
+
}
|
360
|
+
|
361
|
+
async identify () {
|
362
|
+
try {
|
363
|
+
if (this.capabilities.alert) {
|
364
|
+
if (this.capabilities.breathe) {
|
365
|
+
await this.put({ alert: 'breathe' })
|
366
|
+
await timeout(1500)
|
367
|
+
return this.put({ alert: 'stop' })
|
368
|
+
}
|
369
|
+
return this.put({ alert: 'select' })
|
370
|
+
}
|
371
|
+
} catch (error) { this.warn(error) }
|
372
|
+
}
|
373
|
+
|
374
|
+
async initAdaptiveLighting () {
|
375
|
+
if (this.adaptiveLighting == null) {
|
376
|
+
this.adaptiveLighting = new homebridgeLib.AdaptiveLighting(
|
377
|
+
this.brightnessDelegate, this.colorTemperatureDelegate
|
378
|
+
)
|
379
|
+
this.values.supportedTransitionConfiguration =
|
380
|
+
this.adaptiveLighting.generateConfiguration()
|
381
|
+
if (
|
382
|
+
this.values.activeTransitionCount === 1 &&
|
383
|
+
this.values.transitionControl != null
|
384
|
+
) {
|
385
|
+
this.adaptiveLighting.parseControl(this.values.transitionControl)
|
386
|
+
}
|
387
|
+
}
|
388
|
+
}
|
389
|
+
|
390
|
+
async checkAdaptiveLighting () {
|
391
|
+
if (this.adaptiveLighting == null || !this.values.on) {
|
392
|
+
return
|
393
|
+
}
|
394
|
+
const ct = this.adaptiveLighting.getCt(
|
395
|
+
this.values.brightness * this.settings.brightnessAdjustment
|
396
|
+
)
|
397
|
+
if (ct == null || ct === this.values.ct) {
|
398
|
+
return
|
399
|
+
}
|
400
|
+
return this.put({ ct: ct })
|
401
|
+
}
|
402
|
+
|
403
|
+
// Collect changes into a combined request.
|
404
|
+
async put (state) {
|
405
|
+
return new Promise((resolve, reject) => {
|
406
|
+
for (const key in state) {
|
407
|
+
this.targetState[key] = state[key]
|
408
|
+
}
|
409
|
+
const d = { resolve: resolve, reject: reject }
|
410
|
+
this.deferrals.push(d)
|
411
|
+
if (this.updating) {
|
412
|
+
return
|
413
|
+
}
|
414
|
+
this.updating = true
|
415
|
+
if (this.settings.waitTimeUpdate > 0) {
|
416
|
+
setTimeout(() => {
|
417
|
+
this._put()
|
418
|
+
}, this.settings.waitTimeUpdate)
|
419
|
+
} else {
|
420
|
+
this._put()
|
421
|
+
}
|
422
|
+
})
|
423
|
+
}
|
424
|
+
|
425
|
+
// Send the request (for the combined changes) to the gateway.
|
426
|
+
async _put () {
|
427
|
+
const targetState = this.targetState
|
428
|
+
const deferrals = this.deferrals
|
429
|
+
this.targetState = {}
|
430
|
+
this.deferrals = []
|
431
|
+
this.updating = false
|
432
|
+
if (
|
433
|
+
this.gateway.transitionTime !== this.gateway.defaultTransitionTime &&
|
434
|
+
targetState.transitiontime === undefined
|
435
|
+
) {
|
436
|
+
targetState.transitiontime = this.gateway.transitionTime * 10
|
437
|
+
this.gateway.resetTransitionTime()
|
438
|
+
}
|
439
|
+
if (this.capabilities.noTransition) {
|
440
|
+
if (
|
441
|
+
(
|
442
|
+
targetState.on != null || targetState.bri != null ||
|
443
|
+
targetState.bri_inc != null
|
444
|
+
) && (
|
445
|
+
targetState.xy != null || targetState.ct != null ||
|
446
|
+
targetState.hue != null || targetState.sat != null ||
|
447
|
+
targetState.effect != null
|
448
|
+
)
|
449
|
+
) {
|
450
|
+
targetState.transitiontime = 0
|
451
|
+
}
|
452
|
+
}
|
453
|
+
this.client.put(this.rpath, targetState).then((obj) => {
|
454
|
+
this.recentlyUpdated = true
|
455
|
+
for (const d of deferrals) {
|
456
|
+
d.resolve(true)
|
457
|
+
}
|
458
|
+
setTimeout(() => {
|
459
|
+
this.recentlyUpdated = false
|
460
|
+
}, 500)
|
461
|
+
}).catch((error) => {
|
462
|
+
for (const d of deferrals) {
|
463
|
+
d.reject(error)
|
464
|
+
}
|
465
|
+
})
|
466
|
+
}
|
467
|
+
}
|
468
|
+
|
469
|
+
module.exports = Light
|
@@ -6,7 +6,7 @@
|
|
6
6
|
'use strict'
|
7
7
|
|
8
8
|
const homebridgeLib = require('homebridge-lib')
|
9
|
-
const
|
9
|
+
const Deconz = require('../Deconz')
|
10
10
|
|
11
11
|
/**
|
12
12
|
* @memberof DeconzService
|
@@ -46,7 +46,7 @@ class Sensor extends homebridgeLib.ServiceDelegate {
|
|
46
46
|
}
|
47
47
|
|
48
48
|
update (sensor) {
|
49
|
-
this.values.lastUpdated =
|
49
|
+
this.values.lastUpdated = Deconz.Client.dateToString(sensor.state.lastupdated)
|
50
50
|
this.values.enabled = sensor.config.on
|
51
51
|
this.values.statusFault = sensor.config.reachable === false
|
52
52
|
? this.Characteristics.hap.StatusFault.GENERAL_FAULT
|
@@ -22,7 +22,7 @@ class DeconzService {
|
|
22
22
|
static get GatewaySettings () { return require('./GatewaySettings') }
|
23
23
|
// static get Humidity () { return require('./Humidity') }
|
24
24
|
// static get Light () { return require('./Light') }
|
25
|
-
|
25
|
+
static get Light () { return require('./Light') }
|
26
26
|
// static get LightLevel () { return require('./LightLevel') }
|
27
27
|
// static get Outlet () { return require('./Outlet') }
|
28
28
|
// static get OpenClose () { return require('./OpenClose') }
|
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.9",
|
8
8
|
"keywords": [
|
9
9
|
"homebridge-plugin",
|
10
10
|
"homekit",
|
@@ -25,7 +25,7 @@
|
|
25
25
|
"node": "^16.13.2"
|
26
26
|
},
|
27
27
|
"dependencies": {
|
28
|
-
"homebridge-lib": "~5.1.24-
|
28
|
+
"homebridge-lib": "~5.1.24-6",
|
29
29
|
"semver": "^7.3.5",
|
30
30
|
"ws": "^8.4.2",
|
31
31
|
"xml2js": "~0.4.23"
|