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 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.Client(this.clargs.options)
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 WsMonitor(options)
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) })
@@ -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 {string} gid - The device ID of the gateway.
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 (gid, rtype, rid, body) {
57
- toString('gid', gid, true)
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 = gid + this.subtype
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 'Group'
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 didSetway has been polled.
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.id, rtype, rid, body)
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
- const { body, category } = device.resource
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.zigbee
27
- ? body.manufacturername
28
- : gateway.values.manufacturer,
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: 'transitiontime',
132
+ key: 'transitionTime',
133
133
  Characteristic: this.Characteristics.my.TransitionTime,
134
- value: 0.4
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 deconzClient = require('../deconzClient')
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 = deconzClient.dateToString(sensor.state.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
- // static get LightBulb () { return require('./LightBulb') }
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.8",
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-5",
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"