homebridge-deconz 0.1.9 → 0.1.11

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.
@@ -0,0 +1,103 @@
1
+ // homebridge-deconz/lib/DeconzService/Label.js
2
+ // Copyright© 2022-2023 Erik Baauw. All rights reserved.
3
+ //
4
+ // Homebridge plugin for deCONZ.
5
+
6
+ 'use strict'
7
+
8
+ const DeconzService = require('.')
9
+
10
+ /**
11
+ * @memberof DeconzService
12
+ */
13
+ class Label extends DeconzService.SensorsResource {
14
+ constructor (accessory, resource, params = {}) {
15
+ params.Service = accessory.Services.hap.ServiceLabel
16
+ super(accessory, resource, params)
17
+
18
+ this.addCharacteristicDelegate({
19
+ key: 'namespace',
20
+ Characteristic: this.Characteristics.hap.ServiceLabelNamespace,
21
+ value: resource.capabilities.namespace
22
+ })
23
+
24
+ this.addCharacteristicDelegates({ noLastUpdated: true })
25
+
26
+ this.buttonResources = {}
27
+ this.buttonServices = {}
28
+ this.hasRepeat = false
29
+ }
30
+
31
+ createButtonServices (resource) {
32
+ if (resource.body.type.endsWith('Switch')) {
33
+ this.buttonResources[resource.rpath] = {
34
+ buttonEvent: resource.body.state.buttonevent,
35
+ lastUpdated: '',
36
+ toButtonEvent: resource.capabilities.toButtonEvent
37
+ }
38
+ } else if (resource.body.type.endsWith('RelativeRotary')) {
39
+ const keys = Object.keys(resource.capabilities.buttons)
40
+ this.buttonResources[resource.rpath] = {
41
+ lastUpdated: '',
42
+ right: keys[0],
43
+ left: keys[1]
44
+ }
45
+ }
46
+ for (const i in resource.capabilities.buttons) {
47
+ const { label, events, hasRepeat } = resource.capabilities.buttons[i]
48
+ if (hasRepeat) {
49
+ this.hasRepeat = hasRepeat
50
+ }
51
+ this.buttonServices[i] = new DeconzService.Button(this.accessoryDelegate, {
52
+ name: this.name + ' ' + label,
53
+ button: Number(i),
54
+ events,
55
+ hasRepeat
56
+ })
57
+ }
58
+ }
59
+
60
+ updateState (state, rpath) {
61
+ const buttonResource = this.buttonResources[rpath]
62
+ if (buttonResource.lastUpdated === '') {
63
+ buttonResource.lastUpdated = state.lastupdated
64
+ } else if (
65
+ state.lastupdated != null &&
66
+ state.lastupdated !== buttonResource.lastUpdated
67
+ ) {
68
+ buttonResource.lastUpdated = state.lastupdated
69
+ if (buttonResource.buttonEvent !== undefined) {
70
+ const oldValue = buttonResource.buttonEvent
71
+ if (state.buttonevent != null) {
72
+ buttonResource.buttonEvent = buttonResource.toButtonEvent == null
73
+ ? state.buttonevent
74
+ : buttonResource.toButtonEvent(state.buttonevent)
75
+ }
76
+ // TODO handle repeat
77
+ const i = Math.floor(buttonResource.buttonEvent / 1000)
78
+ this.buttonServices[i]?.update(
79
+ buttonResource.buttonEvent, oldValue,
80
+ false // this.accessoryDelegate.settingsService.values.repeat
81
+ )
82
+ } else {
83
+ const i = state.expectedrotation >= 0
84
+ ? buttonResource.right
85
+ : buttonResource.left
86
+ this.buttonServices[i].updateRotation()
87
+ }
88
+ }
89
+ super.updateState(state)
90
+ }
91
+
92
+ updateConfig (config) {
93
+ // TODO handle change in devicemode
94
+ super.updateConfig(config)
95
+ }
96
+
97
+ async identify () {
98
+ this.debug('hasRepeat: %j', this.hasRepeat)
99
+ return super.identify()
100
+ }
101
+ }
102
+
103
+ module.exports = Label
@@ -8,14 +8,31 @@
8
8
  const { AdaptiveLighting, Colour, ServiceDelegate, timeout } = require('homebridge-lib')
9
9
  const DeconzService = require('../DeconzService')
10
10
 
11
- const { xyToHsv, hsvToXy, ctToXy } = Colour
12
- const { History } = ServiceDelegate
11
+ const { defaultGamut, xyToHsv, hsvToXy, ctToXy } = Colour
13
12
 
14
13
  class Light extends DeconzService.LightsResource {
15
14
  constructor (accessory, resource, params = {}) {
16
15
  params.Service = accessory.Services.hap.Lightbulb
17
16
  super(accessory, resource, params)
18
17
 
18
+ this.capabilities = {
19
+ on: this.resource.body.state.on !== undefined,
20
+ bri: this.resource.body.state.bri !== undefined,
21
+ ct: this.resource.body.state.ct !== undefined,
22
+ hs: this.resource.body.state.xy === undefined &&
23
+ this.resource.body.state.hue !== undefined,
24
+ xy: this.resource.body.state.xy !== undefined
25
+ }
26
+ if (this.resource.body?.capabilities?.color?.effects != null) {
27
+ const effects = this.resource.body.capabilities.color.effects
28
+ if (effects.length > 1 && effects[1] === 'colorloop') {
29
+ this.capabilities.colorLoop = true
30
+ this.capabilities.effects = effects.length > 2
31
+ } else if (effects.length > 1) {
32
+ this.capabilities.effects = effects.length > 1
33
+ }
34
+ }
35
+
19
36
  this.addCharacteristicDelegate({
20
37
  key: 'on',
21
38
  Characteristic: this.Characteristics.hap.On,
@@ -81,11 +98,7 @@ class Light extends DeconzService.LightsResource {
81
98
  })
82
99
  }
83
100
 
84
- if (
85
- this.resource.body.config != null &&
86
- this.resource.body.config.color != null &&
87
- this.resource.body.config.color.execute_if_off != null
88
- ) {
101
+ if (this.resource.body?.config?.color?.execute_if_off != null) {
89
102
  this.addCharacteristicDelegate({
90
103
  key: 'colorExecuteIfOff',
91
104
  value: this.resource.body.config.color.execute_if_off
@@ -93,19 +106,27 @@ class Light extends DeconzService.LightsResource {
93
106
  }
94
107
 
95
108
  if (this.capabilities.ct) {
109
+ if (
110
+ this.resource.body?.capabilities?.color?.ct?.min == null ||
111
+ this.resource.body?.capabilities?.color?.ct?.max == null
112
+ ) {
113
+ if (this.capabilities.on) {
114
+ this.warn('using default ct range')
115
+ }
116
+ }
117
+ const ctMin = this.resource.body?.capabilities?.color?.ct?.min ?? 153
118
+ const ctMax = this.resource.body?.capabilities?.color?.ct?.max ?? 500
96
119
  this.colorTemperatureDelegate = this.addCharacteristicDelegate({
97
120
  key: 'colorTemperature',
98
121
  Characteristic: this.Characteristics.hap.ColorTemperature,
99
122
  unit: ' mired',
100
123
  props: {
101
- minValue: this.capabilities.ctMin,
102
- maxValue: this.capabilities.ctMax
124
+ minValue: ctMin,
125
+ maxValue: ctMax
103
126
  },
104
127
  value: this.resource.body.state.ct
105
128
  }).on('didSet', (value, fromHomeKit) => {
106
- const ct = Math.max(
107
- this.capabilities.ctMin, Math.min(value, this.capabilities.ctMax)
108
- )
129
+ const ct = Math.max(ctMin, Math.min(value, ctMax))
109
130
  if (fromHomeKit) {
110
131
  this.checkAdaptiveLighting()
111
132
  this.put({ ct })
@@ -120,6 +141,23 @@ class Light extends DeconzService.LightsResource {
120
141
  }
121
142
 
122
143
  if (this.capabilities.xy) {
144
+ if (
145
+ this.resource.body?.capabilities?.color?.xy?.blue == null ||
146
+ this.resource.body?.capabilities?.color?.xy?.green == null ||
147
+ this.resource.body?.capabilities?.color?.xy?.red == null
148
+ ) {
149
+ if (this.capabilities.on) {
150
+ this.warn('using default xy gamut')
151
+ }
152
+ this.capabilities.gamut = defaultGamut
153
+ } else {
154
+ this.capabilities.gamut = {
155
+ r: this.resource.body.capabilities.color.xy.red,
156
+ g: this.resource.body.capabilities.color.xy.green,
157
+ b: this.resource.body.capabilities.color.xy.blue
158
+ }
159
+ }
160
+
123
161
  this.addCharacteristicDelegate({
124
162
  key: 'hue',
125
163
  Characteristic: this.Characteristics.hap.Hue,
@@ -261,13 +299,7 @@ class Light extends DeconzService.LightsResource {
261
299
  }
262
300
 
263
301
  if (this.capabilities.effects != null) {
264
- this.addCharacteristicDelegate({
265
- key: 'exposeEffects',
266
- value: true
267
- })
268
- if (this.values.exposeEffects) {
269
- this.exposeEffects()
270
- }
302
+ this.exposeEffects()
271
303
  }
272
304
 
273
305
  if (this.resource.rtype === 'lights') {
@@ -279,57 +311,29 @@ class Light extends DeconzService.LightsResource {
279
311
  }
280
312
 
281
313
  exposeEffects () {
282
- this.effectServices = {}
283
- this.addCharacteristicDelegate({
284
- key: 'effect',
285
- value: -1,
314
+ const effectString = this.addCharacteristicDelegate({
315
+ key: 'effectString',
316
+ value: 'none',
286
317
  silent: true
287
- }).on('didSet', (value) => {
288
- for (const i in this.capabilities.effects) {
289
- this.effectServices[i].values.on = value === i
290
- }
291
318
  })
292
- for (const id in this.capabilities.effects) {
293
- const effect = this.capabilities.effects[id]
294
- const service = new ServiceDelegate(this.accessoryDelegate, {
295
- name: this.resource.body.name + ' ' + effect,
296
- Service: this.Services.hap.Lightbulb,
297
- subtype: (this.subtype == null ? 'E' : this.subtype + '-E') + id
298
- })
299
- service.addCharacteristicDelegate({
300
- key: 'on',
301
- Characteristic: this.Characteristics.hap.On
302
- }).on('didSet', (value, fromHomeKit) => {
303
- service.values.lastActivation =
304
- this.accessoryDelegate.historyService.lastActivationValue(History.now())
305
- if (fromHomeKit) {
306
- this.checkAdaptiveLighting()
307
- this.values.effect = value ? id : -1
308
- const newEffect = value
309
- ? effect.toLowerCase()
310
- : 'none'
311
- this.put({ effect: newEffect })
312
- }
313
- })
314
- service.addCharacteristicDelegate({
315
- key: 'index',
316
- Characteristic: this.Characteristics.hap.ServiceLabelIndex,
317
- value: Number(id) + 1
318
- })
319
- service.addCharacteristicDelegate({
320
- key: 'lastActivation',
321
- Characteristic: this.Characteristics.eve.LastActivation,
322
- silent: true
323
- })
324
- this.effectServices[id] = service
325
-
326
- this.characteristicDelegate('configuredName')
327
- .on('didSet', (value) => {
328
- for (const id in this.capabilities.effects) {
329
- this.effectServices[id].values.configuredName = value +
330
- ' ' + this.capabilities.effects[id]
319
+ for (const id in this.resource.body.capabilities.color.effects) {
320
+ const effect = this.resource.body.capabilities.color.effects[id]
321
+ const characteristicName = effect[0].toUpperCase() + effect.slice(1) + 'Effect'
322
+ if (this.Characteristics.my[characteristicName] != null) {
323
+ this.addCharacteristicDelegate({
324
+ key: effect,
325
+ Characteristic: this.Characteristics.my[characteristicName]
326
+ }).on('didSet', (value, fromHomeKit) => {
327
+ if (fromHomeKit) {
328
+ this.checkAdaptiveLighting()
329
+ this.put({ effect: value ? effect : 'none' })
330
+ this.values.effectString = value ? effect : 'none'
331
331
  }
332
332
  })
333
+ effectString.on('didSet', (value) => {
334
+ this.values[effect] = value === effect
335
+ })
336
+ }
333
337
  }
334
338
  // if (this.capabilities.effectSpeed) {
335
339
  // this.addCharacteristicDelegate({
@@ -403,9 +407,8 @@ class Light extends DeconzService.LightsResource {
403
407
  break
404
408
  case 'effect':
405
409
  this.values.colorLoop = value === 'colorloop'
406
- if (this.capabilities.effects != null) {
407
- const effectName = value[0].toUpperCase() + value.slice(1)
408
- this.values.effect = '' + this.capabilities.effects.indexOf(effectName)
410
+ if (this.capabilities.effects) {
411
+ this.values.effectString = value
409
412
  }
410
413
  break
411
414
  case 'hue':
@@ -430,8 +433,10 @@ class Light extends DeconzService.LightsResource {
430
433
  break
431
434
  case 'xy':
432
435
  if (
433
- !this.recentlyUpdated &&
434
- (this.values.colormode !== 'ct' || this.capabilities.computesXy)
436
+ !this.recentlyUpdated && (
437
+ this.values.colormode !== 'ct' ||
438
+ this.resource.body?.capabilities?.color?.ct?.computesXy
439
+ )
435
440
  ) {
436
441
  const { h, s } = xyToHsv(value, this.capabilities.gamut)
437
442
  this.values.hue = h
@@ -24,7 +24,7 @@ class Outlet extends DeconzService.LightsResource {
24
24
  }
25
25
  })
26
26
 
27
- if (!this.capabilities.on) {
27
+ if (this.resource.body.state.on === undefined) {
28
28
  this.addCharacteristicDelegate({
29
29
  key: 'anyOn',
30
30
  Characteristic: this.Characteristics.my.AnyOn,
@@ -15,10 +15,10 @@ const { dateToString } = Deconz.ApiClient
15
15
  */
16
16
  class Power extends DeconzService.SensorsResource {
17
17
  static addResource (service, resource) {
18
- if (service.values.currentConsumption === undefined) {
18
+ if (service.values.consumption === undefined) {
19
19
  service.addCharacteristicDelegate({
20
- key: 'currentConsumption',
21
- Characteristic: service.Characteristics.eve.CurrentConsumption,
20
+ key: 'consumption',
21
+ Characteristic: service.Characteristics.eve.Consumption,
22
22
  unit: ' W'
23
23
  })
24
24
  }
@@ -46,12 +46,13 @@ class Power extends DeconzService.SensorsResource {
46
46
  silent: true
47
47
  })
48
48
  }
49
- service.update(resource.body, resource.rpath)
49
+
50
+ Power.updateResourceState(service, resource.body.state)
50
51
  }
51
52
 
52
53
  static updateResourceState (service, state) {
53
54
  if (state.power != null) {
54
- service.values.currentConsumption = state.power
55
+ service.values.consumption = state.power
55
56
  }
56
57
  if (state.current != null) {
57
58
  service.values.electricCurrent = state.current / 1000
@@ -11,6 +11,30 @@ const DeconzService = require('../DeconzService')
11
11
  * @memberof DeconzService
12
12
  */
13
13
  class Status extends DeconzService.SensorsResource {
14
+ props (caps) {
15
+ if (caps.min == null || caps.max == null) {
16
+ return undefined
17
+ }
18
+ // Eve 3.1 displays the following controls, depending on the properties:
19
+ // 1. {minValue: 0, maxValue: 1, minStep: 1} switch
20
+ // 2. {minValue: a, maxValue: b, minStep: 1}, 1 < b - a <= 20 down|up
21
+ // 3. {minValue: a, maxValue: b}, (a, b) != (0, 1) slider
22
+ // 4. {minValue: a, maxValue: b, minStep: 1}, b - a > 20 slider
23
+ // Avoid the following bugs:
24
+ // 5. {minValue: 0, maxValue: 1} nothing
25
+ // 6. {minValue: a, maxValue: b, minStep: 1}, b - a = 1 switch*
26
+ // *) switch sends values 0 and 1 instead of a and b;
27
+ if (caps.min === 0 && caps.max === 1) {
28
+ // Workaround Eve bug (case 5 above).
29
+ return { minValue: caps.min, maxValue: caps.max, minStep: 1 }
30
+ }
31
+ if (caps.max - caps.min === 1) {
32
+ // Workaround Eve bug (case 6 above).
33
+ return { minValue: caps.min, maxValue: caps.max }
34
+ }
35
+ return { minValue: caps.min, maxValue: caps.max, minStep: 1 }
36
+ }
37
+
14
38
  constructor (accessory, resource, params = {}) {
15
39
  params.Service = accessory.Services.my.Status
16
40
  super(accessory, resource, params)
@@ -29,7 +53,7 @@ class Status extends DeconzService.SensorsResource {
29
53
  this.addCharacteristicDelegate({
30
54
  key: 'status',
31
55
  Characteristic: this.Characteristics.my.Status,
32
- props: resource.capabilities.props
56
+ props: this.props(resource.capabilities)
33
57
  }).on('didSet', async (value, fromHomeKit) => {
34
58
  if (fromHomeKit) {
35
59
  await this.put('/state', { status: value })
@@ -5,101 +5,66 @@
5
5
 
6
6
  'use strict'
7
7
 
8
- const DeconzService = require('../DeconzService')
8
+ const DeconzService = require('.')
9
9
 
10
- /**
11
- * @memberof DeconzService
12
- */
13
- class Switch extends DeconzService.SensorsResource {
10
+ class Switch extends DeconzService.LightsResource {
14
11
  constructor (accessory, resource, params = {}) {
15
- params.Service = accessory.Services.hap.ServiceLabel
12
+ params.Service = accessory.Services.hap.Switch
16
13
  super(accessory, resource, params)
17
14
 
18
15
  this.addCharacteristicDelegate({
19
- key: 'namespace',
20
- Characteristic: this.Characteristics.hap.ServiceLabelNamespace,
21
- value: resource.capabilities.namespace
16
+ key: 'on',
17
+ Characteristic: this.Characteristics.hap.On,
18
+ value: this.capabilities.on
19
+ ? this.resource.body.state.on
20
+ : this.resource.body.state.all_on
21
+ }).on('didSet', (value, fromHomeKit) => {
22
+ if (fromHomeKit) {
23
+ this.put({ on: value })
24
+ }
22
25
  })
23
26
 
24
- this.addCharacteristicDelegates({ noLastUpdated: true })
27
+ if (this.resource.body.state.on === undefined) {
28
+ this.addCharacteristicDelegate({
29
+ key: 'anyOn',
30
+ Characteristic: this.Characteristics.my.AnyOn,
31
+ value: this.resource.body.state.any_on
32
+ }).on('didSet', (value, fromHomeKit) => {
33
+ if (fromHomeKit) {
34
+ this.put({ on: value })
35
+ }
36
+ })
37
+ }
25
38
 
26
- this.buttonResources = {}
27
- this.buttonServices = {}
28
- this.hasRepeat = false
29
- }
39
+ this.addCharacteristicDelegates()
30
40
 
31
- createButtonServices (resource) {
32
- if (resource.body.type.endsWith('Switch')) {
33
- this.buttonResources[resource.rpath] = {
34
- buttonEvent: resource.body.state.buttonevent,
35
- lastUpdated: '',
36
- toButtonEvent: resource.capabilities.toButtonEvent
37
- }
38
- } else if (resource.body.type.endsWith('RelativeRotary')) {
39
- const keys = Object.keys(resource.capabilities.buttons)
40
- this.buttonResources[resource.rpath] = {
41
- lastUpdated: '',
42
- right: keys[0],
43
- left: keys[1]
44
- }
45
- }
46
- for (const i in resource.capabilities.buttons) {
47
- const { label, events, hasRepeat } = resource.capabilities.buttons[i]
48
- if (hasRepeat) {
49
- this.hasRepeat = hasRepeat
50
- }
51
- this.buttonServices[i] = new DeconzService.Button(this.accessoryDelegate, {
52
- name: this.name + ' ' + label,
53
- button: Number(i),
54
- events,
55
- hasRepeat
56
- })
41
+ this.settings = {
42
+ resetTimeout: this.platform.config.resetTimeout,
43
+ waitTimeUpdate: this.platform.config.waitTimeUpdate,
44
+ wallSwitch: false
57
45
  }
58
46
  }
59
47
 
60
48
  updateState (state, rpath) {
61
- const buttonResource = this.buttonResources[rpath]
62
- if (buttonResource.lastUpdated === '') {
63
- buttonResource.lastUpdated = state.lastupdated
64
- } else if (
65
- state.lastupdated != null &&
66
- state.lastupdated !== buttonResource.lastUpdated
67
- ) {
68
- buttonResource.lastUpdated = state.lastupdated
69
- if (buttonResource.buttonEvent !== undefined) {
70
- const oldValue = buttonResource.buttonEvent
71
- if (state.buttonevent != null) {
72
- buttonResource.buttonEvent = buttonResource.toButtonEvent == null
73
- ? state.buttonevent
74
- : buttonResource.toButtonEvent(state.buttonevent)
75
- }
76
- // TODO handle repeat
77
- const i = Math.floor(buttonResource.buttonEvent / 1000)
78
- if (this.buttonServices[i] != null) {
79
- this.buttonServices[i].update(
80
- buttonResource.buttonEvent, oldValue,
81
- false // this.accessoryDelegate.settingsService.values.repeat
82
- )
83
- }
84
- } else {
85
- const i = state.expectedrotation >= 0
86
- ? buttonResource.right
87
- : buttonResource.left
88
- this.buttonServices[i].updateRotation()
49
+ for (const key in state) {
50
+ const value = state[key]
51
+ this.resource.body.state[key] = value
52
+ switch (key) {
53
+ case 'all_on':
54
+ this.values.on = value
55
+ break
56
+ case 'any_on':
57
+ this.values.anyOn = value
58
+ break
59
+ case 'on':
60
+ this.values.on = value
61
+ break
62
+ default:
63
+ break
89
64
  }
90
65
  }
91
66
  super.updateState(state)
92
67
  }
93
-
94
- updateConfig (config) {
95
- // TODO handle change in devicemode
96
- super.updateConfig(config)
97
- }
98
-
99
- async identify () {
100
- this.debug('hasRepeat: %j', this.hasRepeat)
101
- return super.identify()
102
- }
103
68
  }
104
69
 
105
70
  module.exports = Switch
@@ -27,6 +27,7 @@ class DeconzService extends ServiceDelegate {
27
27
  static get Flag () { return require('./Flag') }
28
28
  static get Gateway () { return require('./Gateway') }
29
29
  static get Humidity () { return require('./Humidity') }
30
+ static get Label () { return require('./Label') }
30
31
  static get Leak () { return require('./Leak') }
31
32
  static get Light () { return require('./Light') }
32
33
  static get LightsResource () { return require('./LightsResource') }
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.1.9",
7
+ "version": "0.1.11",
8
8
  "keywords": [
9
9
  "homebridge-plugin",
10
10
  "homekit",
@@ -21,13 +21,13 @@
21
21
  "ui": "cli/ui.js"
22
22
  },
23
23
  "engines": {
24
- "deCONZ": "2.19.3",
24
+ "deCONZ": "2.21.2",
25
25
  "homebridge": "^1.6.0",
26
- "node": "^18.14.2"
26
+ "node": "^18.15.0"
27
27
  },
28
28
  "dependencies": {
29
- "homebridge-lib": "~6.3.11",
30
- "ws": "^8.12.1",
29
+ "homebridge-lib": "~6.3.13",
30
+ "ws": "^8.13.0",
31
31
  "xml2js": "~0.4.23"
32
32
  },
33
33
  "scripts": {