homebridge-deconz 0.0.14 → 0.0.17

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
@@ -703,7 +703,7 @@ class Main extends homebridgeLib.CommandLineTool {
703
703
  const { websocketport } = await this.client.get('/config')
704
704
  options.host = this.client.host + ':' + websocketport
705
705
  this.wsMonitor = new Deconz.WsClient(options)
706
- this.setOptions({ mode: mode })
706
+ this.setOptions({ mode })
707
707
  this.wsMonitor
708
708
  .on('error', (error) => { this.error(error) })
709
709
  .on('listening', (url) => { this.log('listening on %s', url) })
@@ -813,7 +813,7 @@ class Main extends homebridgeLib.CommandLineTool {
813
813
  .parse(...args)
814
814
  const apiKey = await this.client.getApiKey('deconz')
815
815
  this.print(jsonFormatter.stringify(apiKey))
816
- this.gateways[this.bridgeid] = { apiKey: apiKey }
816
+ this.gateways[this.bridgeid] = { apiKey }
817
817
  if (this.client.fingerprint != null) {
818
818
  this.gateways[this.bridgeid].fingerprint = this.client.fingerprint
819
819
  }
@@ -81,9 +81,8 @@ class ApiClient extends homebridgeLib.HttpClient {
81
81
  * @return {integer} lux - The value in lux.
82
82
  */
83
83
  static lightLevelToLux (v) {
84
- let lux = v ? Math.pow(10, (v - 1) / 10000) : 0.0001
85
- lux = Math.round(lux * 10000) / 10000
86
- return Math.max(0.0001, Math.min(lux, 100000))
84
+ v = Math.max(0, Math.min(v, 60001))
85
+ return v ? Math.round(Math.pow(10, (v - 1) / 10000) * 10) / 10 : 0.0001
87
86
  }
88
87
 
89
88
  /** Create a new instance of a Deconz.Client.
@@ -149,7 +148,7 @@ class ApiClient extends homebridgeLib.HttpClient {
149
148
  maxSockets: _options.maxSockets,
150
149
  path: '/api',
151
150
  timeout: _options.timeout,
152
- validStatusCodes: [200, 400, 403, 404]
151
+ validStatusCodes: [200, 400, 403] //, 404]
153
152
  }
154
153
  if (_options.phoscon) {
155
154
  // options.headers = { Accept: 'application/vnd.ddel.v1' }
@@ -44,7 +44,7 @@ class Discovery extends events.EventEmitter {
44
44
  */
45
45
  async config (host) {
46
46
  const client = new homebridgeLib.HttpClient({
47
- host: host,
47
+ host,
48
48
  json: true,
49
49
  path: '/api',
50
50
  timeout: this._options.timeout
@@ -96,7 +96,7 @@ class Discovery extends events.EventEmitter {
96
96
  */
97
97
  async description (host) {
98
98
  const options = {
99
- host: host,
99
+ host,
100
100
  timeout: this._options.timeout
101
101
  }
102
102
  const client = new homebridgeLib.HttpClient(options)
@@ -21,8 +21,9 @@ const sensorsPrios = [
21
21
  'Power',
22
22
  'Consumption',
23
23
  'Temperature',
24
- 'Presence',
24
+ 'Motion',
25
25
  'OpenClose',
26
+ 'AirPurifier',
26
27
  'Thermostat'
27
28
  ]
28
29
 
@@ -256,7 +257,7 @@ class Resource {
256
257
  get prio () {
257
258
  if (this.rtype === 'groups') return -1
258
259
  if (this.rtype === 'lights') return this.endpoint
259
- return sensorsPrios.indexOf(this.type)
260
+ return sensorsPrios.indexOf(this.serviceName)
260
261
  }
261
262
 
262
263
  /** The resource path of the resource, e.g. `/lights/1`.
@@ -270,7 +271,7 @@ class Resource {
270
271
  * corresponding HomeKit service, or `null` for unsupported and unknown
271
272
  * resources.
272
273
  *
273
- * This is derived from the resource type and`type` in the resource body.
274
+ * This is derived from the resource type and `type` in the resource body.
274
275
  * @type {string}
275
276
  */
276
277
  get serviceName () {
@@ -305,6 +306,8 @@ class Resource {
305
306
  }
306
307
  } else { // (this.rtype === 'sensors')
307
308
  switch (this.body.type) {
309
+ case 'CLIPAirPurifier':
310
+ case 'ZHAAirPurifier': return 'AirPurifier'
308
311
  case 'ZHAAirQuality':
309
312
  case 'CLIPAirQuality': return 'AirQuality'
310
313
  case 'ZHAAlarm':
@@ -0,0 +1,38 @@
1
+ // homebridge-deconz/lib/DeconzAccessory/Thermostat.js
2
+ // Copyright © 2022 Erik Baauw. All rights reserved.
3
+ //
4
+ // Homebridge plugin for deCONZ.
5
+
6
+ 'use strict'
7
+
8
+ const DeconzAccessory = require('../DeconzAccessory')
9
+
10
+ class AirPurifier extends DeconzAccessory {
11
+ /** Instantiate a delegate for an accessory corresponding to a device.
12
+ * @param {DeconzAccessory.Gateway} gateway - The gateway.
13
+ * @param {Deconz.Device} device - The device.
14
+ */
15
+ constructor (gateway, device, settings = {}) {
16
+ super(gateway, device, gateway.Accessory.Categories.AIR_PURIFIER)
17
+ this.identify()
18
+
19
+ this.service = this.createService(device.resource, { primaryService: true })
20
+
21
+ for (const subtype in device.resourceBySubtype) {
22
+ const resource = device.resourceBySubtype[subtype]
23
+ if (subtype === device.primary) {
24
+ continue
25
+ }
26
+ this.createService(resource)
27
+ }
28
+
29
+ this.createSettingsService()
30
+
31
+ setImmediate(() => {
32
+ this.debug('initialised')
33
+ this.emit('initialised')
34
+ })
35
+ }
36
+ }
37
+
38
+ module.exports = AirPurifier
@@ -44,6 +44,7 @@ class Gateway extends homebridgeLib.AccessoryDelegate {
44
44
 
45
45
  this.gateway = this
46
46
  this.id = params.config.bridgeid
47
+ this.recommendedSoftware = this.platform.packageJson.engines.deCONZ
47
48
 
48
49
  /** Persisted properties.
49
50
  * @type {Object}
@@ -53,12 +54,20 @@ class Gateway extends homebridgeLib.AccessoryDelegate {
53
54
  * devices.
54
55
  * @property {Object} fullState - The gateway's full state, from the
55
56
  * last time the gateway was polled.
57
+ * @property {Object} settings - The persisted settings, maintained through
58
+ * the Homebridge UI.
56
59
  */
57
60
  this.context // eslint-disable-line no-unused-expressions
58
61
  this.context.host = params.host
59
62
  this.context.config = params.config
60
- if (this.context.blacklist == null) {
61
- this.context.blacklist = {}
63
+ if (this.context.settingsById == null) {
64
+ this.context.settingsById = {}
65
+ // migration
66
+ for (const id in this.context.blacklist) {
67
+ this.context.settingsById[id] = { expose: false }
68
+ }
69
+ delete this.context.blacklist
70
+ // end migration
62
71
  }
63
72
  if (this.context.fullState != null) {
64
73
  this.analyseFullState(this.context.fullState, { analyseOnly: true })
@@ -109,6 +118,9 @@ class Gateway extends homebridgeLib.AccessoryDelegate {
109
118
  '%s %s gateway v%s', this.values.manufacturer, this.values.model,
110
119
  this.values.software
111
120
  )
121
+ if (this.values.software !== this.recommendedSoftware) {
122
+ this.warn('recommended version: deCONZ v%s', this.recommendedSoftware)
123
+ }
112
124
 
113
125
  /** Map of Accessory delegates by id for the gateway.
114
126
  * @type {Object<string, DeconzAccessory.Device>}
@@ -157,7 +169,7 @@ class Gateway extends homebridgeLib.AccessoryDelegate {
157
169
  this.heartbeatEnabled = true
158
170
  this
159
171
  .on('identify', this.identify)
160
- .once('heartbeat', this.init)
172
+ .once('heartbeat', (beat) => { this.initialBeat = beat })
161
173
  .on('heartbeat', this.heartbeat)
162
174
  .on('shutdown', this.shutdown)
163
175
  }
@@ -182,6 +194,15 @@ class Gateway extends homebridgeLib.AccessoryDelegate {
182
194
  this.values.manufacturer, this.values.model, this.values.software,
183
195
  this.nAccessories, this.nDevices, this.nResourcesMonitored
184
196
  )
197
+ if (this.values.software !== this.recommendedSoftware) {
198
+ this.warn('recommended version: deCONZ v%s', this.recommendedSoftware)
199
+ }
200
+ if (this.context.migration != null) {
201
+ this.log(
202
+ 'migration: %s: %d resources',
203
+ this.context.migration, this.nResourcesMonitored
204
+ )
205
+ }
185
206
  if (this.logLevel > 2) {
186
207
  this.vdebug(
187
208
  '%d gateway resouces: %j', this.nResources,
@@ -204,34 +225,30 @@ class Gateway extends homebridgeLib.AccessoryDelegate {
204
225
  '%d accessories with expose errors: %j', exposeErrors.length,
205
226
  exposeErrors
206
227
  )
207
- const blacklist = Object.keys(this.context.blacklist).sort()
228
+ const settings = Object.keys(this.context.settingsById).sort()
208
229
  this.vdebug(
209
- 'blacklist: %d devices: %j', blacklist.length, blacklist)
230
+ 'settings: %d devices: %j', settings.length, settings)
210
231
  }
211
232
  }
212
233
 
213
- /** Initialise the gateway delegate.
214
- */
215
- async init (beat) {
216
- try {
217
- this.debug('initialising...')
218
- this.initialBeat = beat
219
- await this.connect()
220
- this.initialised = true
221
- this.debug('initialised')
222
- this.emit('initialised')
223
- } catch (error) { this.error(error) }
224
- }
225
-
226
234
  /** Update properties from gateway announcement.
227
235
  * @param {string} host - The gateway hostname or IP address and port.
228
236
  * @param {Object} config - The response body of an unauthenticated
229
237
  * GET `/config` (from {@link DeconzDiscovery#config config()}.
230
238
  */
231
- found (host, config) {
232
- this.values.host = host
233
- this.context.config = config
234
- this.values.software = config.swversion
239
+ async found (host, config) {
240
+ try {
241
+ this.context.host = host
242
+ this.values.host = host
243
+ this.context.config = config
244
+ this.values.software = config.swversion
245
+ if (!this.initialised) {
246
+ this.debug('initialising...')
247
+ await this.connect()
248
+ }
249
+ } catch (error) {
250
+ this.error(error)
251
+ }
235
252
  }
236
253
 
237
254
  async shutdown () {
@@ -412,8 +429,8 @@ class Gateway extends homebridgeLib.AccessoryDelegate {
412
429
  for (const id in this.exposeErrorById) {
413
430
  this.resetExposeError(id)
414
431
  }
415
- this.context.fullState = null
416
432
  this.pollNext = true
433
+ this.pollFullState = true
417
434
  } catch (error) {
418
435
  if (
419
436
  error instanceof Deconz.ApiError && error.type === 101 && retry < 8
@@ -456,7 +473,14 @@ class Gateway extends homebridgeLib.AccessoryDelegate {
456
473
  this.deleteService(id)
457
474
  }
458
475
  this.exposeErrors = {}
459
- this.context.blacklist = {}
476
+ this.context.settingsById = {}
477
+ this.context.settingsById[this.id] = {
478
+ autoExposeGroups: false,
479
+ autoExposeLights: false,
480
+ autoExposeSensors: false,
481
+ autoExposeSchedules: false,
482
+ logLevel: 2
483
+ }
460
484
  this.context.fullState = null
461
485
  this.context.migration = null
462
486
  this.service.values.lights = false
@@ -485,11 +509,7 @@ class Gateway extends homebridgeLib.AccessoryDelegate {
485
509
  if (this.deviceById[id] == null) {
486
510
  throw new RangeError(`${id}: unknown device ID`)
487
511
  }
488
- if (expose) {
489
- delete this.context.blacklist[id]
490
- } else {
491
- this.context.blacklist[id] = true
492
- }
512
+ this.context.settingsById[id].expose = expose
493
513
  this.pollNext = true
494
514
  }
495
515
 
@@ -605,7 +625,7 @@ class Gateway extends homebridgeLib.AccessoryDelegate {
605
625
  name: body.name + ' Settings',
606
626
  subtype: id,
607
627
  resource: rpaths.join(', '),
608
- expose: this.context.blacklist[id] == null
628
+ expose: this.context.settingsById[id].expose
609
629
  })
610
630
  this.serviceById[id] = service
611
631
  }
@@ -678,9 +698,11 @@ class Gateway extends homebridgeLib.AccessoryDelegate {
678
698
  try {
679
699
  this.polling = true
680
700
  this.vdebug('%spolling...', this.pollNext ? 'priority ' : '')
681
- if (this.context.fullState == null) {
682
- this.context.fullState = await this.client.get('/')
683
- this.context.fullState.groups[0] = await this.client.get('/groups/0')
701
+ if (this.context.fullState == null || this.pollFullState) {
702
+ const fullState = await this.client.get('/')
703
+ fullState.groups[0] = await this.client.get('/groups/0')
704
+ this.context.fullState = fullState
705
+ this.pollFullState = false
684
706
  } else {
685
707
  const config = await this.client.get('/config')
686
708
  if (config.bridgeid === this.id && config.UTC == null) {
@@ -713,6 +735,11 @@ class Gateway extends homebridgeLib.AccessoryDelegate {
713
735
  this.pollNext = false
714
736
  this.polling = false
715
737
  }
738
+ if (!this.initialised) {
739
+ this.initialised = true
740
+ this.debug('initialised')
741
+ this.emit('initialised')
742
+ }
716
743
  }
717
744
 
718
745
  /** Analyse the peristed full state of the gateway,
@@ -838,7 +865,7 @@ class Gateway extends homebridgeLib.AccessoryDelegate {
838
865
  if (
839
866
  this.deviceById[id] == null
840
867
  ) {
841
- delete this.context.blacklist[id]
868
+ delete this.context.settingsById[id]
842
869
  this.deleteAccessory(id)
843
870
  this.deleteService(id)
844
871
  changed = true
@@ -872,7 +899,10 @@ class Gateway extends homebridgeLib.AccessoryDelegate {
872
899
  for (const rid of rids) {
873
900
  try {
874
901
  const { id } = this.deviceByRidByRtype[rtype][rid]
875
- if (this.context.blacklist[id] == null) {
902
+ if (this.context.settingsById[id] == null) {
903
+ this.context.settingsById[id] = { expose: true }
904
+ }
905
+ if (this.context.settingsById[id].expose) {
876
906
  if (this.accessoryById[id] == null) {
877
907
  this.addAccessory(id)
878
908
  changed = true
@@ -896,6 +926,7 @@ class Gateway extends homebridgeLib.AccessoryDelegate {
896
926
  }
897
927
 
898
928
  this.nAccessories = Object.keys(this.accessoryById).length
929
+ this.nResourcesMonitored = Object.keys(this.accessoryByRpath).length
899
930
  this.nExposeErrors = Object.keys(this.exposeErrorById).length
900
931
  if (this.nExposeErrors === 0) {
901
932
  this.vdebug('%d accessories', this.nAccessories)
@@ -906,8 +937,6 @@ class Gateway extends homebridgeLib.AccessoryDelegate {
906
937
  }
907
938
 
908
939
  if (changed) {
909
- this.nResourcesMonitored = Object.keys(this.accessoryByRpath).length
910
- this.identify()
911
940
  if (this.context.migration == null) {
912
941
  const response = await this.client.post('/resourcelinks', {
913
942
  name: 'homebridge-deconz',
@@ -921,10 +950,7 @@ class Gateway extends homebridgeLib.AccessoryDelegate {
921
950
  links: Object.keys(this.accessoryByRpath).sort()
922
951
  })
923
952
  }
924
- this.log(
925
- 'migration: %s: %d resources',
926
- this.context.migration, this.nResourcesMonitored
927
- )
953
+ this.identify()
928
954
  }
929
955
  }
930
956
 
@@ -34,6 +34,9 @@ class Motion extends DeconzAccessory {
34
34
  this, {},
35
35
  this.service.characteristicDelegate('motion'),
36
36
  this.service.characteristicDelegate('lastActivation'),
37
+ this.serviceByServiceName.LightLevel == null
38
+ ? null
39
+ : this.serviceByServiceName.LightLevel.characteristicDelegate('lightlevel'),
37
40
  this.serviceByServiceName.Temperature == null
38
41
  ? null
39
42
  : this.serviceByServiceName.Temperature.characteristicDelegate('temperature')
@@ -39,7 +39,7 @@ class DeconzAccessory extends homebridgeLib.AccessoryDelegate {
39
39
  manufacturer: device.resource.manufacturer,
40
40
  model: device.resource.model,
41
41
  firmware: device.resource.firmware,
42
- category: category
42
+ category
43
43
  })
44
44
 
45
45
  this.context.gid = gateway.id
@@ -70,13 +70,17 @@ class DeconzPlatform extends homebridgeLib.Platform {
70
70
  })
71
71
  this.discovery
72
72
  .on('error', (error) => {
73
- this.log(
74
- '%s: request %d: %s %s', error.request.name,
75
- error.request.id, error.request.method, error.request.resource
76
- )
77
- this.warn(
78
- '%s: request %d: %s', error.request.name, error.request.id, error
79
- )
73
+ if (error instanceof homebridgeLib.HttpClient.HttpError) {
74
+ this.log(
75
+ '%s: request %d: %s %s', error.request.name,
76
+ error.request.id, error.request.method, error.request.resource
77
+ )
78
+ this.warn(
79
+ '%s: request %d: %s', error.request.name, error.request.id, error
80
+ )
81
+ return
82
+ }
83
+ this.warn(error)
80
84
  })
81
85
  .on('request', (request) => {
82
86
  this.debug(
@@ -105,12 +109,10 @@ class DeconzPlatform extends homebridgeLib.Platform {
105
109
  async foundGateway (host, config) {
106
110
  const id = config.bridgeid
107
111
  if (this.gatewayMap[id] == null) {
108
- this.gatewayMap[id] = new DeconzAccessory.Gateway(this, {
109
- config: config,
110
- host: host
111
- })
112
+ this.gatewayMap[id] = new DeconzAccessory.Gateway(this, { config, host })
112
113
  }
113
- this.gatewayMap[id].found(host, config)
114
+ await this.gatewayMap[id].found(host, config)
115
+ await events.once(this.gatewayMap[id], 'initialised')
114
116
  this.emit('found')
115
117
  }
116
118
 
@@ -128,17 +130,25 @@ class DeconzPlatform extends homebridgeLib.Platform {
128
130
  async init () {
129
131
  try {
130
132
  const jobs = []
131
- this.debug('job %d: find at least one gateway', jobs.length)
132
- jobs.push(events.once(this, 'found'))
133
133
  if (this.config.hosts.length > 0) {
134
134
  for (const host of this.config.hosts) {
135
135
  this.debug('job %d: find gateway at %s', jobs.length, host)
136
136
  jobs.push(this.findHost(host))
137
137
  }
138
138
  } else {
139
+ this.debug('job %d: find at least one gateway', jobs.length)
140
+ jobs.push(events.once(this, 'found'))
139
141
  for (const id in this.gatewayMap) {
142
+ const gateway = this.gatewayMap[id]
143
+ const host = gateway.context.host
140
144
  this.debug('job %d: find gateway %s', jobs.length, id)
141
- jobs.push(this.gatewayMap[id].init())
145
+ jobs.push(events.once(gateway, 'initialised'))
146
+ try {
147
+ const config = await this.discovery.config(host)
148
+ await this.foundGateway(host, config)
149
+ } catch (error) {
150
+ this.warn('%s: %s', id, error)
151
+ }
142
152
  }
143
153
  }
144
154
 
@@ -154,6 +164,15 @@ class DeconzPlatform extends homebridgeLib.Platform {
154
164
 
155
165
  this.log('%d gateways', Object.keys(this.gatewayMap).length)
156
166
  this.emit('initialised')
167
+ const dumpInfo = {
168
+ config: this.config,
169
+ gatewayMap: {}
170
+ }
171
+ for (const id in this.gatewayMap) {
172
+ const gateway = this.gatewayMap[id]
173
+ dumpInfo.gatewayMap[id] = gateway.context
174
+ }
175
+ await this.createDumpFile(dumpInfo)
157
176
  } catch (error) { this.error(error) }
158
177
  }
159
178
 
@@ -172,7 +191,7 @@ class DeconzPlatform extends homebridgeLib.Platform {
172
191
  gatewayByHost[gateway.context.host] = {
173
192
  config: gateway.context.config,
174
193
  host: gateway.context.host,
175
- id: id
194
+ id
176
195
  }
177
196
  }
178
197
  return {
@@ -0,0 +1,216 @@
1
+ // homebridge-deconz/lib/DeconzService/AirPurifier.js
2
+ // Copyright © 2022 Erik Baauw. All rights reserved.
3
+ //
4
+ // Homebridge plugin for deCONZ.
5
+
6
+ 'use strict'
7
+
8
+ const DeconzService = require('../DeconzService')
9
+
10
+ class FilterMaintenance extends DeconzService.SensorsResource {
11
+ constructor (accessory, resource, params = {}) {
12
+ params.Service = accessory.Services.hap.FilterMaintenance
13
+ super(accessory, resource, params)
14
+
15
+ this.addCharacteristicDelegate({
16
+ key: 'filterChange',
17
+ Characteristic: this.Characteristics.hap.FilterChangeIndication
18
+ })
19
+
20
+ if (
21
+ resource.body.config.filterlifetime !== undefined &&
22
+ resource.body.state.filterruntime !== undefined
23
+ ) {
24
+ this.addCharacteristicDelegate({
25
+ key: 'filterLifeLevel',
26
+ Characteristic: this.Characteristics.hap.FilterLifeLevel,
27
+ unit: '%'
28
+ })
29
+ this.addCharacteristicDelegate({
30
+ key: 'resetFilter',
31
+ Characteristic: this.Characteristics.hap.ResetFilterIndication,
32
+ props: { adminOnlyAccess: [this.Characteristic.Access.WRITE] },
33
+ value: 0
34
+ }).on('didSet', async (value, fromHomeKit) => {
35
+ await this.put('/config', { filterlifetime: 6 * 30 * 24 * 60 })
36
+ })
37
+ this.values.filterLifeTime = resource.body.config.filterlifetime
38
+ }
39
+
40
+ this.update(resource.body)
41
+ }
42
+
43
+ updateState (state) {
44
+ if (this.values.filterLifeTime != null && state.filterruntime != null) {
45
+ this.values.filterLifeLevel = 100 - Math.round(
46
+ 100 * state.filterruntime / this.values.filterLifeTime
47
+ )
48
+ }
49
+ if (state.replacefilter != null) {
50
+ this.values.filterChange = state.filterChange
51
+ ? this.Characteristics.hap.FilterChangeIndication.CHANGE_FILTER
52
+ : this.Characteristics.hap.FilterChangeIndication.FILTER_OK
53
+ }
54
+ }
55
+
56
+ updateConfig (config) {
57
+ if (config.filterlifetime != null) {
58
+ this.values.filterLifeTime = config.filterlifetime
59
+ }
60
+ }
61
+ }
62
+
63
+ /**
64
+ * @memberof DeconzService
65
+ */
66
+ class AirPurifier extends DeconzService.SensorsResource {
67
+ constructor (accessory, resource, params = {}) {
68
+ params.Service = accessory.Services.hap.AirPurifier
69
+ super(accessory, resource, params)
70
+
71
+ this.addCharacteristicDelegate({
72
+ key: 'active',
73
+ Characteristic: this.Characteristics.hap.Active
74
+ }).on('didSet', async (value, fromHomeKit) => {
75
+ if (fromHomeKit) {
76
+ await this.put('/config', { mode: this.modeValue(value) })
77
+ }
78
+ })
79
+
80
+ this.addCharacteristicDelegate({
81
+ key: 'currentState',
82
+ Characteristic: this.Characteristics.hap.CurrentAirPurifierState
83
+ })
84
+
85
+ this.addCharacteristicDelegate({
86
+ key: 'targetState',
87
+ Characteristic: this.Characteristics.hap.TargetAirPurifierState
88
+ }).on('didSet', async (value, fromHomeKit) => {
89
+ if (fromHomeKit) {
90
+ await this.put('/config', { mode: this.modeValue(null, value) })
91
+ }
92
+ })
93
+
94
+ this.addCharacteristicDelegate({
95
+ key: 'rotationSpeed',
96
+ Characteristic: this.Characteristics.hap.RotationSpeed,
97
+ unit: '%'
98
+ }).on('didSet', async (value, fromHomeKit) => {
99
+ if (fromHomeKit) {
100
+ await this.put('/config', { mode: this.modeValue(null, null, value) })
101
+ }
102
+ })
103
+
104
+ if (resource.body.state.airquality !== undefined) {
105
+ this.airQualityService = new DeconzService.AirQuality(accessory, resource, {
106
+ linkedServiceDelegate: this
107
+ })
108
+ }
109
+
110
+ if (resource.body.state.replacefilter !== undefined) {
111
+ this.filterService = new FilterMaintenance(accessory, resource, {
112
+ linkedServiceDelegate: this
113
+ })
114
+ }
115
+
116
+ if (resource.body.state.deviceruntime !== undefined) {
117
+ // TODO
118
+ }
119
+
120
+ if (resource.body.config.ledindication !== undefined) {
121
+ // TODO
122
+ }
123
+
124
+ if (resource.body.config.locked !== undefined) {
125
+ this.addCharacteristicDelegate({
126
+ key: 'lockPhysicalControls',
127
+ Characteristic: this.Characteristics.hap.LockPhysicalControls
128
+ }).on('didSet', async (value, fromHomeKit) => {
129
+ if (fromHomeKit) {
130
+ await this.put('/config', {
131
+ locked: value === this.Characteristics.hap.LockPhysicalControls
132
+ .CONTROL_LOCK_ENABLED
133
+ })
134
+ }
135
+ })
136
+ }
137
+
138
+ super.addCharacteristicDelegates()
139
+
140
+ this.update(resource.body)
141
+ }
142
+
143
+ modeValue (
144
+ active = this.values.active,
145
+ targetState = this.values.targetState,
146
+ rotationSpeed = this.values.rotationSpeed
147
+ ) {
148
+ if (active === this.Characteristics.hap.Active.INACTIVE) {
149
+ return 'off'
150
+ }
151
+ if (
152
+ targetState === this.Characteristics.hap.TargetAirPurifierState.AUTO ||
153
+ rotationSpeed === 0
154
+ ) {
155
+ return 'auto'
156
+ }
157
+ return 'speed_' + Math.round(rotationSpeed / 20)
158
+ }
159
+
160
+ updateState (state) {
161
+ if (this.values.filterLifeTime != null && state.filterruntime != null) {
162
+ this.values.filterLifeLevel = 100 - Math.round(
163
+ 100 * state.filterruntime / this.values.filterLifeTime
164
+ )
165
+ }
166
+ if (state.replacefilter != null) {
167
+ this.values.filterChange = state.filterChange
168
+ ? this.Characteristics.hap.FilterChangeIndication.CHANGE_FILTER
169
+ : this.Characteristics.hap.FilterChangeIndication.FILTER_OK
170
+ }
171
+ if (state.speed != null) {
172
+ this.values.active = state.speed > 0
173
+ ? this.Characteristics.hap.Active.ACTIVE
174
+ : this.Characteristics.hap.Active.INACTIVE
175
+ this.values.currentState = state.speed === 0
176
+ ? this.Characteristics.hap.CurrentAirPurifierState.INACTIVE
177
+ : this.Characteristics.hap.CurrentAirPurifierState.PURIFYING_AIR
178
+ this.values.rotationSpeed = state.speed
179
+ }
180
+ super.updateState(state)
181
+ if (this.airQualityService != null) {
182
+ this.airQualityService.updateState(state)
183
+ }
184
+ if (this.filterService != null) {
185
+ this.filterService.updateState(state)
186
+ }
187
+ }
188
+
189
+ updateConfig (config) {
190
+ if (config.filterlifetime != null) {
191
+ this.values.filterLifeTime = config.filterlifetime
192
+ }
193
+ if (config.ledindication != null) {
194
+ // TODO
195
+ }
196
+ if (config.locked != null) {
197
+ this.values.lockPhysicalControls = config.locked
198
+ ? this.Characteristics.hap.LockPhysicalControls.CONTROL_LOCK_ENABLED
199
+ : this.Characteristics.hap.LockPhysicalControls.CONTROL_LOCK_DISABLED
200
+ }
201
+ if (config.mode != null) {
202
+ this.values.targetState = config.mode === 'auto'
203
+ ? this.Characteristics.hap.TargetAirPurifierState.AUTO
204
+ : this.Characteristics.hap.TargetAirPurifierState.MANUAL
205
+ }
206
+ super.updateConfig(config)
207
+ if (this.airQualityService != null) {
208
+ this.airQualityService.updateConfig(config)
209
+ }
210
+ if (this.filterService != null) {
211
+ this.filterService.updateConfig(config)
212
+ }
213
+ }
214
+ }
215
+
216
+ module.exports = AirPurifier
@@ -20,14 +20,27 @@ class AirQuality extends DeconzService.SensorsResource {
20
20
  Characteristic: this.Characteristics.hap.AirQuality
21
21
  })
22
22
 
23
- this.addCharacteristicDelegate({
24
- key: 'vocDensity',
25
- Characteristic: this.Characteristics.hap.VOCDensity,
26
- unit: ' µg/m³',
27
- props: { minValue: 0, maxValue: 65535, minStep: 1 }
28
- })
23
+ if (resource.body.state.airqualityppb !== undefined) {
24
+ this.addCharacteristicDelegate({
25
+ key: 'vocDensity',
26
+ Characteristic: this.Characteristics.hap.VOCDensity,
27
+ unit: ' µg/m³',
28
+ props: { minValue: 0, maxValue: 65535, minStep: 1 }
29
+ })
30
+ }
29
31
 
30
- super.addCharacteristicDelegates()
32
+ if (resource.body.state.pm2_5 !== undefined) {
33
+ this.addCharacteristicDelegate({
34
+ key: 'pm2_5Density',
35
+ Characteristic: this.Characteristics.hap.PM2_5Density,
36
+ unit: ' µg/m³',
37
+ props: { minValue: 0, maxValue: 65535, minStep: 1 }
38
+ })
39
+ }
40
+
41
+ if (params.linkedServiceDelegate == null) {
42
+ super.addCharacteristicDelegates()
43
+ }
31
44
 
32
45
  this.update(resource.body)
33
46
  }
@@ -56,6 +69,9 @@ class AirQuality extends DeconzService.SensorsResource {
56
69
  if (state.airqualityppb != null) {
57
70
  this.values.vocDensity = Math.round(state.airqualityppb * 4.57)
58
71
  }
72
+ if (state.pm2_5 != null) {
73
+ this.values.pm2_5Density = state.pm2_5
74
+ }
59
75
  super.updateState(state)
60
76
  }
61
77
  }
@@ -57,7 +57,7 @@ class Consumption extends DeconzService.SensorsResource {
57
57
  }
58
58
 
59
59
  updateState (state) {
60
- Consumption.updateResourceState(this.service, state)
60
+ Consumption.updateResourceState(this, state)
61
61
  super.updateState(state)
62
62
  }
63
63
  }
@@ -45,6 +45,8 @@ class Contact extends DeconzService.SensorsResource {
45
45
  })
46
46
 
47
47
  this.addCharacteristicDelegates({ noTampered: true })
48
+
49
+ this.update(resource.body)
48
50
  }
49
51
 
50
52
  updateState (state) {
@@ -51,7 +51,7 @@ class Light extends DeconzService.LightsResource {
51
51
  }).on('didSet', (value, fromHomeKit) => {
52
52
  if (fromHomeKit) {
53
53
  const bri = Math.round(value * 2.54)
54
- this.put({ bri: bri })
54
+ this.put({ bri })
55
55
  this.updateAdaptiveLighting()
56
56
  }
57
57
  })
@@ -97,7 +97,7 @@ class Light extends DeconzService.LightsResource {
97
97
  this.capabilities.ctMin, Math.min(value, this.capabilities.ctMax)
98
98
  )
99
99
  if (fromHomeKit) {
100
- this.put({ ct: ct })
100
+ this.put({ ct })
101
101
  this.values.colormode = 'ct'
102
102
  }
103
103
  if (this.capabilities.xy && this.values.colormode === 'ct') {
@@ -118,7 +118,7 @@ class Light extends DeconzService.LightsResource {
118
118
  const xy = hsvToXy(
119
119
  value, this.values.saturation, this.capabilities.gamut
120
120
  )
121
- this.put({ xy: xy })
121
+ this.put({ xy })
122
122
  this.values.colormode = 'xy'
123
123
  }
124
124
  })
@@ -129,7 +129,7 @@ class Light extends DeconzService.LightsResource {
129
129
  }).on('didSet', (value, fromHomeKit) => {
130
130
  if (fromHomeKit) {
131
131
  const xy = hsvToXy(this.values.hue, value, this.capabilities.gamut)
132
- this.put({ xy: xy })
132
+ this.put({ xy })
133
133
  this.values.colormode = 'xy'
134
134
  }
135
135
  })
@@ -141,7 +141,7 @@ class Light extends DeconzService.LightsResource {
141
141
  }).on('didSet', (value, fromHomeKit) => {
142
142
  if (fromHomeKit) {
143
143
  const hue = Math.round(this.values.hue * 65535.0 / 360.0)
144
- this.put({ hue: hue })
144
+ this.put({ hue })
145
145
  this.values.colormode = 'hs'
146
146
  }
147
147
  })
@@ -152,7 +152,7 @@ class Light extends DeconzService.LightsResource {
152
152
  }).on('didSet', (value, fromHomeKit) => {
153
153
  if (fromHomeKit) {
154
154
  const sat = Math.round(this.values.saturation * 254.0 / 100.0)
155
- this.put({ sat: sat })
155
+ this.put({ sat })
156
156
  this.values.colormode = 'hs'
157
157
  }
158
158
  })
@@ -165,7 +165,7 @@ class Light extends DeconzService.LightsResource {
165
165
  }).on('didSet', (value, fromHomeKit) => {
166
166
  if (fromHomeKit) {
167
167
  const effect = value ? 'colorloop' : 'none'
168
- const state = { effect: effect }
168
+ const state = { effect }
169
169
  if (value) {
170
170
  state.colorloopspeed = this.values.colorLoopSpeed
171
171
  }
@@ -181,7 +181,7 @@ class Light extends DeconzService.LightsResource {
181
181
  }).on('didSet', (value, fromHomeKit) => {
182
182
  if (fromHomeKit) {
183
183
  const effect = 'colorloop'
184
- this.put({ effect: effect, colorloopspeed: value })
184
+ this.put({ effect, colorloopspeed: value })
185
185
  this.values.colormode = 'hs'
186
186
  }
187
187
  })
@@ -467,7 +467,7 @@ class Light extends DeconzService.LightsResource {
467
467
  if (this.values.colormode === 'ct' && ct === this.values.colorTemperature) {
468
468
  return
469
469
  }
470
- this.put({ ct: ct })
470
+ this.put({ ct })
471
471
  this.fromAdaptiveLighting = true
472
472
  this.values.colormode = 'ct'
473
473
  if (ct !== this.values.colorTemperature) {
@@ -35,6 +35,8 @@ class LightLevel extends DeconzService.SensorsResource {
35
35
  })
36
36
 
37
37
  this.addCharacteristicDelegates()
38
+
39
+ this.update(resource.body)
38
40
  }
39
41
 
40
42
  updateState (state) {
@@ -35,7 +35,7 @@ class Motion extends DeconzService.SensorsResource {
35
35
  : value === this.Characteristics.eve.Sensitivity.LOW
36
36
  ? 0
37
37
  : Math.round(this.sensitivitymax / 2)
38
- await this.put('/config', { sensitivity: sensitivity })
38
+ await this.put('/config', { sensitivity })
39
39
  }
40
40
  })
41
41
 
@@ -75,7 +75,7 @@ class Power extends DeconzService.SensorsResource {
75
75
  }
76
76
 
77
77
  updateState (state) {
78
- Power.updateResourceState(this.service, state)
78
+ Power.updateResourceState(this, state)
79
79
  super.updateState(state)
80
80
  }
81
81
  }
@@ -42,8 +42,8 @@ class Switch extends DeconzService.SensorsResource {
42
42
  this.buttonServices[i] = new DeconzService.Button(this.accessoryDelegate, {
43
43
  name: this.name + ' ' + label,
44
44
  button: Number(i),
45
- events: events,
46
- hasRepeat: hasRepeat
45
+ events,
46
+ hasRepeat
47
47
  })
48
48
  }
49
49
  }
@@ -27,6 +27,7 @@ class Temperature extends DeconzService.SensorsResource {
27
27
  key: 'offset',
28
28
  Characteristic: this.Characteristics.my.Offset,
29
29
  unit: '°C',
30
+ props: { minValue: -5, maxValue: 5, minStep: 0.1 },
30
31
  value: 0
31
32
  }).on('didSet', async (value, fromHomeKit) => {
32
33
  if (fromHomeKit) {
@@ -77,6 +77,7 @@ class Thermostat extends DeconzService.SensorsResource {
77
77
  key: 'offset',
78
78
  Characteristic: this.Characteristics.my.Offset,
79
79
  unit: '°C',
80
+ props: { minValue: -5, maxValue: 5, minStep: 0.1 },
80
81
  value: 0
81
82
  }).on('didSet', async (value, fromHomeKit) => {
82
83
  if (fromHomeKit) {
@@ -114,7 +114,7 @@ class WindowCovering extends DeconzService.LightsResource {
114
114
  ? this.Characteristics.hap.PositionState.INCREASING
115
115
  : this.Characteristics.hap.PositionState.DECREASING
116
116
  this.moving = new Date()
117
- await this.put({ lift: lift })
117
+ await this.put({ lift })
118
118
  }
119
119
 
120
120
  updateState (state) {
@@ -135,7 +135,7 @@ class WindowCovering extends DeconzService.LightsResource {
135
135
  this.values.closeUpwards = closeUpwards
136
136
  }
137
137
  if (
138
- this.moving == null || new Date() - this.moving >= 15000 || (
138
+ this.moving == null || new Date() - this.moving >= 30000 || (
139
139
  position === this.values.targetPosition &&
140
140
  (closeUpwards == null || closeUpwards === this.targetCloseUpwards)
141
141
  )
@@ -15,6 +15,7 @@ const { dateToString } = Deconz.ApiClient
15
15
  */
16
16
  class DeconzService extends homebridgeLib.ServiceDelegate {
17
17
  static get AirPressure () { return require('./AirPressure') }
18
+ static get AirPurifier () { return require('./AirPurifier') }
18
19
  static get AirQuality () { return require('./AirQuality') }
19
20
  static get Alarm () { return require('./Alarm') }
20
21
  static get Battery () { return require('./Battery') }
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.14",
7
+ "version": "0.0.17",
8
8
  "keywords": [
9
9
  "homebridge-plugin",
10
10
  "homekit",
@@ -20,14 +20,14 @@
20
20
  "deconz": "cli/deconz.js"
21
21
  },
22
22
  "engines": {
23
- "deCONZ": "2.14.1",
24
- "homebridge": "^1.4.0",
25
- "node": "^16.14.0"
23
+ "deCONZ": "2.16.1",
24
+ "homebridge": "^1.4.1",
25
+ "node": "^16.15.1"
26
26
  },
27
27
  "dependencies": {
28
- "homebridge-lib": "~5.3.0",
29
- "semver": "^7.3.5",
30
- "ws": "^8.5.0",
28
+ "homebridge-lib": "~5.6.0",
29
+ "semver": "^7.3.7",
30
+ "ws": "^8.8.0",
31
31
  "xml2js": "~0.4.23"
32
32
  },
33
33
  "scripts": {