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.
@@ -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().split('-')
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
- mac: a?.[0],
57
- endpoint: a?.[1],
58
- cluster: a?.[2]
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 + (cluster == null ? '' : '-' + cluster)
177
+ this.subtype = endpoint + '-' + cluster
137
178
  this.endpoint = endpoint
138
179
  this.cluster = cluster
180
+ realDevice = true
139
181
  } else {
140
- this.subtype = rtype[0].toUpperCase() + rid
141
- this.id = gateway.id + '-' + this.subtype
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 = this.endpoint != null
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 = this.endpoint != null ? body.modelid : body.type
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 = this.endpoint != null
177
- ? body.swversion == null ? '0.0.0' : body.swversion
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.nDevicesByRtype.schedules) {
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') }
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.16",
7
+ "version": "0.1.17",
8
8
  "keywords": [
9
9
  "homebridge-plugin",
10
10
  "homekit",