homebridge-deconz 0.1.16 → 0.1.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/lib/Deconz/Resource.js
CHANGED
@@ -15,6 +15,12 @@ const { buttonEvent } = Deconz.ApiClient
|
|
15
15
|
const { SINGLE, DOUBLE, LONG } = DeconzService.Button
|
16
16
|
const rtypes = ['lights', 'sensors', 'groups']
|
17
17
|
|
18
|
+
const patterns = {
|
19
|
+
uniqueid: /^([0-9a-f]{1,2}(?:[:-]?(?:[0-9a-f]{1,2})){7})-([0-9a-z]{2})(?:-([0-9a-z]{4}))?$/i,
|
20
|
+
clipId: /^(S[0-9]{1,3})-([0-9a-z]{2})-([0-9a-z]{4})$/i,
|
21
|
+
swversion: /^([0-9]+)(?:\.([0-9]+)(?:\.([0-9]+)(?:_([0-9]{4}))?)?)?$/
|
22
|
+
}
|
23
|
+
|
18
24
|
// From low to high.
|
19
25
|
const sensorsPrios = [
|
20
26
|
'Power',
|
@@ -24,7 +30,8 @@ const sensorsPrios = [
|
|
24
30
|
'Motion',
|
25
31
|
'Contact',
|
26
32
|
'AirPurifier',
|
27
|
-
'Thermostat'
|
33
|
+
'Thermostat',
|
34
|
+
'Flag'
|
28
35
|
]
|
29
36
|
|
30
37
|
// =============================================================================
|
@@ -51,14 +58,43 @@ class Resource {
|
|
51
58
|
*/
|
52
59
|
static parseUniqueid (uniqueid) {
|
53
60
|
toString('uniqueid', uniqueid, true)
|
54
|
-
const a = uniqueid.replace(/:/g, '').toUpperCase()
|
61
|
+
const a = patterns.uniqueid.exec(uniqueid.replace(/:/g, '').toUpperCase())
|
62
|
+
return {
|
63
|
+
mac: a?.[1],
|
64
|
+
endpoint: a?.[2],
|
65
|
+
cluster: a?.[3]
|
66
|
+
}
|
67
|
+
}
|
68
|
+
|
69
|
+
/** Parse the `uniqueid` in the resource body of a resource for a CLIP sensor.
|
70
|
+
* @param {string} uniqueid - The `uniqueid`.
|
71
|
+
* @return {object} The MultiCLIP `id`, `endpoint`, and `cluster`.
|
72
|
+
*/
|
73
|
+
static parseClipId (uniqueid) {
|
74
|
+
toString('uniqueid', uniqueid, true)
|
75
|
+
const a = patterns.clipId.exec(uniqueid.replace(/:/g, '').toUpperCase())
|
55
76
|
return {
|
56
|
-
|
57
|
-
endpoint: a?.[
|
58
|
-
cluster: a?.[
|
77
|
+
id: a?.[1],
|
78
|
+
endpoint: a?.[2],
|
79
|
+
cluster: a?.[3]
|
59
80
|
}
|
60
81
|
}
|
61
82
|
|
83
|
+
/** Parse the `swversion` in the resource body of a resource for a Zigbee device.
|
84
|
+
* @param {string} swversion - The `swversion`.
|
85
|
+
* @return {string} The normalised version in semver format.
|
86
|
+
*/
|
87
|
+
static parseSwversion (swversion) {
|
88
|
+
if (swversion == null) {
|
89
|
+
return '0.0.0'
|
90
|
+
}
|
91
|
+
const a = patterns.swversion.exec(swversion)
|
92
|
+
if (a?.[1] === '0' && a?.[2] === '0' && a?.[3] === '0' && a?.[4] != null) {
|
93
|
+
return '0.0.' + Number(a[4]).toString()
|
94
|
+
}
|
95
|
+
return swversion
|
96
|
+
}
|
97
|
+
|
62
98
|
/** Create a new instance of a delegate of a resource.
|
63
99
|
*
|
64
100
|
* @param {DeconzAccessory.Gateway} gateway - The gateway.
|
@@ -90,6 +126,8 @@ class Resource {
|
|
90
126
|
toString('body.name', body.name, true)
|
91
127
|
toString('body.type', body.type, true)
|
92
128
|
|
129
|
+
let realDevice = false
|
130
|
+
|
93
131
|
if (
|
94
132
|
this.rtype === 'lights' ||
|
95
133
|
(this.rtype === 'sensors' && this.body.type.startsWith('Z'))
|
@@ -129,16 +167,30 @@ class Resource {
|
|
129
167
|
* @type {boolean}
|
130
168
|
*/
|
131
169
|
this.zigbee = true
|
170
|
+
|
171
|
+
realDevice = true
|
132
172
|
} else if (this.rtype === 'sensors') {
|
133
173
|
const { mac, endpoint, cluster } = Resource.parseUniqueid(body.uniqueid)
|
134
174
|
if (mac != null && endpoint != null && cluster != null) {
|
175
|
+
// uniqueid for proxy device has proper mac, endpoint, cluster
|
135
176
|
this.id = mac
|
136
|
-
this.subtype = endpoint +
|
177
|
+
this.subtype = endpoint + '-' + cluster
|
137
178
|
this.endpoint = endpoint
|
138
179
|
this.cluster = cluster
|
180
|
+
realDevice = true
|
139
181
|
} else {
|
140
|
-
|
141
|
-
|
182
|
+
const { id, endpoint, cluster } = Resource.parseClipId(body.uniqueid)
|
183
|
+
if (id != null && endpoint != null && cluster != null) {
|
184
|
+
// uniqueid for MultiCLIP has proper id, endpoint, cluster
|
185
|
+
this.id = gateway.id + '-' + id
|
186
|
+
this.subtype = endpoint + '-' + cluster
|
187
|
+
this.endpoint = endpoint
|
188
|
+
this.cluster = cluster
|
189
|
+
} else {
|
190
|
+
// ignore uniqueid for regular CLIP
|
191
|
+
this.subtype = rtype[0].toUpperCase() + rid
|
192
|
+
this.id = gateway.id + '-' + this.subtype
|
193
|
+
}
|
142
194
|
}
|
143
195
|
this.zigbee = false
|
144
196
|
} else {
|
@@ -154,7 +206,7 @@ class Resource {
|
|
154
206
|
* For virtual devices, this is the _Manufacturer_ for the gateway.
|
155
207
|
* @type {string}
|
156
208
|
*/
|
157
|
-
this.manufacturer =
|
209
|
+
this.manufacturer = realDevice
|
158
210
|
? body.manufacturername.replace(/\//g, '')
|
159
211
|
: gateway.values.manufacturer
|
160
212
|
|
@@ -165,7 +217,7 @@ class Resource {
|
|
165
217
|
* For virtual devices, this is the `type` in the resource body.
|
166
218
|
* @type {string}
|
167
219
|
*/
|
168
|
-
this.model =
|
220
|
+
this.model = realDevice ? body.modelid : body.type
|
169
221
|
|
170
222
|
/** The associated HomeKit _Firmware Version_.
|
171
223
|
*
|
@@ -173,8 +225,8 @@ class Resource {
|
|
173
225
|
* resource body.
|
174
226
|
* For virtual devices, this is the _Firmware Version_ for the gateway.
|
175
227
|
*/
|
176
|
-
this.firmware =
|
177
|
-
?
|
228
|
+
this.firmware = realDevice
|
229
|
+
? Resource.parseSwversion(body.swversion)
|
178
230
|
: gateway.values.software
|
179
231
|
|
180
232
|
/** The name of the {@link DeconzService} subclass of the delegate of the
|
@@ -237,6 +289,7 @@ class Resource {
|
|
237
289
|
case 'On/Off light': return 'Light'
|
238
290
|
case 'On/Off output': return 'Outlet'
|
239
291
|
case 'On/Off plug-in unit': return 'Outlet'
|
292
|
+
case 'On/Off switch': return 'Switch'
|
240
293
|
case 'Smart plug': return 'Outlet'
|
241
294
|
case 'Configuration tool': return ''
|
242
295
|
case 'Range extender': return ''
|
@@ -103,6 +103,14 @@ class Gateway extends AccessoryDelegate {
|
|
103
103
|
} catch (error) { this.error(error) }
|
104
104
|
})
|
105
105
|
|
106
|
+
this.addPropertyDelegate({
|
107
|
+
key: 'exposeSchedules',
|
108
|
+
value: false,
|
109
|
+
silent: true
|
110
|
+
}).on('didSet', async (value) => {
|
111
|
+
this.pollNext = true
|
112
|
+
})
|
113
|
+
|
106
114
|
this.addPropertyDelegate({
|
107
115
|
key: 'heartrate',
|
108
116
|
value: 30,
|
@@ -149,6 +157,7 @@ class Gateway extends AccessoryDelegate {
|
|
149
157
|
value: false,
|
150
158
|
silent: true
|
151
159
|
}).on('didSet', async (value) => {
|
160
|
+
this.service.values.search = value
|
152
161
|
if (value) {
|
153
162
|
try {
|
154
163
|
await this.client.search()
|
@@ -229,6 +238,11 @@ class Gateway extends AccessoryDelegate {
|
|
229
238
|
DeconzService.Button.LONG
|
230
239
|
})
|
231
240
|
|
241
|
+
/** The service delegates for the Schedule services.
|
242
|
+
* @type {Object<string, DeconzService.Schedule>}
|
243
|
+
*/
|
244
|
+
this.scheduleServicesByRid = {}
|
245
|
+
|
232
246
|
this.createClient()
|
233
247
|
this.createWsClient()
|
234
248
|
this.heartbeatEnabled = true
|
@@ -342,7 +356,9 @@ class Gateway extends AccessoryDelegate {
|
|
342
356
|
|
343
357
|
update (config) {
|
344
358
|
this.values.software = config.swversion
|
345
|
-
this.values.firmware = config.fwversion
|
359
|
+
this.values.firmware = parseInt(config.fwversion.slice(6, 8)) + '.' +
|
360
|
+
parseInt(config.fwversion.slice(2, 4), 16) + '.' +
|
361
|
+
parseInt(config.fwversion.slice(4, 6), 16)
|
346
362
|
this.values.wsPort = config.websocketport
|
347
363
|
this.service.update(config)
|
348
364
|
if (this.checkApiKeys) {
|
@@ -772,6 +788,7 @@ class Gateway extends AccessoryDelegate {
|
|
772
788
|
: {
|
773
789
|
brightnessAdjustment: this.values.brightnessAdjustment * 100,
|
774
790
|
expose: this.values.expose,
|
791
|
+
exposeSchedules: this.values.exposeSchedules,
|
775
792
|
heartrate: this.values.heartrate,
|
776
793
|
logLevel: this.values.logLevel,
|
777
794
|
periodicEvents: this.values.periodicEvents,
|
@@ -830,6 +847,7 @@ class Gateway extends AccessoryDelegate {
|
|
830
847
|
if (this.values.apiKey != null) {
|
831
848
|
optionParser
|
832
849
|
.intKey('brightnessAdjustment', 10, 100)
|
850
|
+
.boolKey('exposeSchedules')
|
833
851
|
.intKey('heartrate', 1, 60)
|
834
852
|
.boolKey('periodicEvents')
|
835
853
|
.boolKey('restart')
|
@@ -846,6 +864,7 @@ class Gateway extends AccessoryDelegate {
|
|
846
864
|
responseBody[key] = this.values[key]
|
847
865
|
break
|
848
866
|
case 'expose':
|
867
|
+
case 'exposeSchedules':
|
849
868
|
case 'heartrate':
|
850
869
|
case 'logLevel':
|
851
870
|
case 'periodicEvents':
|
@@ -919,7 +938,7 @@ class Gateway extends AccessoryDelegate {
|
|
919
938
|
await this.wsClient.close()
|
920
939
|
return
|
921
940
|
}
|
922
|
-
if (config.fwversion === '0x00000000') {
|
941
|
+
if (config.bridgeid === '0000000000000000' || config.fwversion === '0x00000000') {
|
923
942
|
this.warn('deCONZ not ready')
|
924
943
|
return
|
925
944
|
}
|
@@ -930,7 +949,7 @@ class Gateway extends AccessoryDelegate {
|
|
930
949
|
this.context.fullState.groups = await this.client.get('/groups')
|
931
950
|
this.context.fullState.groups[0] = await this.client.get('/groups/0')
|
932
951
|
}
|
933
|
-
if (this.
|
952
|
+
if (this.values.exposeSchedules) {
|
934
953
|
this.context.fullState.schedules = await this.client.get('/schedules')
|
935
954
|
}
|
936
955
|
}
|
@@ -1114,6 +1133,24 @@ class Gateway extends AccessoryDelegate {
|
|
1114
1133
|
)
|
1115
1134
|
}
|
1116
1135
|
|
1136
|
+
this.vdebug('analysing schedules...')
|
1137
|
+
if (this.values.exposeSchedules) {
|
1138
|
+
for (const rid in fullState.schedules) {
|
1139
|
+
if (this.scheduleServicesByRid[rid] == null) {
|
1140
|
+
this.scheduleServicesByRid[rid] = new DeconzService.Schedule(
|
1141
|
+
this, rid, fullState.schedules[rid]
|
1142
|
+
)
|
1143
|
+
}
|
1144
|
+
this.scheduleServicesByRid[rid].update(fullState.schedules[rid])
|
1145
|
+
}
|
1146
|
+
}
|
1147
|
+
for (const rid in this.scheduleServicesByRid) {
|
1148
|
+
if (!this.values.exposeSchedules || fullState.schedules[rid] == null) {
|
1149
|
+
this.scheduleServicesByRid[rid].destroy()
|
1150
|
+
delete this.scheduleServicesByRid[rid]
|
1151
|
+
}
|
1152
|
+
}
|
1153
|
+
|
1117
1154
|
if (changed) {
|
1118
1155
|
await this.updateMigration()
|
1119
1156
|
this.identify()
|
@@ -31,6 +31,16 @@ class Gateway extends ServiceDelegate {
|
|
31
31
|
silent: true
|
32
32
|
})
|
33
33
|
|
34
|
+
this.addCharacteristicDelegate({
|
35
|
+
key: 'search',
|
36
|
+
Characteristic: this.Characteristics.my.Search,
|
37
|
+
value: false
|
38
|
+
}).on('didSet', (value, fromHomeKit) => {
|
39
|
+
if (fromHomeKit) {
|
40
|
+
this.gateway.values.search = value
|
41
|
+
}
|
42
|
+
})
|
43
|
+
|
34
44
|
this.addCharacteristicDelegate({
|
35
45
|
key: 'transitionTime',
|
36
46
|
Characteristic: this.Characteristics.my.TransitionTime,
|
@@ -0,0 +1,69 @@
|
|
1
|
+
// homebridge-deconz/lib/DeconzService/Schedule.js
|
2
|
+
// Copyright© 2022-2023 Erik Baauw. All rights reserved.
|
3
|
+
//
|
4
|
+
// Homebridge plugin for deCONZ.
|
5
|
+
|
6
|
+
'use strict'
|
7
|
+
|
8
|
+
const Deconz = require('../Deconz')
|
9
|
+
const { ServiceDelegate } = require('homebridge-lib')
|
10
|
+
|
11
|
+
const { HttpError } = Deconz.ApiClient
|
12
|
+
|
13
|
+
/**
|
14
|
+
* @memberof DeconzService
|
15
|
+
*/
|
16
|
+
class Schedule extends ServiceDelegate {
|
17
|
+
constructor (accessory, rid, body) {
|
18
|
+
super(accessory, {
|
19
|
+
id: accessory.gateway.id + '-T' + rid,
|
20
|
+
name: body.name,
|
21
|
+
Service: accessory.Services.my.Resource,
|
22
|
+
subtype: 'T' + rid,
|
23
|
+
exposeConfiguredName: true
|
24
|
+
})
|
25
|
+
this.id = accessory.gateway.id + '-T' + rid
|
26
|
+
this.gateway = accessory.gateway
|
27
|
+
this.accessory = accessory
|
28
|
+
this.client = accessory.client
|
29
|
+
this.rtype = 'schedules'
|
30
|
+
this.rid = rid
|
31
|
+
this.rpath = '/' + this.rtype + '/' + this.rid
|
32
|
+
|
33
|
+
this.addCharacteristicDelegate({
|
34
|
+
key: 'enabled',
|
35
|
+
Characteristic: this.Characteristics.my.Enabled
|
36
|
+
}).on('didSet', async (value, fromHomeKit) => {
|
37
|
+
await this.put({ status: value ? 'enabled' : 'disabled' })
|
38
|
+
this.values.statusActive = value
|
39
|
+
})
|
40
|
+
|
41
|
+
this.addCharacteristicDelegate({
|
42
|
+
key: 'statusActive',
|
43
|
+
Characteristic: this.Characteristics.hap.StatusActive
|
44
|
+
})
|
45
|
+
|
46
|
+
// this.addCharacteristicDelegate({
|
47
|
+
// key: 'index',
|
48
|
+
// Characteristic: this.Characteristics.hap.ServiceLabelIndex,
|
49
|
+
// value: rid
|
50
|
+
// })
|
51
|
+
}
|
52
|
+
|
53
|
+
update (body) {
|
54
|
+
this.values.enabled = body.status === 'enabled'
|
55
|
+
this.values.statusActive = this.values.enabled
|
56
|
+
}
|
57
|
+
|
58
|
+
async put (body) {
|
59
|
+
try {
|
60
|
+
await this.client.put(this.rpath, body)
|
61
|
+
} catch (error) {
|
62
|
+
if (!(error instanceof HttpError)) {
|
63
|
+
this.warn(error)
|
64
|
+
}
|
65
|
+
}
|
66
|
+
}
|
67
|
+
}
|
68
|
+
|
69
|
+
module.exports = Schedule
|
@@ -35,6 +35,7 @@ class DeconzService extends ServiceDelegate {
|
|
35
35
|
static get Motion () { return require('./Motion') }
|
36
36
|
static get Outlet () { return require('./Outlet') }
|
37
37
|
static get Power () { return require('./Power') }
|
38
|
+
static get Schedule () { return require('./Schedule') }
|
38
39
|
static get SensorsResource () { return require('./SensorsResource') }
|
39
40
|
static get Status () { return require('./Status') }
|
40
41
|
static get Smoke () { return require('./Smoke') }
|