homebridge-nb 1.4.4 → 1.4.6

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.
@@ -38,6 +38,10 @@
38
38
  "minimum": 0,
39
39
  "maximum": 2000
40
40
  },
41
+ "removeStaleAccessories": {
42
+ "description": "Remove stale accessories, whose devices are no longer exposed by a Nuki bridge.",
43
+ "type": "boolean"
44
+ },
41
45
  "timeout": {
42
46
  "description": "The timeout in seconds to wait for a response from a Nuki bridge. Default: 15.",
43
47
  "type": "integer",
@@ -80,8 +84,9 @@
80
84
  ]
81
85
  },
82
86
  "openerResetTimeout",
83
- "timeout",
84
- "port"
87
+ "port",
88
+ "removeStaleAccessories",
89
+ "timeout"
85
90
  ]
86
91
  }
87
92
  ]
@@ -0,0 +1,321 @@
1
+ // homebridge-nb/lib/NbAccessory/Bridge.js
2
+ // Copyright © 2020-2023 Erik Baauw. All rights reserved.
3
+ //
4
+ // Homebridge plug-in for Nuki Bridge.
5
+
6
+ 'use strict'
7
+
8
+ const { AccessoryDelegate, ServiceDelegate, toHexString } = require('homebridge-lib')
9
+ const NbAccessory = require('../NbAccessory')
10
+ const NbService = require('../NbService')
11
+ const { NbClient } = require('hb-nb-tools')
12
+
13
+ class Bridge extends AccessoryDelegate {
14
+ constructor (platform, context) {
15
+ super(platform, {
16
+ id: context.id,
17
+ name: context.name,
18
+ category: platform.Accessory.Categories.RANGE_EXTENDER,
19
+ model: 'Bridge',
20
+ firmware: context.firmware
21
+ })
22
+ this.id = context.id
23
+ this.context.host = context.host
24
+ this.context.token = context.token
25
+ this.context.firmware = context.firmware
26
+ this.log(
27
+ 'Nuki Bridge v%s %s at %s', context.firmware, context.id, context.host
28
+ )
29
+ this.on('shutdown', this.shutdown)
30
+ this.heartbeatEnabled = true
31
+ this.once('heartbeat', this.init)
32
+
33
+ this.smartLocks = {}
34
+ this.doorSensors = {}
35
+ this.keypads = {}
36
+ this.openers = {}
37
+ this.service = new NbService.Bridge(this)
38
+ this.manageLogLevel(this.service.characteristicDelegate('logLevel'))
39
+ this.dummyService = new ServiceDelegate.Dummy(this)
40
+
41
+ this.client = new NbClient({
42
+ host: this.context.host,
43
+ timeout: platform.config.timeout,
44
+ token: this.context.token
45
+ })
46
+ this.client
47
+ .on('error', (error) => {
48
+ this.log(
49
+ 'request %d: %s %s', error.request.id,
50
+ error.request.method, error.request.resource
51
+ )
52
+ this.warn('request %d: error: %s', error.request.id, error)
53
+ })
54
+ .on('request', (request) => {
55
+ this.debug(
56
+ 'request %d: %s %s', request.id, request.method, request.resource
57
+ )
58
+ this.vdebug(
59
+ 'request %d: %s %s', request.id, request.method, request.url
60
+ )
61
+ })
62
+ .on('response', (response) => {
63
+ this.vdebug(
64
+ 'request %d: response: %j', response.request.id, response.body
65
+ )
66
+ this.debug(
67
+ 'request %d: %s %s', response.request.id,
68
+ response.statusCode, response.statusMessage
69
+ )
70
+ })
71
+ .on('event', (event) => {
72
+ this.debug('event: %j', event)
73
+ const id = event.nukiId.toString(16).toUpperCase()
74
+ switch (event.deviceType) {
75
+ case NbClient.DeviceTypes.SMARTLOCK:
76
+ case NbClient.DeviceTypes.SMARTDOOR:
77
+ case NbClient.DeviceTypes.SMARTLOCK3:
78
+ if (this.smartLocks[id] != null) {
79
+ this.smartLocks[id].update(event)
80
+ }
81
+ if (this.doorSensors[id + '-S'] != null) {
82
+ this.doorSensors[id + '-S'].update(event)
83
+ }
84
+ break
85
+ case NbClient.DeviceTypes.OPENER:
86
+ if (this.openers[id] != null) {
87
+ this.openers[id].update(event)
88
+ }
89
+ break
90
+ default:
91
+ break
92
+ }
93
+ })
94
+ }
95
+
96
+ get host () { return this.context.host }
97
+ set host (value) {
98
+ if (value !== this.context.host) {
99
+ this.debug('now at %s', value)
100
+ this.client.host = value
101
+ this.context.host = value
102
+ this.once('heartbeat', this.init)
103
+ }
104
+ }
105
+
106
+ async init (beat) {
107
+ try {
108
+ await this.client.init()
109
+ this.values.firmware = this.client.firmware
110
+ this.context.firmware = this.values.firmware
111
+ if (this.values.firmware !== this.platform.packageJson.engines.nuki) {
112
+ this.warn(
113
+ 'recommended version: Nuki Bridge v%s',
114
+ this.platform.packageJson.engines.nuki
115
+ )
116
+ }
117
+ switch (this.client.encryption) {
118
+ case 'none':
119
+ this.warn('using plain-text tokens')
120
+ break
121
+ case 'hashedToken':
122
+ this.warn('using deprecated hashed tokens')
123
+ break
124
+ default:
125
+ break
126
+ }
127
+ } catch (error) {
128
+ return
129
+ }
130
+ if (this.context.callbackUrl) {
131
+ this.warn('unclean shutdown - checking for stale subscriptions')
132
+ try {
133
+ const response = await this.client.callbackList()
134
+ for (const callback of response.body.callbacks) {
135
+ if (callback.url === this.context.callbackUrl) {
136
+ this.log('remove stale subscription')
137
+ await this.client.callbackRemove(callback.id)
138
+ }
139
+ }
140
+ } catch (error) {
141
+ this.warn(error)
142
+ }
143
+ }
144
+ this.context.callbackUrl = await this.platform.addClient(this.client)
145
+ this.initialBeat = beat
146
+ try {
147
+ await this.heartbeat(beat)
148
+ } catch (error) {}
149
+ this.on('heartbeat', this.heartbeat)
150
+ this.debug('initialised')
151
+ this.emit('initialised')
152
+ }
153
+
154
+ async checkSubscription () {
155
+ if (this.callbackId == null) {
156
+ this.log('subscribe to event notifications')
157
+ const response = await this.client.callbackAdd(this.context.callbackUrl)
158
+ if (!response.body.success) {
159
+ this.error(response.body.message)
160
+ return
161
+ }
162
+ }
163
+ const response = await this.client.callbackList()
164
+ for (const callback of response.body.callbacks) {
165
+ if (callback.url === this.context.callbackUrl) {
166
+ this.debug('subscription: %j', callback)
167
+ this.callbackId = callback.id
168
+ return
169
+ }
170
+ }
171
+ if (this.callbackId != null) {
172
+ this.warn('lost subscription to event notifications')
173
+ this.callbackId = null
174
+ }
175
+ return this.checkSubscription()
176
+ }
177
+
178
+ async shutdown () {
179
+ if (this.client != null) {
180
+ const response = await this.client.callbackList()
181
+ for (const callback of response.body.callbacks) {
182
+ if (callback.url === this.context.callbackUrl) {
183
+ try {
184
+ this.log('unsubscribe from event notifications')
185
+ await this.client.callbackRemove(callback.id)
186
+ } catch (error) {
187
+ this.error(error)
188
+ }
189
+ }
190
+ }
191
+ this.platform.removeClient(this.client)
192
+ delete this.context.callbackUrl
193
+ }
194
+ }
195
+
196
+ addDoorSensor (id, context) {
197
+ this.doorSensors[id] = new NbAccessory.DoorSensor(this, context)
198
+ }
199
+
200
+ addKeypad (id, context) {
201
+ this.keypads[id] = new NbAccessory.Keypad(this, context)
202
+ }
203
+
204
+ addOpener (id, context) {
205
+ this.openers[id] = new NbAccessory.Opener(this, context)
206
+ }
207
+
208
+ addSmartLock (id, context) {
209
+ this.smartLocks[id] = new NbAccessory.SmartLock(this, context)
210
+ }
211
+
212
+ checkDoorSensor (id, device) {
213
+ if (
214
+ device.lastKnownState.doorsensorState != null &&
215
+ device.lastKnownState.doorsensorState !== NbClient.DoorSensorStates.DEACTIVATED
216
+ ) {
217
+ if (this.doorSensors[id + '-S'] == null) {
218
+ this.addDoorSensor(id + '-S', { id: id + '-S', device })
219
+ }
220
+ this.doorSensors[id + '-S'].context.device = device
221
+ this.doorSensors[id + '-S'].update(device.lastKnownState)
222
+ } else if (this.doorSensors[id + '-S'] != null) {
223
+ this.doorSensors[id + '-S'].destroy()
224
+ delete this.doorSensors[id + '-S']
225
+ }
226
+ }
227
+
228
+ checkKeypad (id, device) {
229
+ if (device.lastKnownState.keypadBatteryCritical != null) {
230
+ if (this.keypads[id + '-K'] == null) {
231
+ this.addKeypad(id + '-K', { id: id + '-K', device })
232
+ }
233
+ this.keypads[id + '-K'].context.device = device
234
+ this.keypads[id + '-K'].update(device.lastKnownState)
235
+ } else if (this.keypads[id + '-K'] != null) {
236
+ this.keypads[id + '-K'].destroy()
237
+ delete this.keypads[id + '-K']
238
+ }
239
+ }
240
+
241
+ checkFirmware (info) {
242
+ if (this.values.firmware !== info.versions.firmwareVersion) {
243
+ this.values.firmware = info.versions.firmwareVersion
244
+ this.context.firmware = this.values.firmware
245
+ if (this.values.firmware !== this.platform.packageJson.engines.nuki) {
246
+ this.warn(
247
+ 'recommended version: Nuki Bridge v%s',
248
+ this.platform.packageJson.engines.nuki
249
+ )
250
+ }
251
+ }
252
+ }
253
+
254
+ async heartbeat (beat) {
255
+ if ((beat - this.initialBeat) % this.service.values.heartrate === 0) {
256
+ try {
257
+ await this.checkSubscription()
258
+ let response = await this.client.info()
259
+ this.debug('bridge: %j', response.body)
260
+ this.checkFirmware(response.body)
261
+ this.service.update(response.body)
262
+ response = await this.client.list()
263
+ for (const device of response.body) {
264
+ try {
265
+ this.debug('device: %j', device)
266
+ if (device.firmwareVersion == null) { // Issue 93.
267
+ continue
268
+ }
269
+ const id = toHexString(device.nukiId, 8)
270
+ if (!this.platform.isWhitelisted(id)) {
271
+ continue
272
+ }
273
+ switch (device.deviceType) {
274
+ case NbClient.DeviceTypes.SMARTLOCK:
275
+ case NbClient.DeviceTypes.SMARTDOOR:
276
+ case NbClient.DeviceTypes.SMARTLOCK3:
277
+ if (device.lastKnownState == null) {
278
+ this.warn('%s: no last known state', id)
279
+ continue
280
+ }
281
+ if (device.name == null || device.name === '') {
282
+ device.name = 'Nuki_' + id
283
+ }
284
+ if (this.smartLocks[id] == null) {
285
+ this.addSmartLock(id, { id, device })
286
+ }
287
+ this.smartLocks[id].context.device = device
288
+ this.smartLocks[id].update(device.lastKnownState)
289
+ this.checkDoorSensor(id, device)
290
+ this.checkKeypad(id, device)
291
+ break
292
+ case NbClient.DeviceTypes.OPENER:
293
+ if (device.lastKnownState == null) {
294
+ this.warn('%s: no last known state', id)
295
+ continue
296
+ }
297
+ if (device.name == null || device.name === '') {
298
+ device.name = 'Nuki_Opener_' + id
299
+ }
300
+ if (this.openers[id] == null) {
301
+ this.addOpener(id, { id, device })
302
+ }
303
+ this.openers[id].context.device = device
304
+ this.openers[id].update(device.lastKnownState)
305
+ this.checkKeypad(id, device)
306
+ break
307
+ default:
308
+ break
309
+ }
310
+ } catch (error) {
311
+ this.warn('heartbeat error: %s', error)
312
+ }
313
+ }
314
+ } catch (error) {
315
+ this.warn('heartbeat error: %s', error)
316
+ }
317
+ }
318
+ }
319
+ }
320
+
321
+ module.exports = Bridge
@@ -0,0 +1,57 @@
1
+ // homebridge-nb/lib/NbAccessory/DoorSensor.js
2
+ // Copyright © 2020-2023 Erik Baauw. All rights reserved.
3
+ //
4
+ // Homebridge plug-in for Nuki Bridge.
5
+
6
+ 'use strict'
7
+
8
+ const { ServiceDelegate } = require('homebridge-lib')
9
+ const NbAccessory = require('.')
10
+ const NbService = require('../NbService')
11
+
12
+ class DoorSensor extends NbAccessory {
13
+ constructor (bridge, params) {
14
+ params.category = bridge.Accessory.Categories.DOOR
15
+ params.model = 'Door Sensor'
16
+ super(bridge, {
17
+ id: params.id,
18
+ name: params.device.name + ' Sensor',
19
+ device: params.device,
20
+ category: bridge.Accessory.Categories.DOOR,
21
+ model: 'Door Sensor'
22
+ })
23
+ this.service = new NbService.DoorSensor(this)
24
+ if (params.device.lastKnownState.doorsensorBatteryCritical) {
25
+ this.batteryService = new ServiceDelegate.Battery(this, {
26
+ statusLowBattery: params.device.lastKnownState.doorsensorBatteryCritical
27
+ ? this.Characteristics.hap.StatusLowBattery.BATTERY_LEVEL_LOW
28
+ : this.Characteristics.hap.StatusLowBattery.BATTERY_LEVEL_NORMAL
29
+ })
30
+ }
31
+ this.historyService = new ServiceDelegate.History(this, {
32
+ contactDelegate: this.service.characteristicDelegate('contact'),
33
+ lastContactDelegate: this.service.characteristicDelegate('lastActivation'),
34
+ timesOpenedDelegate: this.service.characteristicDelegate('timesOpened')
35
+ })
36
+ }
37
+
38
+ update (state) {
39
+ this.service.update(state)
40
+ if (state.doorsensorBatteryCritical != null) {
41
+ this.batteryService.values.statusLowBattery = state.doorsensorBatteryCritical
42
+ ? this.Characteristics.hap.StatusLowBattery.BATTERY_LEVEL_LOW
43
+ : this.Characteristics.hap.StatusLowBattery.BATTERY_LEVEL_NORMAL
44
+ }
45
+ }
46
+
47
+ async identify () {
48
+ try {
49
+ const response = await this.client.lockState(
50
+ this.id, this.context.deviceType
51
+ )
52
+ this.update(response.body)
53
+ } catch (error) { this.error(error) }
54
+ }
55
+ }
56
+
57
+ module.exports = DoorSensor
@@ -0,0 +1,48 @@
1
+ // homebridge-nb/lib/NbAccessory/Keypad.js
2
+ // Copyright © 2020-2023 Erik Baauw. All rights reserved.
3
+ //
4
+ // Homebridge plug-in for Nuki Bridge.
5
+
6
+ 'use strict'
7
+
8
+ const { ServiceDelegate } = require('homebridge-lib')
9
+ const NbAccessory = require('../NbAccessory')
10
+
11
+ class Keypad extends NbAccessory {
12
+ constructor (bridge, params) {
13
+ params.category = bridge.Accessory.Categories.DOOR_LOCK
14
+ params.model = 'Keypad'
15
+ super(bridge, {
16
+ id: params.id,
17
+ name: params.device.name + ' Keypad',
18
+ device: params.device,
19
+ category: bridge.Accessory.Categories.PROGRAMMABLE_SWITCH,
20
+ model: 'Keypad'
21
+ })
22
+ this.service = new ServiceDelegate.Dummy(this)
23
+ this.batteryService = new ServiceDelegate.Battery(this, {
24
+ statusLowBattery: params.device.lastKnownState.keypadBatteryCritical
25
+ ? this.Characteristics.hap.StatusLowBattery.BATTERY_LEVEL_LOW
26
+ : this.Characteristics.hap.StatusLowBattery.BATTERY_LEVEL_NORMAL
27
+ })
28
+ }
29
+
30
+ update (state) {
31
+ if (state.doorsensorBatteryCritical != null) {
32
+ this.batteryService.values.statusLowBattery = state.doorsensorBatteryCritical
33
+ ? this.Characteristics.hap.StatusLowBattery.BATTERY_LEVEL_LOW
34
+ : this.Characteristics.hap.StatusLowBattery.BATTERY_LEVEL_NORMAL
35
+ }
36
+ }
37
+
38
+ async identify () {
39
+ try {
40
+ const response = await this.client.lockState(
41
+ this.id, this.context.deviceType
42
+ )
43
+ this.update(response.body)
44
+ } catch (error) { this.error(error) }
45
+ }
46
+ }
47
+
48
+ module.exports = Keypad
@@ -0,0 +1,51 @@
1
+ // homebridge-nb/lib/NbAccessory/Opener.js
2
+ // Copyright © 2020-2023 Erik Baauw. All rights reserved.
3
+ //
4
+ // Homebridge plug-in for Nuki Bridge.
5
+
6
+ 'use strict'
7
+
8
+ const { ServiceDelegate } = require('homebridge-lib')
9
+ const NbAccessory = require('../NbAccessory')
10
+ const NbService = require('../NbService')
11
+ const { NbClient } = require('hb-nb-tools')
12
+
13
+ class Opener extends NbAccessory {
14
+ constructor (bridge, params) {
15
+ super(bridge, {
16
+ id: params.id,
17
+ name: params.device.name,
18
+ device: params.device,
19
+ category: bridge.Accessory.Categories.DOOR_LOCK,
20
+ model: NbClient.modelName(params.device.deviceType, params.device.firmwareVersion)
21
+ })
22
+ this.openerService = new NbService.Opener(this)
23
+ this.doorBellService = new NbService.DoorBell(this)
24
+ this.batteryService = new ServiceDelegate.Battery(this, {
25
+ statusLowBattery: params.device.lastKnownState.doorsensorBatteryCritical
26
+ ? this.Characteristics.hap.StatusLowBattery.BATTERY_LEVEL_LOW
27
+ : this.Characteristics.hap.StatusLowBattery.BATTERY_LEVEL_NORMAL
28
+ })
29
+ }
30
+
31
+ update (state) {
32
+ this.openerService.update(state)
33
+ this.doorBellService.update(state)
34
+ if (state.batteryCritical != null) {
35
+ this.batteryService.values.statusLowBattery = state.batteryCritical
36
+ ? this.Characteristics.hap.StatusLowBattery.BATTERY_LEVEL_LOW
37
+ : this.Characteristics.hap.StatusLowBattery.BATTERY_LEVEL_NORMAL
38
+ }
39
+ }
40
+
41
+ async identify () {
42
+ try {
43
+ const response = await this.client.lockState(
44
+ this.id, NbClient.DeviceTypes.OPENER
45
+ )
46
+ this.update(response.body)
47
+ } catch (error) { this.error(error) }
48
+ }
49
+ }
50
+
51
+ module.exports = Opener
@@ -0,0 +1,67 @@
1
+ // homebridge-nb/lib/NbAccessory/SmartLock.js
2
+ // Copyright © 2020-2023 Erik Baauw. All rights reserved.
3
+ //
4
+ // Homebridge plug-in for Nuki Bridge.
5
+
6
+ 'use strict'
7
+
8
+ const { ServiceDelegate } = require('homebridge-lib')
9
+ const NbAccessory = require('../NbAccessory')
10
+ const NbService = require('../NbService')
11
+ const { NbClient } = require('hb-nb-tools')
12
+
13
+ class SmartLock extends NbAccessory {
14
+ constructor (bridge, params) {
15
+ super(bridge, {
16
+ id: params.id,
17
+ name: params.device.name,
18
+ device: params.device,
19
+ category: bridge.Accessory.Categories.DOOR_LOCK,
20
+ model: NbClient.modelName(params.device.deviceType, params.device.firmwareVersion)
21
+ })
22
+ this.service = new NbService.SmartLock(this)
23
+ if (this.platform.config.latch) {
24
+ this.latchService = new NbService.Latch(this)
25
+ }
26
+ this.batteryService = new ServiceDelegate.Battery(this, {
27
+ batteryLevel: params.device.lastKnownState.batteryChargeState,
28
+ chargingState: params.device.lastKnownState.batteryCharging
29
+ ? this.Characteristics.hap.ChargingState.CHARGING
30
+ : this.Characteristics.hap.ChargingState.NOT_CHARGING,
31
+ statusLowBattery: params.device.lastKnownState.batteryCritical
32
+ ? this.Characteristics.hap.StatusLowBattery.BATTERY_LEVEL_LOW
33
+ : this.Characteristics.hap.StatusLowBattery.BATTERY_LEVEL_NORMAL
34
+ })
35
+ }
36
+
37
+ update (state) {
38
+ this.service.update(state)
39
+ if (this.platform.config.latch) {
40
+ this.latchService.update(state)
41
+ }
42
+ if (state.batteryChargeState) {
43
+ this.batteryService.values.batteryLevel = state.batteryChargeState
44
+ }
45
+ if (state.batteryCharging != null) {
46
+ this.batteryService.values.chargingState = state.batteryCharging
47
+ ? this.Characteristics.hap.ChargingState.CHARGING
48
+ : this.Characteristics.hap.ChargingState.NOT_CHARGING
49
+ }
50
+ if (state.batteryCritical != null) {
51
+ this.batteryService.values.statusLowBattery = state.batteryCritical
52
+ ? this.Characteristics.hap.StatusLowBattery.BATTERY_LEVEL_LOW
53
+ : this.Characteristics.hap.StatusLowBattery.BATTERY_LEVEL_NORMAL
54
+ }
55
+ }
56
+
57
+ async identify () {
58
+ try {
59
+ const response = await this.client.lockState(
60
+ this.id, this.context.deviceType
61
+ )
62
+ this.update(response.body)
63
+ } catch (error) { this.error(error) }
64
+ }
65
+ }
66
+
67
+ module.exports = SmartLock
@@ -0,0 +1,38 @@
1
+ // homebridge-nb/lib/NbAccessory/index.js
2
+ // Copyright © 2020-2023 Erik Baauw. All rights reserved.
3
+ //
4
+ // Homebridge plug-in for Nuki Bridge.
5
+
6
+ 'use strict'
7
+
8
+ const { AccessoryDelegate } = require('homebridge-lib')
9
+
10
+ class NbAccessory extends AccessoryDelegate {
11
+ static get Bridge () { return require('./Bridge') }
12
+ static get DoorSensor () { return require('./DoorSensor') }
13
+ static get Keypad () { return require('./Keypad') }
14
+ static get Opener () { return require('./Opener') }
15
+ static get SmartLock () { return require('./SmartLock') }
16
+
17
+ constructor (bridge, params) {
18
+ super(bridge.platform, {
19
+ id: params.id,
20
+ name: params.name,
21
+ category: params.category,
22
+ manufacturer: 'Nuki Home Solutions GmbH',
23
+ model: params.model,
24
+ firmware: params.device.firmwareVersion
25
+ })
26
+ this.inheritLogLevel(bridge)
27
+ this.id = params.id
28
+ this.bridge = bridge
29
+ this.client = this.bridge.client
30
+ this.context.bridgeId = this.bridge.id
31
+ this.deviceType = params.device
32
+ this.log('Nuki %s v%s %s', params.model, params.device.firmwareVersion, params.id)
33
+ this.on('identify', this.identify)
34
+ setImmediate(() => { this.emit('initialised') })
35
+ }
36
+ }
37
+
38
+ module.exports = NbAccessory