hb-nb-tools 1.0.0
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/README.md +34 -0
- package/cli/nb.js +12 -0
- package/index.js +38 -0
- package/lib/NbClient.js +224 -0
- package/lib/NbDiscovery.js +57 -0
- package/lib/NbListener.js +105 -0
- package/lib/NbTool.js +645 -0
- package/package.json +37 -0
package/README.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Homebridge NB Tools
|
|
2
|
+
[](https://www.npmjs.com/package/hb-nb-tools)
|
|
3
|
+
[](https://www.npmjs.com/package/hb-nb-tools)
|
|
4
|
+
[](https://standardjs.com)
|
|
5
|
+
|
|
6
|
+
</span>
|
|
7
|
+
|
|
8
|
+
## Homebridge NB Tools
|
|
9
|
+
Copyright © 2020-2023 Erik Baauw. All rights reserved.
|
|
10
|
+
|
|
11
|
+
This repository provides a standalone installation of the command-line tools from [Homebridge NB](https://github.com/ebaauw/homebridge-nb):
|
|
12
|
+
|
|
13
|
+
Tool | Description
|
|
14
|
+
--------- | -----------
|
|
15
|
+
`nb ` | Interact with Nuki bridge from command line.
|
|
16
|
+
|
|
17
|
+
Each command-line tool takes a `-h` or `--help` argument to provide a brief overview of its functionality and command-line arguments.
|
|
18
|
+
|
|
19
|
+
### Prerequisites
|
|
20
|
+
Homebridge NB communicates with the Nuki Bridge using the local
|
|
21
|
+
[Nuki Bridge HTTP API](https://developer.nuki.io/page/nuki-bridge-http-api-1-12/4).
|
|
22
|
+
You need to enable this API through the Nuki app.
|
|
23
|
+
|
|
24
|
+
### Installation
|
|
25
|
+
```
|
|
26
|
+
$ sudo npm -g i hb-nb-tools
|
|
27
|
+
```
|
|
28
|
+
Homebridge NB Tools uses [`sodium-plus`](https://github.com/paragonie/sodium-plus) for [encrypted tokens](https://developer.nuki.io/t/bridge-beta-fw-1-22-1-2-14-0-with-new-encrypted-bridge-http-api-token/15816).
|
|
29
|
+
This optional dependency contains C++ modules, that might need to be compiled on installation.
|
|
30
|
+
In case you want to install Homebridge NB Tools on a system that doesn't include the build tools needed for this, you might need to skip installation of `sodium-plus` by
|
|
31
|
+
```
|
|
32
|
+
$ sudo npm -g i hb-nb-tools --omit=optional
|
|
33
|
+
```
|
|
34
|
+
Homebrdige NB Tools reverts to hashed tokens when `sodium-plus` isn't installed.
|
package/cli/nb.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// nb.js
|
|
4
|
+
//
|
|
5
|
+
// Command line interface to Nuki bridge HTTP API.
|
|
6
|
+
// Copyright © 2018-2023 Erik Baauw. All rights reserved.
|
|
7
|
+
|
|
8
|
+
'use strict'
|
|
9
|
+
|
|
10
|
+
const { NbTool } = require('../index')
|
|
11
|
+
|
|
12
|
+
new NbTool(require('../package.json')).main()
|
package/index.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// hb-nb-tools/index.js
|
|
2
|
+
//
|
|
3
|
+
// Homebridge NB Tools.
|
|
4
|
+
// Copyright © 2017-2023 Erik Baauw. All rights reserved.
|
|
5
|
+
|
|
6
|
+
'use strict'
|
|
7
|
+
|
|
8
|
+
/** Homebridge NB Tools.
|
|
9
|
+
*
|
|
10
|
+
* @module hbNbTools
|
|
11
|
+
*/
|
|
12
|
+
class hbNbTools {
|
|
13
|
+
/** Colour conversions.
|
|
14
|
+
* <br>See {@link NbClient}.
|
|
15
|
+
* @type {Class}
|
|
16
|
+
* @memberof module:hbNbTools
|
|
17
|
+
*/
|
|
18
|
+
static get NbClient () { return require('./lib/NbClient') }
|
|
19
|
+
|
|
20
|
+
/** Parser and validator for command-line arguments.
|
|
21
|
+
* <br>See {@link NbDiscovery}.
|
|
22
|
+
* @type {Class}
|
|
23
|
+
* @memberof module:hbNbTools
|
|
24
|
+
*/
|
|
25
|
+
static get NbDiscovery () { return require('./lib/NbDiscovery') }
|
|
26
|
+
|
|
27
|
+
/** Command-line tool.
|
|
28
|
+
* <br>See {@link NbListener}.
|
|
29
|
+
* @type {Class}
|
|
30
|
+
* @memberof module:hbNbTools
|
|
31
|
+
*/
|
|
32
|
+
static get NbListener () { return require('./lib/NbListener') }
|
|
33
|
+
|
|
34
|
+
// Command-line tools.
|
|
35
|
+
static get NbTool () { return require('./lib/NbTool') }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
module.exports = hbNbTools
|
package/lib/NbClient.js
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
// hb-nb-tools/lib/NbClient.js
|
|
2
|
+
// Copyright © 2020-2023 Erik Baauw. All rights reserved.
|
|
3
|
+
//
|
|
4
|
+
// Homebridge NB Tools.
|
|
5
|
+
|
|
6
|
+
'use strict'
|
|
7
|
+
|
|
8
|
+
const crypto = require('crypto')
|
|
9
|
+
const hbLibTools = require('hb-lib-tools')
|
|
10
|
+
const { HttpClient, JsonFormatter, OptionParser } = hbLibTools
|
|
11
|
+
let sodiumPlus
|
|
12
|
+
try {
|
|
13
|
+
sodiumPlus = require('sodium-plus')
|
|
14
|
+
} catch (error) {}
|
|
15
|
+
const { CryptographyKey, SodiumPlus } = sodiumPlus || {}
|
|
16
|
+
|
|
17
|
+
class NbClient extends HttpClient {
|
|
18
|
+
static get DeviceTypes () {
|
|
19
|
+
return {
|
|
20
|
+
SMARTLOCK: 0,
|
|
21
|
+
OPENER: 2,
|
|
22
|
+
SMARTDOOR: 3,
|
|
23
|
+
SMARTLOCK3: 4
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
static get LockStates () {
|
|
28
|
+
return {
|
|
29
|
+
UNCALIBRATED: 0,
|
|
30
|
+
LOCKED: 1,
|
|
31
|
+
UNLOCKING: 2,
|
|
32
|
+
UNLOCKED: 3,
|
|
33
|
+
LOCKING: 4,
|
|
34
|
+
UNLATCHED: 5,
|
|
35
|
+
UNLOCKED_LOCK_N_GO: 6,
|
|
36
|
+
UNLATCHING: 7,
|
|
37
|
+
MOTOR_BLOCKED: 254,
|
|
38
|
+
UNDEFINED: 255
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
static get DoorSensorStates () {
|
|
43
|
+
return {
|
|
44
|
+
DEACTIVATED: 0,
|
|
45
|
+
CLOSED: 2,
|
|
46
|
+
OPEN: 3,
|
|
47
|
+
UNKNOWN: 4,
|
|
48
|
+
CALIBRATING: 5
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
static get LockActions () {
|
|
53
|
+
return {
|
|
54
|
+
UNLOCK: 1,
|
|
55
|
+
LOCK: 2,
|
|
56
|
+
UNLATCH: 3,
|
|
57
|
+
LOCK_N_GO: 4,
|
|
58
|
+
LOCK_N_GO_WITH_UNLATCH: 5
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
static get OpenerModes () {
|
|
63
|
+
return {
|
|
64
|
+
DOOR_MODE: 2,
|
|
65
|
+
CONTINUOUS_MODE: 3
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
static get OpenerStates () {
|
|
70
|
+
return {
|
|
71
|
+
UNTRAINED: 0,
|
|
72
|
+
ONLINE: 1,
|
|
73
|
+
RTO_ACTIVE: 3,
|
|
74
|
+
OPEN: 5,
|
|
75
|
+
OPENING: 7,
|
|
76
|
+
BOOT_RUN: 253,
|
|
77
|
+
UNDEFINED: 255
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
static get OpenerActions () {
|
|
82
|
+
return {
|
|
83
|
+
ACTIVATE_RTO: 1,
|
|
84
|
+
DEACTIVATE_RTO: 2,
|
|
85
|
+
OPEN: 3,
|
|
86
|
+
ACTIVATE_CM: 4,
|
|
87
|
+
DEACTIVATE_CM: 5
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
static modelName (deviceType, firmware) {
|
|
92
|
+
switch (deviceType) {
|
|
93
|
+
case NbClient.DeviceTypes.SMARTLOCK:
|
|
94
|
+
if (firmware[0] === '1') {
|
|
95
|
+
return 'Smart Lock 1.0'
|
|
96
|
+
}
|
|
97
|
+
return 'Smart Lock 2.0'
|
|
98
|
+
case NbClient.DeviceTypes.OPENER:
|
|
99
|
+
return 'Opener'
|
|
100
|
+
case NbClient.DeviceTypes.SMARTDOOR:
|
|
101
|
+
return 'Smart Door'
|
|
102
|
+
case NbClient.DeviceTypes.SMARTLOCK3:
|
|
103
|
+
return 'Smart Lock 3.0'
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
constructor (params = {}) {
|
|
108
|
+
const _params = {
|
|
109
|
+
encryption: sodiumPlus == null ? 'hashedToken' : 'encryptedToken',
|
|
110
|
+
port: 8080,
|
|
111
|
+
timeout: 5
|
|
112
|
+
}
|
|
113
|
+
const optionParser = new OptionParser(_params)
|
|
114
|
+
optionParser
|
|
115
|
+
.hostKey('host')
|
|
116
|
+
.intKey('timeout', 1, 60)
|
|
117
|
+
.stringKey('token')
|
|
118
|
+
.enumKey('encryption')
|
|
119
|
+
.enumKeyValue('encryption', 'none')
|
|
120
|
+
.enumKeyValue('encryption', 'hashedToken')
|
|
121
|
+
.enumKeyValue('encryption', 'encryptedToken')
|
|
122
|
+
.parse(params)
|
|
123
|
+
super({
|
|
124
|
+
host: _params.hostname + ':' + _params.port,
|
|
125
|
+
json: true,
|
|
126
|
+
keepAlive: true,
|
|
127
|
+
maxSockets: 1,
|
|
128
|
+
timeout: _params.timeout
|
|
129
|
+
})
|
|
130
|
+
this._params = _params
|
|
131
|
+
this._jsonFormatter = new JsonFormatter()
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
get id () { return this._params.id }
|
|
135
|
+
get name () { return 'Nuki_Bridge_' + this._params.id }
|
|
136
|
+
get encryption () { return this._params.encryption }
|
|
137
|
+
get firmware () { return this._params.firmware }
|
|
138
|
+
get token () { return this._params.token }
|
|
139
|
+
|
|
140
|
+
async auth () {
|
|
141
|
+
const response = await this._get('/auth')
|
|
142
|
+
if (response.body.success) {
|
|
143
|
+
this._params.token = response.body.token
|
|
144
|
+
return response.body.token
|
|
145
|
+
}
|
|
146
|
+
throw new Error('Nuki bridge button not pressed')
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async info () { return this._get('/info') }
|
|
150
|
+
async list () { return this._get('/list') }
|
|
151
|
+
async log () { return this._get('/log') }
|
|
152
|
+
async reboot () { return this._get('/reboot') }
|
|
153
|
+
async fwupdate () { return this._get('/fwupdate') }
|
|
154
|
+
|
|
155
|
+
async lockState (nukiId, deviceType) {
|
|
156
|
+
return this._get('/lockState', { nukiId, deviceType })
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async lock (nukiId, deviceType) {
|
|
160
|
+
return this._get('/lock', { nukiId, deviceType })
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async unlock (nukiId, deviceType) {
|
|
164
|
+
return this._get('/unlock', { nukiId, deviceType })
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async lockAction (nukiId, deviceType, action) {
|
|
168
|
+
return this._get(
|
|
169
|
+
'/lockAction', { nukiId, deviceType, action }
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async init () {
|
|
174
|
+
const response = await this.info()
|
|
175
|
+
this._params.id = response.body.ids.serverId.toString(16).toUpperCase()
|
|
176
|
+
this._params.firmware = response.body.versions.firmwareVersion
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async callbackAdd (url) {
|
|
180
|
+
return this._get('/callback/add', { url: encodeURIComponent(url) })
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async callbackList () { return this._get('/callback/list') }
|
|
184
|
+
async callbackRemove (id) { return this._get('/callback/remove', { id }) }
|
|
185
|
+
|
|
186
|
+
async _get (resource, params = {}) {
|
|
187
|
+
// Append parameters
|
|
188
|
+
let separator = '?'
|
|
189
|
+
for (const param in params) {
|
|
190
|
+
resource += separator + param + '=' + params[param]
|
|
191
|
+
separator = '&'
|
|
192
|
+
}
|
|
193
|
+
let suffix = ''
|
|
194
|
+
if (resource !== '/auth') {
|
|
195
|
+
if (this._params.encryption === 'none') {
|
|
196
|
+
suffix = separator + 'token=' + this._params.token
|
|
197
|
+
} else {
|
|
198
|
+
const date = new Date().toISOString()
|
|
199
|
+
const ts = date.slice(0, 19) + 'Z'
|
|
200
|
+
if (this._params.encryption === 'encryptedToken') {
|
|
201
|
+
if (this._sodium == null) {
|
|
202
|
+
const hash = crypto.createHash('sha256')
|
|
203
|
+
hash.update(this._params.token)
|
|
204
|
+
this._key = new CryptographyKey(hash.digest())
|
|
205
|
+
this._sodium = await SodiumPlus.auto()
|
|
206
|
+
}
|
|
207
|
+
const rnr = await this._sodium.randombytes_uniform(10000)
|
|
208
|
+
const nonce = await this._sodium.randombytes_buf(24)
|
|
209
|
+
const ctoken = await this._sodium.crypto_secretbox([ts, rnr].join(','), nonce, this._key)
|
|
210
|
+
suffix = separator + 'ctoken=' + ctoken.toString('hex') + '&nonce=' + nonce.toString('hex')
|
|
211
|
+
} else if (this._params.encryption === 'hashedToken') { // deprecated
|
|
212
|
+
const hash = crypto.createHash('sha256')
|
|
213
|
+
const rnr = Number(date[18] + date.slice(20, 23))
|
|
214
|
+
hash.update([ts, rnr, this._params.token].join(','))
|
|
215
|
+
suffix = separator + 'ts=' + ts + '&rnr=' + rnr +
|
|
216
|
+
'&hash=' + hash.digest('hex')
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return super.get(resource, undefined, suffix)
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
module.exports = NbClient
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// hb-nb-tools/lib/NbDiscovery.js
|
|
2
|
+
// Copyright © 2020-2023 Erik Baauw. All rights reserved.
|
|
3
|
+
//
|
|
4
|
+
// Homebridge NB Tools.
|
|
5
|
+
|
|
6
|
+
'use strict'
|
|
7
|
+
|
|
8
|
+
const hbLibTools = require('hb-lib-tools')
|
|
9
|
+
const { HttpClient, OptionParser } = hbLibTools
|
|
10
|
+
|
|
11
|
+
class NbDiscovery extends HttpClient {
|
|
12
|
+
constructor (params = {}) {
|
|
13
|
+
const config = {
|
|
14
|
+
timeout: 5
|
|
15
|
+
}
|
|
16
|
+
const optionParser = new OptionParser(config)
|
|
17
|
+
optionParser.intKey('timeout', 1, 60)
|
|
18
|
+
optionParser.parse(params)
|
|
19
|
+
super({
|
|
20
|
+
https: true,
|
|
21
|
+
host: 'api.nuki.io',
|
|
22
|
+
json: true,
|
|
23
|
+
keepAlive: false,
|
|
24
|
+
name: 'nuki server',
|
|
25
|
+
path: '/discover',
|
|
26
|
+
timeout: config.timeout
|
|
27
|
+
})
|
|
28
|
+
this.config = config
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async discover () {
|
|
32
|
+
const bridges = []
|
|
33
|
+
const response = await super.get('/bridges')
|
|
34
|
+
if (response == null) {
|
|
35
|
+
return bridges
|
|
36
|
+
}
|
|
37
|
+
for (const bridge of response.body.bridges) {
|
|
38
|
+
const client = new HttpClient({
|
|
39
|
+
host: bridge.ip + ':' + bridge.port,
|
|
40
|
+
path: '',
|
|
41
|
+
timeout: this.config.timeout,
|
|
42
|
+
validStatusCodes: [200, 401]
|
|
43
|
+
})
|
|
44
|
+
client
|
|
45
|
+
.on('error', (error) => { this.emit('error', error) })
|
|
46
|
+
.on('request', (request) => { this.emit('request', request) })
|
|
47
|
+
.on('response', (response) => { this.emit('response', response) })
|
|
48
|
+
try {
|
|
49
|
+
await client.get('/info')
|
|
50
|
+
bridges.push(bridge)
|
|
51
|
+
} catch (error) {}
|
|
52
|
+
}
|
|
53
|
+
return bridges
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = NbDiscovery
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// homebridge-nb/lib/NbListener.js
|
|
2
|
+
// Copyright © 2020-2023 Erik Baauw. All rights reserved.
|
|
3
|
+
//
|
|
4
|
+
// Homebridge NB Tools.
|
|
5
|
+
|
|
6
|
+
'use strict'
|
|
7
|
+
|
|
8
|
+
const events = require('events')
|
|
9
|
+
const http = require('http')
|
|
10
|
+
|
|
11
|
+
class NbListener extends events.EventEmitter {
|
|
12
|
+
constructor (port = 0) {
|
|
13
|
+
super()
|
|
14
|
+
this._myPort = port
|
|
15
|
+
this._clients = {}
|
|
16
|
+
this._clientsByName = {}
|
|
17
|
+
this._server = http.createServer((request, response) => {
|
|
18
|
+
let buffer = ''
|
|
19
|
+
request.on('data', (data) => {
|
|
20
|
+
buffer += data
|
|
21
|
+
})
|
|
22
|
+
request.on('end', async () => {
|
|
23
|
+
try {
|
|
24
|
+
request.body = buffer
|
|
25
|
+
if (request.method === 'GET' && request.url === '/notify') {
|
|
26
|
+
// Provide an easy way to check that listener is reachable.
|
|
27
|
+
response.writeHead(200, { 'Content-Type': 'text/html' })
|
|
28
|
+
response.write('<table>')
|
|
29
|
+
response.write(`<caption><h3>Listening to ${Object.keys(this._clients).length} clients</h3></caption>`)
|
|
30
|
+
response.write('<tr><th scope="col">Nuki Bridge</th>')
|
|
31
|
+
response.write('<th scope="col">IP Address</th>')
|
|
32
|
+
response.write('<th scope="col">Local IP Address</th>')
|
|
33
|
+
for (const name of Object.keys(this._clientsByName).sort()) {
|
|
34
|
+
const client = this._clientsByName[name]
|
|
35
|
+
response.write(`<tr><td>${name}</td><td>${client.address}</td>`)
|
|
36
|
+
response.write(`<td>${client.localAddress}</td></tr>`)
|
|
37
|
+
}
|
|
38
|
+
response.write('</table>')
|
|
39
|
+
} else if (request.method === 'POST') {
|
|
40
|
+
const array = request.url.split('/')
|
|
41
|
+
const client = this._clients[array[2]]
|
|
42
|
+
if (array[1] === 'notify' && client !== null) {
|
|
43
|
+
const obj = JSON.parse(request.body)
|
|
44
|
+
client.emit('event', obj)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
response.end()
|
|
48
|
+
} catch (error) {
|
|
49
|
+
this.emit('error', error)
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
})
|
|
53
|
+
this._server
|
|
54
|
+
.on('error', (error) => { this.emit('error', error) })
|
|
55
|
+
.on('close', () => {
|
|
56
|
+
this.emit('close', this._callbackUrl)
|
|
57
|
+
delete this._callbackUrl
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async _listen () {
|
|
62
|
+
if (this._server.listening) {
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
return new Promise((resolve, reject) => {
|
|
66
|
+
try {
|
|
67
|
+
this._server.listen(this._myPort, '0.0.0.0', () => {
|
|
68
|
+
const address = this._server.address()
|
|
69
|
+
this._myIp = address.address
|
|
70
|
+
this._myPort = address.port
|
|
71
|
+
this._callbackUrl =
|
|
72
|
+
'http://' + this._myIp + ':' + this._myPort + '/notify'
|
|
73
|
+
/** Emitted when the web server has started.
|
|
74
|
+
* @event NbListener#listening
|
|
75
|
+
* @param {string} url - The url the web server is listening on.
|
|
76
|
+
*/
|
|
77
|
+
this.emit('listening', this._callbackUrl)
|
|
78
|
+
return resolve()
|
|
79
|
+
})
|
|
80
|
+
} catch (error) {
|
|
81
|
+
return reject(error)
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async addClient (nbClient) {
|
|
87
|
+
this._clients[nbClient.id] = nbClient
|
|
88
|
+
this._clientsByName[nbClient.name] = nbClient
|
|
89
|
+
await this._listen()
|
|
90
|
+
const callbackUrl = 'http://' + nbClient.localAddress + ':' + this._myPort +
|
|
91
|
+
'/notify/' + nbClient.id
|
|
92
|
+
return callbackUrl
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async removeClient (nbClient) {
|
|
96
|
+
this.removeAllListeners(nbClient.id)
|
|
97
|
+
delete this._clientsByName[nbClient.name]
|
|
98
|
+
delete this._clients[nbClient.id]
|
|
99
|
+
if (Object.keys(this._clients).length === 0) {
|
|
100
|
+
await this._server.close()
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
module.exports = NbListener
|
package/lib/NbTool.js
ADDED
|
@@ -0,0 +1,645 @@
|
|
|
1
|
+
// hb-nb-tools/lib/nb.js
|
|
2
|
+
// Copyright © 2020-2023 Erik Baauw. All rights reserved.
|
|
3
|
+
//
|
|
4
|
+
// Command line interface to Nuki bridge HTTP API.
|
|
5
|
+
|
|
6
|
+
'use strict'
|
|
7
|
+
|
|
8
|
+
const NbClient = require('./NbClient')
|
|
9
|
+
const NbDiscovery = require('./NbDiscovery')
|
|
10
|
+
const NbListener = require('./NbListener')
|
|
11
|
+
const hbLibTools = require('hb-lib-tools')
|
|
12
|
+
const packageJson = require('../package.json')
|
|
13
|
+
|
|
14
|
+
const { CommandLineParser, CommandLineTool, JsonFormatter, OptionParser } = hbLibTools
|
|
15
|
+
const { b, u } = CommandLineTool
|
|
16
|
+
const { UsageError } = CommandLineParser
|
|
17
|
+
|
|
18
|
+
const usage = {
|
|
19
|
+
nb: `${b('nb')} [${b('-hVD')}] [${b('-H')} ${u('hostname')}[${b(':')}${u('port')}]] [${b('-T')} ${u('token')}] [${b('-E')} [${b('none')}|${b('hasedToken')}|${b('encryptedToken')}]] [${b('-t')} ${u('timeout')}] ${u('command')} [${u('argument')} ...]`,
|
|
20
|
+
|
|
21
|
+
discover: `${b('discover')} [${b('-h')}] [${b('-t')} ${u('timeout')}]`,
|
|
22
|
+
|
|
23
|
+
auth: `${b('auth')} [${b('-h')}]`,
|
|
24
|
+
info: `${b('info')} [${b('-h')}]`,
|
|
25
|
+
getlog: `${b('getlog')} [${b('-h')}]`,
|
|
26
|
+
clearlog: `${b('clearlog')} [${b('-h')}]`,
|
|
27
|
+
reboot: `${b('reboot')} [${b('-h')}]`,
|
|
28
|
+
fwupdate: `${b('fwupdate')} [${b('-h')}]`,
|
|
29
|
+
list: `${b('list')} [${b('-h')}]`,
|
|
30
|
+
|
|
31
|
+
lockState: `${b('lockState')} [${b('-h')}] ${u('nukiId')} ${u('deviceType')}`,
|
|
32
|
+
lock: `${b('lock')} [${b('-h')}] ${u('nukiId')} ${u('deviceType')}`,
|
|
33
|
+
unlock: `${b('unlock')} [${b('-h')}] ${u('nukiId')} ${u('deviceType')}`,
|
|
34
|
+
lockAction: `${b('lockAction')} [${b('-h')}] ${u('nukiId')} ${u('deviceType')} ${u('action')}`,
|
|
35
|
+
|
|
36
|
+
eventlog: `${b('eventlog')} [${b('-hns')}]`,
|
|
37
|
+
callbackList: `${b('callbackList')} [${b('-h')}]`,
|
|
38
|
+
callbackRemove: `${b('callbackRemove')} [${b('-h')}] ${u('id')}`
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const description = {
|
|
42
|
+
nb: 'Command line interface to Nuki bridge HTTP API.',
|
|
43
|
+
|
|
44
|
+
discover: 'Discover Nuki bridges.',
|
|
45
|
+
|
|
46
|
+
auth: 'Obtain Nuki bridge token.',
|
|
47
|
+
info: 'Get Nuki bridge info.',
|
|
48
|
+
getlog: 'Get Nuki bridge log.',
|
|
49
|
+
clearlog: 'Clear Nuki bridge log.',
|
|
50
|
+
reboot: 'Reboot Nuki bridge.',
|
|
51
|
+
fwupdate: 'Trigger a firmware update of the bridge and connected devices.',
|
|
52
|
+
list: 'Get list of paired Nuki devices.',
|
|
53
|
+
|
|
54
|
+
lockState: 'Refresh state from paired Nuki device.',
|
|
55
|
+
lock: 'Lock paired Nuki device.',
|
|
56
|
+
unlock: 'Unlock paired Nuki device.',
|
|
57
|
+
lockAction: 'Send action to paired Nuki device.',
|
|
58
|
+
|
|
59
|
+
eventlog: 'Add Nuki bridge subscription and listen for events.',
|
|
60
|
+
callbackList: 'List Nuki bridge subscriptions.',
|
|
61
|
+
callbackRemove: 'Remove Nuki bridge subscription.'
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const help = {
|
|
65
|
+
nb: `${description.nb}
|
|
66
|
+
|
|
67
|
+
Usage: ${usage.nb}
|
|
68
|
+
|
|
69
|
+
Parameters:
|
|
70
|
+
${b('-h')}, ${b('--help')}
|
|
71
|
+
Print this help and exit.
|
|
72
|
+
|
|
73
|
+
${b('-V')}, ${b('--version')}
|
|
74
|
+
Print version and exit.
|
|
75
|
+
|
|
76
|
+
${b('-D')}, ${b('--debug')}
|
|
77
|
+
Print debug messages for communication with Nuki bridge.
|
|
78
|
+
|
|
79
|
+
${b('-H')} ${u('hostname')}[${b(':')}${u('port')}], ${b('--host=')}${u('hostname')}[${b(':')}${u('port')}]
|
|
80
|
+
Connect to Nuki bridge at ${u('hostname')}${b(':8080')} or ${u('hostname')}${b(':')}${u('port')}.
|
|
81
|
+
You can also specify the hostname and port in the ${b('NB_HOST')} environment variable.
|
|
82
|
+
|
|
83
|
+
${b('-T')} ${u('token')}, ${b('--token=')}${u('token')}
|
|
84
|
+
Use token ${u('token')} to connect to the Nuki bridge.
|
|
85
|
+
You can also specify the token in the ${b('NB_TOKEN')} environment variable.
|
|
86
|
+
|
|
87
|
+
${b('-E')} [${b('none')}|${b('hasedToken')}|${b('encryptedToken')}], ${b('--encryption=')}[${b('none')}|${b('hasedToken')}|${b('encryptedToken')}]
|
|
88
|
+
Use encryption method for communication with the Nuki bridge.
|
|
89
|
+
The default is ${b('hashedToken')}.
|
|
90
|
+
|
|
91
|
+
${b('-t')} ${u('timeout')}
|
|
92
|
+
Set timeout to ${u('timeout')} seconds instead of default ${b('5')}.
|
|
93
|
+
|
|
94
|
+
Commands:
|
|
95
|
+
${usage.discover}
|
|
96
|
+
${description.discover}
|
|
97
|
+
|
|
98
|
+
${usage.auth}
|
|
99
|
+
${description.auth}
|
|
100
|
+
|
|
101
|
+
${usage.info}
|
|
102
|
+
${description.info}
|
|
103
|
+
|
|
104
|
+
${usage.getlog}
|
|
105
|
+
${description.getlog}
|
|
106
|
+
|
|
107
|
+
${usage.clearlog}
|
|
108
|
+
${description.clearlog}
|
|
109
|
+
|
|
110
|
+
${usage.reboot}
|
|
111
|
+
${description.reboot}
|
|
112
|
+
|
|
113
|
+
${usage.fwupdate}
|
|
114
|
+
${description.fwupdate}
|
|
115
|
+
|
|
116
|
+
${usage.list}
|
|
117
|
+
${description.list}
|
|
118
|
+
|
|
119
|
+
${usage.lockState}
|
|
120
|
+
${description.lockState}
|
|
121
|
+
|
|
122
|
+
${usage.lock}
|
|
123
|
+
${description.lock}
|
|
124
|
+
|
|
125
|
+
${usage.unlock}
|
|
126
|
+
${description.unlock}
|
|
127
|
+
|
|
128
|
+
${usage.lockAction}
|
|
129
|
+
${description.lockAction}
|
|
130
|
+
|
|
131
|
+
${usage.eventlog}
|
|
132
|
+
${description.eventlog}
|
|
133
|
+
|
|
134
|
+
${usage.callbackList}
|
|
135
|
+
${description.callbackList}
|
|
136
|
+
|
|
137
|
+
${usage.callbackRemove}
|
|
138
|
+
${description.callbackRemove}
|
|
139
|
+
|
|
140
|
+
For more help, issue: ${b('nb')} ${u('command')} ${b('-h')}`,
|
|
141
|
+
discover: `${description.discover}
|
|
142
|
+
|
|
143
|
+
Usage: ${usage.discover}
|
|
144
|
+
|
|
145
|
+
Parameters:
|
|
146
|
+
${b('-h')}, ${b('--help')}
|
|
147
|
+
Print this help and exit.`,
|
|
148
|
+
auth: `${description.auth}
|
|
149
|
+
|
|
150
|
+
Usage: ${usage.auth}
|
|
151
|
+
|
|
152
|
+
Parameters:
|
|
153
|
+
${b('-h')}, ${b('--help')}
|
|
154
|
+
Print this help and exit.`,
|
|
155
|
+
info: `${description.info}
|
|
156
|
+
|
|
157
|
+
Usage: ${usage.info}
|
|
158
|
+
|
|
159
|
+
Parameters:
|
|
160
|
+
${b('-h')}, ${b('--help')}
|
|
161
|
+
Print this help and exit.`,
|
|
162
|
+
getlog: `${description.getlog}
|
|
163
|
+
|
|
164
|
+
Usage: ${usage.getlog}
|
|
165
|
+
|
|
166
|
+
Parameters:
|
|
167
|
+
${b('-h')}, ${b('--help')}
|
|
168
|
+
Print this help and exit.`,
|
|
169
|
+
clearlog: `${description.clearlog}
|
|
170
|
+
|
|
171
|
+
Usage: ${usage.clearlog}
|
|
172
|
+
|
|
173
|
+
Parameters:
|
|
174
|
+
${b('-h')}, ${b('--help')}
|
|
175
|
+
Print this help and exit.`,
|
|
176
|
+
reboot: `${description.reboot}
|
|
177
|
+
|
|
178
|
+
Usage: ${usage.reboot}
|
|
179
|
+
|
|
180
|
+
Parameters:
|
|
181
|
+
${b('-h')}, ${b('--help')}
|
|
182
|
+
Print this help and exit.`,
|
|
183
|
+
fwupdate: `${description.fwupdate}
|
|
184
|
+
|
|
185
|
+
Usage: ${usage.fwupdate}
|
|
186
|
+
|
|
187
|
+
Parameters:
|
|
188
|
+
${b('-h')}, ${b('--help')}
|
|
189
|
+
Print this help and exit.`,
|
|
190
|
+
list: `${description.list}
|
|
191
|
+
|
|
192
|
+
Usage: ${usage.list}
|
|
193
|
+
|
|
194
|
+
Parameters:
|
|
195
|
+
${b('-h')}, ${b('--help')}
|
|
196
|
+
Print this help and exit.`,
|
|
197
|
+
lockState: `${description.lockState}
|
|
198
|
+
|
|
199
|
+
Usage: ${usage.lockState}
|
|
200
|
+
|
|
201
|
+
Parameters:
|
|
202
|
+
${b('-h')}, ${b('--help')}
|
|
203
|
+
Print this help and exit.
|
|
204
|
+
|
|
205
|
+
${u('nukiId')}
|
|
206
|
+
The ID of the Nuki device (from ${b('nb list')}).
|
|
207
|
+
|
|
208
|
+
${u('deviceType')}
|
|
209
|
+
The type of the Nuki device (from ${b('nb list')}):
|
|
210
|
+
0: Smart Lock 1.0 or 2.0
|
|
211
|
+
2: Opener
|
|
212
|
+
3: Smart Door
|
|
213
|
+
4: Smart Lock 3.0`,
|
|
214
|
+
lock: `${description.lock}
|
|
215
|
+
|
|
216
|
+
Usage: ${usage.lock}
|
|
217
|
+
|
|
218
|
+
Parameters:
|
|
219
|
+
${b('-h')}, ${b('--help')}
|
|
220
|
+
Print this help and exit.
|
|
221
|
+
|
|
222
|
+
${u('nukiId')}
|
|
223
|
+
The ID of the Nuki device (from ${b('nb list')}).
|
|
224
|
+
|
|
225
|
+
${u('deviceType')}
|
|
226
|
+
The type of the Nuki device (from ${b('nb list')}):
|
|
227
|
+
0: Smart Lock 1.0 or 2.0
|
|
228
|
+
2: Opener
|
|
229
|
+
3: Smart Door
|
|
230
|
+
4: Smart Lock 3.0`,
|
|
231
|
+
unlock: `${description.unlock}
|
|
232
|
+
|
|
233
|
+
Usage: ${usage.unlock}
|
|
234
|
+
|
|
235
|
+
Parameters:
|
|
236
|
+
${b('-h')}, ${b('--help')}
|
|
237
|
+
Print this help and exit.
|
|
238
|
+
|
|
239
|
+
${u('nukiId')}
|
|
240
|
+
The ID of the Nuki device (from ${b('nb list')}).
|
|
241
|
+
|
|
242
|
+
${u('deviceType')}
|
|
243
|
+
The type of the Nuki device (from ${b('nb list')}):
|
|
244
|
+
0: Smart Lock 1.0 or 2.0
|
|
245
|
+
2: Opener
|
|
246
|
+
3: Smart Door
|
|
247
|
+
4: Smart Lock 3.0`,
|
|
248
|
+
lockAction: `${description.lockAction}
|
|
249
|
+
|
|
250
|
+
Usage: ${usage.lockAction}
|
|
251
|
+
|
|
252
|
+
Parameters:
|
|
253
|
+
${b('-h')}, ${b('--help')}
|
|
254
|
+
Print this help and exit.
|
|
255
|
+
|
|
256
|
+
${u('nukiId')}
|
|
257
|
+
The ID of the Nuki device (from ${b('nb list')}).
|
|
258
|
+
|
|
259
|
+
${u('deviceType')}
|
|
260
|
+
The type of the Nuki device (from ${b('nb list')}):
|
|
261
|
+
0: Smart Lock 1.0 or 2.0
|
|
262
|
+
2: Opener
|
|
263
|
+
3: Smart Door
|
|
264
|
+
4: Smart Lock 3.0
|
|
265
|
+
|
|
266
|
+
${u('action')}
|
|
267
|
+
The action to send to the Nuki device:
|
|
268
|
+
Smart Lock, Smart Door Opener
|
|
269
|
+
- ------------------------ -------------------------
|
|
270
|
+
1 unlock activate rto
|
|
271
|
+
2 lock deactivate rto
|
|
272
|
+
3 unlatch electric strike actuation
|
|
273
|
+
4 lock ‘n’ go activate continuous mode
|
|
274
|
+
5 lock ‘n’ go with unlatch deactivate continuous mode`,
|
|
275
|
+
eventlog: `${description.eventlog}
|
|
276
|
+
|
|
277
|
+
Usage: ${usage.eventlog}
|
|
278
|
+
|
|
279
|
+
Parameters:
|
|
280
|
+
${b('-h')}, ${b('--help')}
|
|
281
|
+
Print this help and exit.
|
|
282
|
+
|
|
283
|
+
${b('-n')}, ${b('--noWhiteSpace')}
|
|
284
|
+
Do not include spaces nor newlines in JSON output.
|
|
285
|
+
|
|
286
|
+
${b('-s')}, ${b('--service')}
|
|
287
|
+
Do not output timestamps (useful when running as service).`,
|
|
288
|
+
callbackList: `${description.callbackList}
|
|
289
|
+
|
|
290
|
+
Usage: ${usage.callbackList}
|
|
291
|
+
|
|
292
|
+
Parameters:
|
|
293
|
+
${b('-h')}, ${b('--help')}
|
|
294
|
+
Print this help and exit.`,
|
|
295
|
+
callbackRemove: `${description.callbackRemove}
|
|
296
|
+
|
|
297
|
+
Usage: ${usage.callbackRemove}
|
|
298
|
+
|
|
299
|
+
Parameters:
|
|
300
|
+
${b('-h')}, ${b('--help')}
|
|
301
|
+
Print this help and exit.
|
|
302
|
+
|
|
303
|
+
${u('id')}
|
|
304
|
+
Remove callback with ID ${u('id')} (from ${b('nb callbackList')}).`
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
class NbTool extends CommandLineTool {
|
|
308
|
+
constructor (pkgJson) {
|
|
309
|
+
super({ mode: 'command', debug: false })
|
|
310
|
+
this.pkgJson = pkgJson
|
|
311
|
+
this.usage = usage.nb
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
parseArguments () {
|
|
315
|
+
const parser = new CommandLineParser(this.pkgJson)
|
|
316
|
+
const clargs = {
|
|
317
|
+
options: {
|
|
318
|
+
host: process.env.NB_HOST,
|
|
319
|
+
token: process.env.NB_TOKEN
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
parser
|
|
323
|
+
.help('h', 'help', help.nb)
|
|
324
|
+
.version('V', 'version')
|
|
325
|
+
.flag('D', 'debug', () => {
|
|
326
|
+
if (this.debugEnabled) {
|
|
327
|
+
this.setOptions({ vdebug: true })
|
|
328
|
+
} else {
|
|
329
|
+
this.setOptions({ debug: true, chalk: true })
|
|
330
|
+
}
|
|
331
|
+
})
|
|
332
|
+
.option('H', 'host', (value) => {
|
|
333
|
+
OptionParser.toHost('host', value, false, true)
|
|
334
|
+
clargs.options.host = value
|
|
335
|
+
})
|
|
336
|
+
.option('t', 'timeout', (value) => {
|
|
337
|
+
clargs.options.timeout = OptionParser.toInt('timeout', value, 1, 60, true)
|
|
338
|
+
})
|
|
339
|
+
.option('T', 'token', (value) => {
|
|
340
|
+
clargs.options.token = OptionParser.toString(
|
|
341
|
+
'token', value, true, true
|
|
342
|
+
)
|
|
343
|
+
})
|
|
344
|
+
.option('E', 'encryption', (value) => {
|
|
345
|
+
clargs.options.encryption = OptionParser.toString(
|
|
346
|
+
'encryption', value, true, true
|
|
347
|
+
)
|
|
348
|
+
if (!['none', 'hashedToken', 'encryptedToken'].includes(clargs.options.encryption)) {
|
|
349
|
+
throw new UsageError(`${value}: invalid encryption value`)
|
|
350
|
+
}
|
|
351
|
+
})
|
|
352
|
+
.parameter('command', (value) => {
|
|
353
|
+
if (usage[value] == null || typeof this[value] !== 'function') {
|
|
354
|
+
throw new UsageError(`${value}: unknown command`)
|
|
355
|
+
}
|
|
356
|
+
clargs.command = value
|
|
357
|
+
})
|
|
358
|
+
.remaining((list) => { clargs.args = list })
|
|
359
|
+
.parse()
|
|
360
|
+
return clargs
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
async main () {
|
|
364
|
+
try {
|
|
365
|
+
this.usage = usage.nb
|
|
366
|
+
const clargs = this.parseArguments()
|
|
367
|
+
this.jsonFormatter = new JsonFormatter({ sortKeys: true })
|
|
368
|
+
if (clargs.command !== 'discover') {
|
|
369
|
+
if (clargs.options.host == null) {
|
|
370
|
+
await this.fatal(`Missing host. Set ${b('NB_HOST')} or specify ${b('-H')}.`)
|
|
371
|
+
}
|
|
372
|
+
if (clargs.command === 'auth') {
|
|
373
|
+
clargs.options.timeout = 60
|
|
374
|
+
}
|
|
375
|
+
const name = clargs.options.host
|
|
376
|
+
this.client = new NbClient(clargs.options)
|
|
377
|
+
this.client
|
|
378
|
+
.on('error', (error) => {
|
|
379
|
+
this.log(
|
|
380
|
+
'%s: request %d: %s %s', name, error.request.id,
|
|
381
|
+
error.request.method, error.request.resource
|
|
382
|
+
)
|
|
383
|
+
this.warn(
|
|
384
|
+
'%s: request %d: error: %s', name, error.request.id, error
|
|
385
|
+
)
|
|
386
|
+
})
|
|
387
|
+
.on('request', (request) => {
|
|
388
|
+
this.debug(
|
|
389
|
+
'%s: request %d: %s %s', name, request.id,
|
|
390
|
+
request.method, request.resource
|
|
391
|
+
)
|
|
392
|
+
this.vdebug(
|
|
393
|
+
'%s: request %d: %s %s', name, request.id,
|
|
394
|
+
request.method, request.url
|
|
395
|
+
)
|
|
396
|
+
})
|
|
397
|
+
.on('response', (response) => {
|
|
398
|
+
this.vdebug(
|
|
399
|
+
'%s: request %d: response: %j', name, response.request.id,
|
|
400
|
+
response.body
|
|
401
|
+
)
|
|
402
|
+
this.debug(
|
|
403
|
+
'%s: request %d: %d %s', name, response.request.id,
|
|
404
|
+
response.statusCode, response.statusMessage
|
|
405
|
+
)
|
|
406
|
+
})
|
|
407
|
+
if (clargs.options.token == null && clargs.command !== 'auth') {
|
|
408
|
+
let args = ''
|
|
409
|
+
if (clargs.options.host !== process.env.NB_HOST) {
|
|
410
|
+
args += ' -H ' + clargs.options.host
|
|
411
|
+
}
|
|
412
|
+
await this.fatal(
|
|
413
|
+
`Missing token. Set ${b('NB_TOKEN')} or specify ${b('-T')}. Run ${b('nb' + args + ' auth')} to obtain the token.`
|
|
414
|
+
)
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
this.name = 'nb ' + clargs.command
|
|
418
|
+
this.usage = `${b('nb')} ${usage[clargs.command]}`
|
|
419
|
+
this.parser = new CommandLineParser(packageJson)
|
|
420
|
+
this.parser.help('h', 'help', help[clargs.command])
|
|
421
|
+
await this[clargs.command](clargs.args)
|
|
422
|
+
} catch (error) {
|
|
423
|
+
await this.fatal(error)
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
async discover (...args) {
|
|
428
|
+
const options = {}
|
|
429
|
+
this.parser
|
|
430
|
+
.option('t', 'timeout', (value, key) => {
|
|
431
|
+
options.timeout = OptionParser.toInt('timeout', value, 1, 60, true)
|
|
432
|
+
})
|
|
433
|
+
.parse(...args)
|
|
434
|
+
const nbDiscovery = new NbDiscovery(options)
|
|
435
|
+
nbDiscovery
|
|
436
|
+
.on('error', (error) => {
|
|
437
|
+
this.log(
|
|
438
|
+
'%s: request %d: %s %s', error.request.name, error.request.id,
|
|
439
|
+
error.request.method, error.request.resource
|
|
440
|
+
)
|
|
441
|
+
this.warn(
|
|
442
|
+
'%s: request %d: error: %s', error.request.name, error.request.id, error
|
|
443
|
+
)
|
|
444
|
+
})
|
|
445
|
+
.on('request', (request) => {
|
|
446
|
+
this.debug(
|
|
447
|
+
'%s: request %d: %s %s', request.name, request.id,
|
|
448
|
+
request.method, request.resource
|
|
449
|
+
)
|
|
450
|
+
this.vdebug(
|
|
451
|
+
'%s: request %d: %s %s', request.name, request.id,
|
|
452
|
+
request.method, request.url
|
|
453
|
+
)
|
|
454
|
+
})
|
|
455
|
+
.on('response', (response) => {
|
|
456
|
+
this.vdebug(
|
|
457
|
+
'%s: request %d: response: %j', response.request.name, response.request.id,
|
|
458
|
+
response.body
|
|
459
|
+
)
|
|
460
|
+
this.debug(
|
|
461
|
+
'%s: request %d: %d %s', response.request.name, response.request.id,
|
|
462
|
+
response.statusCode, response.statusMessage
|
|
463
|
+
)
|
|
464
|
+
})
|
|
465
|
+
const bridges = await nbDiscovery.discover()
|
|
466
|
+
this.print(this.jsonFormatter.stringify(bridges))
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
async auth (...args) {
|
|
470
|
+
this.parser.parse(...args)
|
|
471
|
+
this.log('press button on Nuki bridge to obtain token')
|
|
472
|
+
const token = await this.client.auth()
|
|
473
|
+
this.print(token)
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
async info (...args) {
|
|
477
|
+
this.parser.parse(...args)
|
|
478
|
+
const response = await this.client.info()
|
|
479
|
+
this.print(this.jsonFormatter.stringify(response.body))
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
async getlog (...args) {
|
|
483
|
+
this.parser.parse(...args)
|
|
484
|
+
const response = await this.client.log()
|
|
485
|
+
this.print(this.jsonFormatter.stringify(response.body))
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
async clearlog (...args) {
|
|
489
|
+
this.parser.parse(...args)
|
|
490
|
+
const response = await this.client.clearlog()
|
|
491
|
+
this.print(this.jsonFormatter.stringify(response.body))
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
async reboot (...args) {
|
|
495
|
+
this.parser.parse(...args)
|
|
496
|
+
const response = await this.client.reboot()
|
|
497
|
+
this.print(this.jsonFormatter.stringify(response.body))
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
async fwupdate (...args) {
|
|
501
|
+
this.parser.parse(...args)
|
|
502
|
+
const response = await this.client.fwupdate()
|
|
503
|
+
this.print(this.jsonFormatter.stringify(response.body))
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
async list (...args) {
|
|
507
|
+
this.parser.parse(...args)
|
|
508
|
+
const response = await this.client.list()
|
|
509
|
+
this.print(this.jsonFormatter.stringify(response.body))
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
async lockState (...args) {
|
|
513
|
+
let nukiId
|
|
514
|
+
let deviceType
|
|
515
|
+
this.parser
|
|
516
|
+
.parameter('nukiId', (value) => {
|
|
517
|
+
nukiId = OptionParser.toInt('nukiId', value, 0, Infinity, true)
|
|
518
|
+
})
|
|
519
|
+
.parameter('deviceType', (value) => {
|
|
520
|
+
deviceType = OptionParser.toInt('deviceType', value, 0, 2, true)
|
|
521
|
+
})
|
|
522
|
+
.parse(...args)
|
|
523
|
+
const response = await this.client.lockState(nukiId, deviceType)
|
|
524
|
+
this.print(this.jsonFormatter.stringify(response.body))
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
async lock (...args) {
|
|
528
|
+
let nukiId
|
|
529
|
+
let deviceType
|
|
530
|
+
this.parser
|
|
531
|
+
.parameter('nukiId', (value) => {
|
|
532
|
+
nukiId = OptionParser.toInt('nukiId', value, 0, Infinity, true)
|
|
533
|
+
})
|
|
534
|
+
.parameter('deviceType', (value) => {
|
|
535
|
+
deviceType = OptionParser.toInt('deviceType', value, 0, 2, true)
|
|
536
|
+
})
|
|
537
|
+
.parse(...args)
|
|
538
|
+
const response = await this.client.lock(nukiId, deviceType)
|
|
539
|
+
this.print(this.jsonFormatter.stringify(response.body))
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
async unlock (...args) {
|
|
543
|
+
let nukiId
|
|
544
|
+
let deviceType
|
|
545
|
+
this.parser
|
|
546
|
+
.parameter('nukiId', (value) => {
|
|
547
|
+
nukiId = OptionParser.toInt('nukiId', value, 0, Infinity, true)
|
|
548
|
+
})
|
|
549
|
+
.parameter('deviceType', (value) => {
|
|
550
|
+
deviceType = OptionParser.toInt('deviceType', value, 0, 2, true)
|
|
551
|
+
})
|
|
552
|
+
.parse(...args)
|
|
553
|
+
const response = await this.client.unlock(nukiId, deviceType)
|
|
554
|
+
this.print(this.jsonFormatter.stringify(response.body))
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
async lockAction (...args) {
|
|
558
|
+
let nukiId
|
|
559
|
+
let deviceType
|
|
560
|
+
let action
|
|
561
|
+
this.parser
|
|
562
|
+
.parameter('nukiId', (value) => {
|
|
563
|
+
nukiId = OptionParser.toInt('nukiId', value, 0, Infinity, true)
|
|
564
|
+
})
|
|
565
|
+
.parameter('deviceType', (value) => {
|
|
566
|
+
deviceType = OptionParser.toInt('deviceType', value, 0, 2, true)
|
|
567
|
+
})
|
|
568
|
+
.parameter('action', (value) => {
|
|
569
|
+
action = OptionParser.toInt('action', value, 1, 5, true)
|
|
570
|
+
})
|
|
571
|
+
.parse(...args)
|
|
572
|
+
const response = await this.client.lockAction(nukiId, deviceType, action)
|
|
573
|
+
this.print(this.jsonFormatter.stringify(response.body))
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
async destroy () {
|
|
577
|
+
if (this.listener != null) {
|
|
578
|
+
const response = await this.client.callbackList()
|
|
579
|
+
for (const callback of response.body.callbacks) {
|
|
580
|
+
if (callback.url === this._callbackUrl) {
|
|
581
|
+
this.log(
|
|
582
|
+
'Removing subscription %s for %s', callback.id, callback.url
|
|
583
|
+
)
|
|
584
|
+
try {
|
|
585
|
+
await this.client.callbackRemove(callback.id)
|
|
586
|
+
} catch (error) {
|
|
587
|
+
this.error(error)
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
this.listener.removeClient(this.client)
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
async eventlog (...args) {
|
|
596
|
+
let noWhiteSpace = false
|
|
597
|
+
let mode = 'daemon'
|
|
598
|
+
this.parser
|
|
599
|
+
.flag('n', 'noWhiteSpace', () => { noWhiteSpace = true })
|
|
600
|
+
.flag('s', 'service', () => { mode = 'service' })
|
|
601
|
+
.parse(...args)
|
|
602
|
+
this.setOptions({ mode })
|
|
603
|
+
const jsonFormatter = new JsonFormatter({ sortKeys: true, noWhiteSpace })
|
|
604
|
+
|
|
605
|
+
this.listener = new NbListener()
|
|
606
|
+
this.listener
|
|
607
|
+
.on('error', (error) => { this.error(error) })
|
|
608
|
+
.on('listening', (url) => {
|
|
609
|
+
this.log('listening on %s', url)
|
|
610
|
+
})
|
|
611
|
+
.on('close', (url) => {
|
|
612
|
+
this.log('closed %s', url)
|
|
613
|
+
})
|
|
614
|
+
this.client.on('event', (event) => {
|
|
615
|
+
this.log('%s', jsonFormatter.stringify(event))
|
|
616
|
+
})
|
|
617
|
+
|
|
618
|
+
await this.client.init()
|
|
619
|
+
this._callbackUrl = await this.listener.addClient(this.client)
|
|
620
|
+
const response = await this.client.callbackAdd(this._callbackUrl)
|
|
621
|
+
if (!response.body.success) {
|
|
622
|
+
this.listener.removeClient(this.client)
|
|
623
|
+
this.error(response.body.message)
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
async callbackList (...args) {
|
|
628
|
+
this.parser.parse(...args)
|
|
629
|
+
const response = await this.client.callbackList()
|
|
630
|
+
this.print(this.jsonFormatter.stringify(response.body))
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
async callbackRemove (...args) {
|
|
634
|
+
let id
|
|
635
|
+
this.parser
|
|
636
|
+
.parameter('id', (value) => {
|
|
637
|
+
id = OptionParser.toInt('id', value, 0, Infinity, true)
|
|
638
|
+
})
|
|
639
|
+
.parse(...args)
|
|
640
|
+
const response = await this.client.callbackRemove(id)
|
|
641
|
+
this.print(this.jsonFormatter.stringify(response.body))
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
module.exports = NbTool
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "hb-nb-tools",
|
|
3
|
+
"description": "Homebridge NB Command-Line Tools",
|
|
4
|
+
"author": "Erik Baauw",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"version": "1.0.0",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"nuki",
|
|
9
|
+
"smart-lock",
|
|
10
|
+
"lock",
|
|
11
|
+
"opener"
|
|
12
|
+
],
|
|
13
|
+
"bin": {
|
|
14
|
+
"nb": "cli/nb.js"
|
|
15
|
+
},
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": "^18.15.0"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"hb-lib-tools": "~1.0.6"
|
|
21
|
+
},
|
|
22
|
+
"optionalDependencies": {
|
|
23
|
+
"sodium-plus": "~0.9.0"
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"prepare": "standard",
|
|
27
|
+
"test": "standard && echo \"Error: no test specified\" && exit 1"
|
|
28
|
+
},
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/ebaauw/hb-nb-tools.git"
|
|
32
|
+
},
|
|
33
|
+
"bugs": {
|
|
34
|
+
"url": "https://github.com/ebaauw/hb-nb-tools/issues"
|
|
35
|
+
},
|
|
36
|
+
"homepage": "https://github.com/ebaauw/hb-nb-tools#readme"
|
|
37
|
+
}
|