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 +2 -2
- package/lib/Deconz/ApiClient.js +3 -4
- package/lib/Deconz/Discovery.js +2 -2
- package/lib/Deconz/Resource.js +6 -3
- package/lib/DeconzAccessory/AirPurifier.js +38 -0
- package/lib/DeconzAccessory/Gateway.js +67 -41
- package/lib/DeconzAccessory/Motion.js +3 -0
- package/lib/DeconzAccessory/index.js +1 -1
- package/lib/DeconzPlatform.js +35 -16
- package/lib/DeconzService/AirPurifier.js +216 -0
- package/lib/DeconzService/AirQuality.js +23 -7
- package/lib/DeconzService/Consumption.js +1 -1
- package/lib/DeconzService/Contact.js +2 -0
- package/lib/DeconzService/Light.js +9 -9
- package/lib/DeconzService/LightLevel.js +2 -0
- package/lib/DeconzService/Motion.js +1 -1
- package/lib/DeconzService/Power.js +1 -1
- package/lib/DeconzService/Switch.js +2 -2
- package/lib/DeconzService/Temperature.js +1 -0
- package/lib/DeconzService/Thermostat.js +1 -0
- package/lib/DeconzService/WindowCovering.js +2 -2
- package/lib/DeconzService/index.js +1 -0
- package/package.json +7 -7
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
|
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
|
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
|
}
|
package/lib/Deconz/ApiClient.js
CHANGED
@@ -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
|
-
|
85
|
-
|
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
|
151
|
+
validStatusCodes: [200, 400, 403] //, 404]
|
153
152
|
}
|
154
153
|
if (_options.phoscon) {
|
155
154
|
// options.headers = { Accept: 'application/vnd.ddel.v1' }
|
package/lib/Deconz/Discovery.js
CHANGED
@@ -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
|
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
|
99
|
+
host,
|
100
100
|
timeout: this._options.timeout
|
101
101
|
}
|
102
102
|
const client = new homebridgeLib.HttpClient(options)
|
package/lib/Deconz/Resource.js
CHANGED
@@ -21,8 +21,9 @@ const sensorsPrios = [
|
|
21
21
|
'Power',
|
22
22
|
'Consumption',
|
23
23
|
'Temperature',
|
24
|
-
'
|
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.
|
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.
|
61
|
-
this.context.
|
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.
|
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
|
228
|
+
const settings = Object.keys(this.context.settingsById).sort()
|
208
229
|
this.vdebug(
|
209
|
-
'
|
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
|
-
|
233
|
-
|
234
|
-
|
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.
|
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
|
-
|
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.
|
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
|
-
|
683
|
-
|
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.
|
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.
|
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.
|
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')
|
package/lib/DeconzPlatform.js
CHANGED
@@ -70,13 +70,17 @@ class DeconzPlatform extends homebridgeLib.Platform {
|
|
70
70
|
})
|
71
71
|
this.discovery
|
72
72
|
.on('error', (error) => {
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
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(
|
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
|
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
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
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
|
-
|
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
|
}
|
@@ -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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
470
|
+
this.put({ ct })
|
471
471
|
this.fromAdaptiveLighting = true
|
472
472
|
this.values.colormode = 'ct'
|
473
473
|
if (ct !== this.values.colorTemperature) {
|
@@ -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
|
38
|
+
await this.put('/config', { sensitivity })
|
39
39
|
}
|
40
40
|
})
|
41
41
|
|
@@ -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
|
46
|
-
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
|
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 >=
|
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.
|
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.
|
24
|
-
"homebridge": "^1.4.
|
25
|
-
"node": "^16.
|
23
|
+
"deCONZ": "2.16.1",
|
24
|
+
"homebridge": "^1.4.1",
|
25
|
+
"node": "^16.15.1"
|
26
26
|
},
|
27
27
|
"dependencies": {
|
28
|
-
"homebridge-lib": "~5.
|
29
|
-
"semver": "^7.3.
|
30
|
-
"ws": "^8.
|
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": {
|