homebridge-deconz 1.2.11 → 1.3.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/config.schema.json +0 -5
- package/homebridge-ui/public/index.html +0 -5
- package/homebridge-ui/server.js +0 -1
- package/lib/Deconz/Resource.js +1 -1
- package/lib/DeconzAccessory/Gateway.js +78 -38
- package/lib/DeconzAccessory/index.js +14 -0
- package/lib/DeconzPlatform.js +6 -5
- package/lib/DeconzService/Contact.js +1 -1
- package/lib/DeconzService/DoorLock.js +56 -0
- package/package.json +5 -5
package/config.schema.json
CHANGED
|
@@ -14,10 +14,6 @@
|
|
|
14
14
|
"required": true,
|
|
15
15
|
"default": "deCONZ"
|
|
16
16
|
},
|
|
17
|
-
"forceHttp": {
|
|
18
|
-
"description": "Use plain http instead of https.",
|
|
19
|
-
"type": "boolean"
|
|
20
|
-
},
|
|
21
17
|
"hosts": {
|
|
22
18
|
"title": "Gateways",
|
|
23
19
|
"type": "array",
|
|
@@ -94,7 +90,6 @@
|
|
|
94
90
|
"title": "Advanced Settings",
|
|
95
91
|
"description": "Don't change these, unless you understand what you're doing.",
|
|
96
92
|
"items": [
|
|
97
|
-
"forceHttp",
|
|
98
93
|
"noResponse",
|
|
99
94
|
"parallelRequests",
|
|
100
95
|
"stealth",
|
|
@@ -82,10 +82,6 @@ Copyright © 2022-2026 Erik Baauw. All rights reserved.
|
|
|
82
82
|
default: 'deCONZ',
|
|
83
83
|
type: 'string'
|
|
84
84
|
},
|
|
85
|
-
forceHttp: {
|
|
86
|
-
description: "Use plain http instead of https.",
|
|
87
|
-
type: "boolean"
|
|
88
|
-
},
|
|
89
85
|
noResponse: {
|
|
90
86
|
description: "Report unreachable lights as <i>No Response</i> in HomeKit.",
|
|
91
87
|
type: "boolean"
|
|
@@ -147,7 +143,6 @@ Copyright © 2022-2026 Erik Baauw. All rights reserved.
|
|
|
147
143
|
title: "Advanced Settings",
|
|
148
144
|
description: "Don't change these, unless you understand what you're doing.",
|
|
149
145
|
items: [
|
|
150
|
-
"forceHttp",
|
|
151
146
|
"parallelRequests",
|
|
152
147
|
"stealth",
|
|
153
148
|
"timeout",
|
package/homebridge-ui/server.js
CHANGED
package/lib/Deconz/Resource.js
CHANGED
|
@@ -344,7 +344,7 @@ class Resource {
|
|
|
344
344
|
case 'Battery': return 'Battery'
|
|
345
345
|
case 'CarbonMonoxide': return 'CarbonMonoxide'
|
|
346
346
|
case 'Consumption': return 'Consumption'
|
|
347
|
-
|
|
347
|
+
case 'DoorLock': return 'DoorLock'
|
|
348
348
|
case 'Daylight': return 'Daylight'
|
|
349
349
|
case 'DaylightOffset': return ''
|
|
350
350
|
case 'Fire': return 'Smoke'
|
|
@@ -78,12 +78,6 @@ class Gateway extends AccessoryDelegate {
|
|
|
78
78
|
if (this.context.settingsById == null) {
|
|
79
79
|
this.context.settingsById = {}
|
|
80
80
|
}
|
|
81
|
-
// if (this.context.fullState != null) {
|
|
82
|
-
// this.analyseFullState(this.context.fullState, {
|
|
83
|
-
// analyseOnly: true,
|
|
84
|
-
// logUnsupported: true
|
|
85
|
-
// })
|
|
86
|
-
// }
|
|
87
81
|
|
|
88
82
|
this.addPropertyDelegate({
|
|
89
83
|
key: 'apiKey',
|
|
@@ -127,6 +121,11 @@ class Gateway extends AccessoryDelegate {
|
|
|
127
121
|
this.pollNext = true
|
|
128
122
|
})
|
|
129
123
|
|
|
124
|
+
this.addPropertyDelegate({
|
|
125
|
+
key: 'fingerprint',
|
|
126
|
+
silent: true
|
|
127
|
+
})
|
|
128
|
+
|
|
130
129
|
this.addPropertyDelegate({
|
|
131
130
|
key: 'heartrate',
|
|
132
131
|
value: 30,
|
|
@@ -137,16 +136,15 @@ class Gateway extends AccessoryDelegate {
|
|
|
137
136
|
key: 'host',
|
|
138
137
|
value: params.host,
|
|
139
138
|
silent: true
|
|
140
|
-
}).on('didSet', (value) => {
|
|
141
|
-
if (this.client != null) {
|
|
142
|
-
this.client.host = value
|
|
143
|
-
}
|
|
144
|
-
if (this.wsClient != null) {
|
|
145
|
-
this.wsClient.host = this.values.host.split(':')[0] +
|
|
146
|
-
':' + this.values.wsPort
|
|
147
|
-
}
|
|
148
139
|
})
|
|
149
140
|
|
|
141
|
+
this.addPropertyDelegate({
|
|
142
|
+
key: 'https',
|
|
143
|
+
value: params.config.https,
|
|
144
|
+
silent: true
|
|
145
|
+
})
|
|
146
|
+
this.values.https = params.config.https
|
|
147
|
+
|
|
150
148
|
this.addPropertyDelegate({
|
|
151
149
|
key: 'periodicEvents',
|
|
152
150
|
value: false,
|
|
@@ -196,16 +194,32 @@ class Gateway extends AccessoryDelegate {
|
|
|
196
194
|
}
|
|
197
195
|
})
|
|
198
196
|
|
|
197
|
+
this.addPropertyDelegate({
|
|
198
|
+
key: 'url',
|
|
199
|
+
silent: true
|
|
200
|
+
}).on('didSet', async (value) => {
|
|
201
|
+
if (value != null) {
|
|
202
|
+
this.log('gateway %s at %s', this.id, value)
|
|
203
|
+
try {
|
|
204
|
+
await this.createClient()
|
|
205
|
+
this.values.wsPort = null
|
|
206
|
+
} catch (error) { this.warn(error) }
|
|
207
|
+
}
|
|
208
|
+
})
|
|
209
|
+
this.values.url = null
|
|
210
|
+
|
|
199
211
|
this.addPropertyDelegate({
|
|
200
212
|
key: 'wsPort',
|
|
201
|
-
value:
|
|
213
|
+
value: null,
|
|
202
214
|
silent: true
|
|
203
|
-
}).on('didSet', (value) => {
|
|
204
|
-
if (
|
|
205
|
-
|
|
206
|
-
|
|
215
|
+
}).on('didSet', async (value) => {
|
|
216
|
+
if (value != null) {
|
|
217
|
+
try {
|
|
218
|
+
await this.createWsClient()
|
|
219
|
+
} catch (error) { this.warn(error) }
|
|
207
220
|
}
|
|
208
221
|
})
|
|
222
|
+
this.values.wsPort = null
|
|
209
223
|
|
|
210
224
|
this.log(
|
|
211
225
|
'%s %s gateway v%s', this.values.manufacturer, this.values.model,
|
|
@@ -256,12 +270,15 @@ class Gateway extends AccessoryDelegate {
|
|
|
256
270
|
*/
|
|
257
271
|
this.scheduleServicesByRid = {}
|
|
258
272
|
|
|
259
|
-
this.createClient()
|
|
260
|
-
this.createWsClient()
|
|
261
273
|
this.heartbeatEnabled = true
|
|
262
274
|
this
|
|
263
275
|
.on('identify', this.identify)
|
|
264
|
-
.once('heartbeat', (beat) => {
|
|
276
|
+
.once('heartbeat', async (beat) => {
|
|
277
|
+
this.initialBeat = beat
|
|
278
|
+
try {
|
|
279
|
+
await this.createClient()
|
|
280
|
+
} catch (error) { this.warn(error) }
|
|
281
|
+
})
|
|
265
282
|
.on('heartbeat', this.heartbeat)
|
|
266
283
|
.on('shutdown', this.shutdown)
|
|
267
284
|
}
|
|
@@ -330,9 +347,11 @@ class Gateway extends AccessoryDelegate {
|
|
|
330
347
|
*/
|
|
331
348
|
async found (host, config) {
|
|
332
349
|
try {
|
|
333
|
-
this.values.host = host
|
|
334
350
|
this.context.config = config
|
|
351
|
+
this.values.host = host
|
|
352
|
+
this.values.https = config.https
|
|
335
353
|
this.values.software = config.swversion
|
|
354
|
+
this.values.url = 'http' + (config.https ? 's' : '') + '://' + host
|
|
336
355
|
if (!this.initialised) {
|
|
337
356
|
this.debug('initialising...')
|
|
338
357
|
await this.connect()
|
|
@@ -344,7 +363,7 @@ class Gateway extends AccessoryDelegate {
|
|
|
344
363
|
|
|
345
364
|
async shutdown () {
|
|
346
365
|
this.service.values.statusActive = false
|
|
347
|
-
return this.wsClient
|
|
366
|
+
return this.wsClient?.close()
|
|
348
367
|
}
|
|
349
368
|
|
|
350
369
|
/** Called every second.
|
|
@@ -372,7 +391,9 @@ class Gateway extends AccessoryDelegate {
|
|
|
372
391
|
this.values.firmware = parseInt(config.fwversion.slice(6, 8)) + '.' +
|
|
373
392
|
parseInt(config.fwversion.slice(2, 4), 16) + '.' +
|
|
374
393
|
parseInt(config.fwversion.slice(4, 6), 16)
|
|
375
|
-
this.values.wsPort =
|
|
394
|
+
this.values.wsPort = this.values.https
|
|
395
|
+
? config.websocketport_wss ?? config.websocketport
|
|
396
|
+
: config.websocketport
|
|
376
397
|
this.service.update(config)
|
|
377
398
|
if (this.checkApiKeys) {
|
|
378
399
|
const myEntry = config.whitelist[this.values.apiKey]
|
|
@@ -390,13 +411,16 @@ class Gateway extends AccessoryDelegate {
|
|
|
390
411
|
|
|
391
412
|
/** Create {@link DeconzAccessory.Gateway#client}.
|
|
392
413
|
*/
|
|
393
|
-
createClient () {
|
|
414
|
+
async createClient () {
|
|
415
|
+
this.client?.removeAllListeners()
|
|
394
416
|
/** REST API client for the gateway.
|
|
395
417
|
* @type {DeconzClient}
|
|
396
418
|
*/
|
|
397
419
|
this.client = new ApiClient({
|
|
398
420
|
apiKey: this.values.apiKey,
|
|
399
421
|
config: this.context.config,
|
|
422
|
+
https: this.values.https,
|
|
423
|
+
fingerprint: this.values.fingerprint,
|
|
400
424
|
host: this.values.host,
|
|
401
425
|
maxSockets: this.platform.config.parallelRequests,
|
|
402
426
|
timeout: this.platform.config.timeout,
|
|
@@ -442,18 +466,29 @@ class Gateway extends AccessoryDelegate {
|
|
|
442
466
|
response.statusCode, response.statusMessage
|
|
443
467
|
)
|
|
444
468
|
})
|
|
469
|
+
return this.connect()
|
|
445
470
|
}
|
|
446
471
|
|
|
447
472
|
/** Create {@link DeconzAccessory.Gateway#wsclient}.
|
|
448
473
|
*/
|
|
449
|
-
createWsClient () {
|
|
474
|
+
async createWsClient () {
|
|
475
|
+
this.wsRestart = true
|
|
476
|
+
await this.wsClient?.close()
|
|
477
|
+
this.wsClient?.removeAllListeners()
|
|
478
|
+
const wss = this.values.https && this.context.fullState?.config.websocketport_wss != null
|
|
479
|
+
const port = wss
|
|
480
|
+
? this.context.fullState?.config.websocketport_wss
|
|
481
|
+
: this.context.fullState?.config.websocketport
|
|
482
|
+
const options = {
|
|
483
|
+
wss,
|
|
484
|
+
fingerprint: this.values.fingerprint,
|
|
485
|
+
host: this.values.host.split(':')[0] + ':' + port,
|
|
486
|
+
retryTime: 0
|
|
487
|
+
}
|
|
450
488
|
/** Client for gateway web socket notifications.
|
|
451
489
|
* @type {DeconzWsClient}
|
|
452
490
|
*/
|
|
453
|
-
this.wsClient = new WsClient(
|
|
454
|
-
host: this.values.host.split(':')[0] + ':' + this.values.wsPort,
|
|
455
|
-
retryTime: 15
|
|
456
|
-
})
|
|
491
|
+
this.wsClient = new WsClient(options)
|
|
457
492
|
this.wsClient
|
|
458
493
|
.on('error', (error) => {
|
|
459
494
|
this.warn('websocket communication error: %s', error)
|
|
@@ -490,14 +525,13 @@ class Gateway extends AccessoryDelegate {
|
|
|
490
525
|
this.pollFullState = true
|
|
491
526
|
})
|
|
492
527
|
.on('closed', (url, retryTime) => {
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
)
|
|
497
|
-
} else {
|
|
498
|
-
this.log('websocket connection to %s closed', url)
|
|
528
|
+
this.log('websocket connection to %s closed', url)
|
|
529
|
+
if (!this.wsRestart) {
|
|
530
|
+
this.values.wsPort = null
|
|
499
531
|
}
|
|
500
532
|
})
|
|
533
|
+
this.wsClient.listen()
|
|
534
|
+
this.wsRestart = false
|
|
501
535
|
}
|
|
502
536
|
|
|
503
537
|
/** Connect to the gateway.
|
|
@@ -515,8 +549,8 @@ class Gateway extends AccessoryDelegate {
|
|
|
515
549
|
if (this.values.apiKey == null) {
|
|
516
550
|
this.values.apiKey =
|
|
517
551
|
await this.client.getApiKey('homebridge-deconz')
|
|
552
|
+
this.values.fingerprint = this.client.fingerprint
|
|
518
553
|
}
|
|
519
|
-
this.wsClient.listen()
|
|
520
554
|
this.service.values.restart = false
|
|
521
555
|
this.service.values.statusActive = true
|
|
522
556
|
this.checkApiKeys = true
|
|
@@ -989,6 +1023,9 @@ class Gateway extends AccessoryDelegate {
|
|
|
989
1023
|
this.vdebug('%spolling...', this.pollNext ? 'priority ' : '')
|
|
990
1024
|
if (this.context.fullState == null || this.pollFullState) {
|
|
991
1025
|
const fullState = await this.client.get('/')
|
|
1026
|
+
if (this.values.https && (this.values.fingerprint == null || this.values.fingerprint === '')) {
|
|
1027
|
+
this.values.fingerprint = this.client.fingerprint
|
|
1028
|
+
}
|
|
992
1029
|
try {
|
|
993
1030
|
fullState.groups[0] = await this.client.get('/groups/0')
|
|
994
1031
|
} catch (error) {}
|
|
@@ -1025,6 +1062,9 @@ class Gateway extends AccessoryDelegate {
|
|
|
1025
1062
|
this.warn('deCONZ not ready')
|
|
1026
1063
|
return
|
|
1027
1064
|
}
|
|
1065
|
+
if (this.values.https && (this.values.fingerprint == null || this.values.fingerprint === '')) {
|
|
1066
|
+
this.values.fingerprint = this.client.fingerprint
|
|
1067
|
+
}
|
|
1028
1068
|
this.context.fullState.config = config
|
|
1029
1069
|
this.context.fullState.lights = await this.client.get('/lights')
|
|
1030
1070
|
this.context.fullState.sensors = await this.client.get('/sensors')
|
|
@@ -158,11 +158,25 @@ class DeconzAccessory extends AccessoryDelegate {
|
|
|
158
158
|
} else if (params.serviceName === 'Battery') {
|
|
159
159
|
service = this.servicesByServiceName.Battery?.[0]
|
|
160
160
|
} else if (params.serviceName === 'Consumption') {
|
|
161
|
+
if (this.servicesByServiceName.Consumption?.[0] != null) {
|
|
162
|
+
this.warn(
|
|
163
|
+
'%s: ignoring additional ZHAConsumption resource - consider splitting the accessory',
|
|
164
|
+
resource.rpath
|
|
165
|
+
)
|
|
166
|
+
return
|
|
167
|
+
}
|
|
161
168
|
service = this.servicesByServiceName.Power?.[0]
|
|
162
169
|
if (service != null) {
|
|
163
170
|
service.addResource(resource)
|
|
164
171
|
}
|
|
165
172
|
} else if (params.serviceName === 'Power') {
|
|
173
|
+
if (this.servicesByServiceName.Power?.[0] != null) {
|
|
174
|
+
this.warn(
|
|
175
|
+
'%s: ignoring additional ZHAPower resource - consider splitting the accessory',
|
|
176
|
+
resource.rpath
|
|
177
|
+
)
|
|
178
|
+
return
|
|
179
|
+
}
|
|
166
180
|
service = this.servicesByServiceName.Consumption?.[0]
|
|
167
181
|
if (service != null) {
|
|
168
182
|
service.addResource(resource)
|
package/lib/DeconzPlatform.js
CHANGED
|
@@ -29,7 +29,6 @@ class DeconzPlatform extends Platform {
|
|
|
29
29
|
|
|
30
30
|
parseConfigJson (configJson) {
|
|
31
31
|
this.config = {
|
|
32
|
-
forceHttp: false,
|
|
33
32
|
hosts: [],
|
|
34
33
|
noResponse: false,
|
|
35
34
|
parallelRequests: 10,
|
|
@@ -48,7 +47,6 @@ class DeconzPlatform extends Platform {
|
|
|
48
47
|
})
|
|
49
48
|
.stringKey('name')
|
|
50
49
|
.stringKey('platform')
|
|
51
|
-
.boolKey('forceHttp')
|
|
52
50
|
.stringKey('host')
|
|
53
51
|
.arrayKey('hosts')
|
|
54
52
|
.boolKey('noResponse')
|
|
@@ -69,7 +67,6 @@ class DeconzPlatform extends Platform {
|
|
|
69
67
|
this.config.hosts.push(this.config.host)
|
|
70
68
|
}
|
|
71
69
|
this.discovery = new Discovery({
|
|
72
|
-
forceHttp: this.config.forceHttp,
|
|
73
70
|
timeout: this.config.timeout
|
|
74
71
|
})
|
|
75
72
|
this.discovery
|
|
@@ -116,7 +113,9 @@ class DeconzPlatform extends Platform {
|
|
|
116
113
|
this.gatewayMap[id] = new DeconzAccessory.Gateway(this, { config, host })
|
|
117
114
|
}
|
|
118
115
|
await this.gatewayMap[id].found(host, config)
|
|
119
|
-
|
|
116
|
+
if (!this.gatewayMap[id].initialised) {
|
|
117
|
+
await once(this.gatewayMap[id], 'initialised')
|
|
118
|
+
}
|
|
120
119
|
this.emit('found')
|
|
121
120
|
}
|
|
122
121
|
|
|
@@ -224,7 +223,9 @@ class DeconzPlatform extends Platform {
|
|
|
224
223
|
const configs = await this.discovery.discover()
|
|
225
224
|
const jobs = []
|
|
226
225
|
for (const host in configs) {
|
|
227
|
-
|
|
226
|
+
if (this.config.hosts.length === 0 || this.gatewayMap[configs[host].bridgeid] != null) {
|
|
227
|
+
jobs.push(this.foundGateway(host, configs[host]))
|
|
228
|
+
}
|
|
228
229
|
}
|
|
229
230
|
for (const job of jobs) {
|
|
230
231
|
try {
|
|
@@ -19,7 +19,7 @@ class Contact extends DeconzService.SensorsResource {
|
|
|
19
19
|
Characteristic: this.Characteristics.hap.ContactSensorState
|
|
20
20
|
})
|
|
21
21
|
|
|
22
|
-
// With _Status
|
|
22
|
+
// With _Status Tampered_ Eve thinks the _Door Sensor_ is an Eve Windows Guard
|
|
23
23
|
// (instead of an Eve Door & Window) and won't display it.
|
|
24
24
|
this.addCharacteristicDelegates({ noTampered: true })
|
|
25
25
|
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// homebridge-deconz/lib/DeconzService/DoorLock.js
|
|
2
|
+
// Copyright © 2022-2026 Erik Baauw. All rights reserved.
|
|
3
|
+
//
|
|
4
|
+
// Homebridge plugin for deCONZ.
|
|
5
|
+
|
|
6
|
+
import { DeconzService } from '../DeconzService/index.js'
|
|
7
|
+
import '../DeconzService/SensorsResource.js'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @memberof DeconzService
|
|
11
|
+
*/
|
|
12
|
+
class DoorLock extends DeconzService.SensorsResource {
|
|
13
|
+
constructor (accessory, resource, params = {}) {
|
|
14
|
+
params.Service = accessory.Services.hap.LockMechanism
|
|
15
|
+
super(accessory, resource, params)
|
|
16
|
+
|
|
17
|
+
this.addCharacteristicDelegate({
|
|
18
|
+
key: 'currentState',
|
|
19
|
+
Characteristic: this.Characteristics.hap.LockCurrentState
|
|
20
|
+
})
|
|
21
|
+
this.addCharacteristicDelegate({
|
|
22
|
+
key: 'targetState',
|
|
23
|
+
Characteristic: this.Characteristics.hap.LockTargetState
|
|
24
|
+
}).on('didSet', async (value, fromHomeKit) => {
|
|
25
|
+
if (fromHomeKit) {
|
|
26
|
+
await this.put('/config', {
|
|
27
|
+
lock: value === this.Characteristics.hap.LockTargetState.SECURED
|
|
28
|
+
})
|
|
29
|
+
this.locking = new Date()
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
this.addCharacteristicDelegates()
|
|
33
|
+
|
|
34
|
+
this.update(resource.body, resource.rpath)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
updateState (state) {
|
|
38
|
+
if (state.lockstate != null) {
|
|
39
|
+
this.values.currentState = state.lockestate === 'locked'
|
|
40
|
+
? this.Characteristics.hap.LockCurrentState.SECURED
|
|
41
|
+
: this.Characteristics.hap.LockCurrentState.UNSECURED
|
|
42
|
+
}
|
|
43
|
+
super.updateState(state)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
updateConfig (config) {
|
|
47
|
+
if (config.lock != null && new Date() - this.locking >= 5000) {
|
|
48
|
+
this.values.targetLockState = config.lock
|
|
49
|
+
? this.Characteristics.hap.LockTargetState.SECURED
|
|
50
|
+
: this.Characteristics.hap.LockTargetState.UNSECURED
|
|
51
|
+
}
|
|
52
|
+
super.updateConfig(config)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
DeconzService.DoorLock = DoorLock
|
package/package.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"ebaauw"
|
|
8
8
|
],
|
|
9
9
|
"license": "Apache-2.0",
|
|
10
|
-
"version": "1.
|
|
10
|
+
"version": "1.3.0",
|
|
11
11
|
"keywords": [
|
|
12
12
|
"homebridge-plugin",
|
|
13
13
|
"homekit",
|
|
@@ -26,13 +26,13 @@
|
|
|
26
26
|
"ui": "cli/ui.js"
|
|
27
27
|
},
|
|
28
28
|
"engines": {
|
|
29
|
-
"deCONZ": "2.
|
|
30
|
-
"homebridge": "^1.11.
|
|
29
|
+
"deCONZ": "2.32.5",
|
|
30
|
+
"homebridge": "^1.11.2||^2.0.0-beta",
|
|
31
31
|
"node": "^24||^22||^20"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"hb-deconz-tools": "~
|
|
35
|
-
"homebridge-lib": "~7.3.
|
|
34
|
+
"hb-deconz-tools": "~3.0.0",
|
|
35
|
+
"homebridge-lib": "~7.3.2"
|
|
36
36
|
},
|
|
37
37
|
"scripts": {
|
|
38
38
|
"prepare": "standard && rm -rf out && jsdoc -c jsdoc.json",
|