homebridge-deconz 0.1.17 → 0.1.18

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.
@@ -1,442 +0,0 @@
1
- // homebridge-deconz/lib/Deconz/ApiClient.js
2
- //
3
- // Homebridge plug-in for deCONZ.
4
- // Copyright © 2018-2023 Erik Baauw. All rights reserved.
5
-
6
- 'use strict'
7
-
8
- const events = require('events')
9
- const { HttpClient, OptionParser, timeout } = require('homebridge-lib')
10
- const os = require('os')
11
-
12
- const Deconz = require('../Deconz')
13
-
14
- // Estmate the number of Zigbee messages resulting from PUT body.
15
- function numberOfZigbeeMessages (body = {}) {
16
- let n = 0
17
- if (Object.keys(body).includes('on')) {
18
- n++
19
- }
20
- if (
21
- Object.keys(body).includes('bri') ||
22
- Object.keys(body).includes('bri_inc')
23
- ) {
24
- n++
25
- }
26
- if (
27
- Object.keys(body).includes('xy') ||
28
- Object.keys(body).includes('ct') ||
29
- Object.keys(body).includes('hue') ||
30
- Object.keys(body).includes('sat') ||
31
- Object.keys(body).includes('effect')
32
- ) {
33
- n++
34
- }
35
- return n === 0 ? 1 : n
36
- }
37
-
38
- /** REST API client for a deCONZ gateway.
39
- *
40
- * See the [deCONZ API](https://dresden-elektronik.github.io/deconz-rest-doc/)
41
- * documentation for a better understanding of the API.
42
- * @extends HttpClient
43
- * @memberof Deconz
44
- */
45
- class ApiClient extends HttpClient {
46
- /** Events reported through `buttonevent`.
47
- * @type {Object<string, integer>}
48
- */
49
- static get buttonEvent () {
50
- return {
51
- PRESS: 0,
52
- HOLD: 1,
53
- SHORT_RELEASE: 2,
54
- LONG_RELEASE: 3,
55
- DOUBLE_PRESS: 4,
56
- TRIPLE_PRESS: 5,
57
- QUADRUPLE_PRESS: 6,
58
- SHAKE: 7,
59
- DROP: 8,
60
- TILT: 9
61
- }
62
- }
63
-
64
- /** Convert date as reported by deCONZ to human readable string.
65
- * @param {string} date - The ISO-8601 date string.
66
- * @param {boolean} [utc=true] - Treat date as UTC, even with missing `Z`.
67
- * @param {string} date - The human readable date string.
68
- */
69
- static dateToString (date, utc = true) {
70
- if (date == null || date === 'none') {
71
- return 'n/a'
72
- }
73
- if (utc && !date.endsWith('Z')) {
74
- date += 'Z'
75
- }
76
- return String(new Date(date)).slice(0, 24)
77
- }
78
-
79
- /** Convert `lightlevel` to lux.
80
- * @param {integer} lightLevel - The `lightlevel` as reported by deCONZ.
81
- * @return {integer} lux - The value in lux.
82
- */
83
- static lightLevelToLux (v) {
84
- v = Math.max(0, Math.min(v, 60001))
85
- return v ? Math.round(Math.pow(10, (v - 1) / 10000) * 10) / 10 : 0.0001
86
- }
87
-
88
- /** Create a new instance of a Deconz.ApiClient.
89
- *
90
- * The caller is expected to verify that the given host is a reachable
91
- * deCONZ gateway, by calling
92
- * {@link Deconz.Discovery#config Deconz.Discovery#config()} and passing the
93
- * response as `params.config`.<br>
94
- * The caller is expected to persist the API key, passing it as
95
- * `params.apiKey`.
96
- * If no API key is known {@link Deconz.ApiClient#getApiKey getApiKey()} can
97
- * be called to create one.<br>
98
- *
99
- * @param {object} params - Parameters.
100
- * @param {?string} params.config - The bridge/gateway public configuration,
101
- * i.e. the response of {@link Deconz.Discovery#config config()}.
102
- * @param {!string} params.host - Hostname/IP address and port of the
103
- * deCONZ gateway.
104
- * @param {boolean} [params.keepAlive=false] - Keep server connection(s)
105
- * open.
106
- * @param {integer} [params.maxSockets=20] - Throttle requests to maximum
107
- * number of parallel connections.
108
- * @param {boolean} [params.phoscon=false] - Mimic Phoscon web app to use
109
- * deCONZ gateway API extensions.
110
- * @param {integer} [params.timeout=5] - Request timeout (in seconds).
111
- * @param {?string} params.apiKey - The API key of the deCONZ gateway.
112
- * @param {integer} [params.waitTimePut=50] - The time (in milliseconds),
113
- * after sending a PUT request, to wait before sending another PUT request.
114
- * @param {integer} [params.waitTimePutGroup=1000] - The time (in
115
- * milliseconds), after sending a PUT request, to wait before sending
116
- * another PUT request.
117
- * @param {integer} [params.waitTimeResend=300] - The time, in milliseconds,
118
- * to wait before resending a request after an ECONNRESET, an http status
119
- * 503, or an api 901 error.
120
- */
121
- constructor (params = {}) {
122
- const _options = {
123
- keepAlive: false,
124
- maxSockets: 20,
125
- timeout: 5,
126
- waitTimePut: 50,
127
- waitTimePutGroup: 1000,
128
- waitTimeResend: 300
129
- }
130
- const optionParser = new OptionParser(_options)
131
- optionParser
132
- .objectKey('config', true)
133
- .stringKey('host', true)
134
- .boolKey('keepAlive')
135
- .intKey('maxSockets', 1, 20)
136
- .boolKey('phoscon')
137
- .intKey('timeout', 1, 60)
138
- .stringKey('apiKey')
139
- .intKey('waitTimePut', 0, 50)
140
- .intKey('waitTimePutGroup', 0, 1000)
141
- .intKey('waitTimeResend', 0, 1000)
142
- .parse(params)
143
-
144
- const options = {
145
- host: _options.host,
146
- json: true,
147
- keepAlive: _options.keepAlive,
148
- maxSockets: _options.maxSockets,
149
- path: '/api',
150
- timeout: _options.timeout,
151
- validStatusCodes: [200, 400, 403] //, 404]
152
- }
153
- if (_options.phoscon) {
154
- // options.headers = { Accept: 'application/vnd.ddel.v1' }
155
- options.headers = { Accept: 'application/vnd.ddel.v3,application/vnd.ddel.v2,application/vnd.ddel.v1.1' }
156
- }
157
- if (_options.apiKey) {
158
- options.path += '/' + _options.apiKey
159
- }
160
- super(options)
161
- this._options = _options
162
- this.waitForIt = false
163
- this.setMaxListeners(30)
164
- }
165
-
166
- /** The ID (Zigbee mac address) of the deCONZ gateway.
167
- * @type {string}
168
- * @readonly
169
- */
170
- get bridgeid () { return this._options.config.bridgeid }
171
-
172
- /** Server (base) path, `/api/`_API_key_.
173
- * @type {string}
174
- * @readonly
175
- */
176
- get path () { return super.path }
177
-
178
- /** The API key.
179
- * @type {string}
180
- */
181
- get apiKey () { return this._options.apiKey }
182
- set apiKey (value) {
183
- this._options.apiKey = value
184
- let path = '/api'
185
- if (value != null) {
186
- path += '/' + value
187
- }
188
- super.path = path
189
- }
190
-
191
- // ===========================================================================
192
-
193
- /** Issue a GET request of `/api/`_API_key_`/`_resource_.
194
- *
195
- * @param {string} resource - The resource.<br>
196
- * This might be a resource as exposed by the API, e.g. `/lights/1/state`,
197
- * or an attribute returned by the API, e.g. `/lights/1/state/on`.
198
- * @return {*} response - The JSON response body converted to JavaScript.
199
- * @throws {Deconz.ApiError} In case of error.
200
- */
201
- async get (resource) {
202
- if (typeof resource !== 'string' || resource[0] !== '/') {
203
- throw new TypeError(`${resource}: invalid resource`)
204
- }
205
- let path = resource.slice(1).split('/')
206
- switch (path[0]) {
207
- case 'lights':
208
- if (path.length === 3 && path[2] === 'connectivity2') {
209
- path = []
210
- break
211
- }
212
- // falls through
213
- case 'groups':
214
- if (path.length >= 3 && path[2] === 'scenes') {
215
- resource = '/' + path.shift() + '/' + path.shift() + '/' + path.shift()
216
- if (path.length >= 1) {
217
- resource += '/' + path.shift()
218
- }
219
- break
220
- }
221
- // falls through
222
- case 'schedules':
223
- case 'sensors':
224
- case 'rules':
225
- case 'resourcelinks':
226
- if (path.length > 2) {
227
- resource = '/' + path.shift() + '/' + path.shift()
228
- break
229
- }
230
- path = []
231
- break
232
- case 'config':
233
- case 'capabilities':
234
- if (path.length > 1) {
235
- resource = '/' + path.shift()
236
- break
237
- }
238
- // falls through
239
- default:
240
- path = []
241
- break
242
- }
243
- let { body } = await this.request('GET', resource)
244
- for (const key of path) {
245
- if (typeof body === 'object' && body != null) {
246
- body = body[key]
247
- }
248
- }
249
- if (body == null && path.length > 0) {
250
- throw new Error(
251
- `/${path.join('/')}: not found in resource ${resource}`
252
- )
253
- }
254
- return body
255
- }
256
-
257
- /** Issue a PUT request to `/api/`_API_key_`/`_resource_.
258
- *
259
- * ApiClient throttles the number of PUT requests to limit the Zigbee traffic
260
- * to 20 unicast messsages per seconds, or 1 broadcast message per second,
261
- * delaying the request when needed.
262
- * @param {string} resource - The resource.
263
- * @param {*} body - The body, which will be converted to JSON.
264
- * @return {Deconz.ApiResponse} response - The response.
265
- * @throws {Deconz.ApiError} In case of error, except for non-critical API errors.
266
- */
267
- async put (resource, body) {
268
- if (this.waitForIt) {
269
- while (this.waitForIt) {
270
- try {
271
- await events.once(this, '_go')
272
- } catch (error) {}
273
- }
274
- }
275
- const timeout = numberOfZigbeeMessages(body) * (
276
- resource.startsWith('/groups')
277
- ? this._options.waitTimePutGroup
278
- : this._options.waitTimePut
279
- )
280
- if (timeout > 0) {
281
- this.waitForIt = true
282
- setTimeout(() => {
283
- this.waitForIt = false
284
- this.emit('_go')
285
- }, timeout)
286
- }
287
- return this.request('PUT', resource, body)
288
- }
289
-
290
- /** Issue a POST request to `/api/`_API_key_`/`_resource_.
291
- *
292
- * @param {string} resource - The resource.
293
- * @param {*} body - The body, which will be converted to JSON.
294
- * @return {Deconz.ApiResponse} response - The response.
295
- * @throws {Deconz.ApiError} In case of error.
296
- */
297
- async post (resource, body) {
298
- return this.request('POST', resource, body)
299
- }
300
-
301
- /** Issue a DELETE request of `/api/`_API_key_`/`_resource_.
302
- * @param {string} resource - The resource.
303
- * @param {*} body - The body, which will be converted to JSON.
304
- * @return {Deconz.ApiResponse} response - The response.
305
- * @throws {Deconz.ApiError} In case of error.
306
- */
307
- async delete (resource, body) {
308
- return this.request('DELETE', resource, body)
309
- }
310
-
311
- // ===========================================================================
312
-
313
- /** Obtain an API key and set {@link Deconz.ApiClient#apiKey apiKey}.
314
- *
315
- * Calls {@link Deconz.ApiClient#post post()} to issue a POST request to `/api`.
316
- *
317
- * Before calling `getApiKey`, the deCONZ gateway must be unlocked, unless
318
- * the client is running on the same host as the gateway.
319
- * @return {string} apiKey - The newly created API key.
320
- * @throws {HttpError} In case of HTTP error.
321
- * @throws {Deconz.ApiError} In case of API error.
322
- */
323
- async getApiKey (application) {
324
- if (typeof application !== 'string' || application === '') {
325
- throw new TypeError(`${application}: invalid application name`)
326
- }
327
- const apiKey = this._options.apiKey
328
- const body = { devicetype: `${application}#${os.hostname().split('.')[0]}` }
329
- this.apiKey = null
330
- try {
331
- const response = await this.post('/', body)
332
- this.apiKey = response.success.username
333
- return this.apiKey
334
- } catch (error) {
335
- this.apiKey = apiKey
336
- throw (error)
337
- }
338
- }
339
-
340
- /** Delete the API key and clear {@link Deconz.ApiClient#apiKey apiKey}.
341
- * @throws {HttpError} In case of HTTP error.
342
- * @throws {Deconz.ApiError} In case of API error.
343
- */
344
- async deleteApiKey () {
345
- try {
346
- await this.delete('/config/whitelist/' + this.apiKey)
347
- } catch (error) {}
348
- this.apiKey = null
349
- }
350
-
351
- /** Unlock the gateway to allow creating a new API key.
352
- *
353
- * Calls {@link Deconz.ApiClient#put put()} to issue a PUT request to
354
- * `/api/`_API_key_`/config`.
355
- *
356
- * @return {Deconz.ApiResponse} response - The response.
357
- * @throws {HttpError} In case of HTTP error.
358
- * @throws {Deconz.ApiError} In case of API error.
359
- */
360
- async unlock () {
361
- return this.put('/config', { unlock: 60 })
362
- }
363
-
364
- /** Search for new devices.
365
- *
366
- * Calls {@link Deconz.ApiClient#put put()} to issue a PUT request to
367
- * `/api/`_API_key_`/config`, to enable pairing of new Zigbee devices.
368
- *
369
- * To see the newly paired devices, issue a GET request of
370
- * `/api/`_API_key_`/lights/new` and/or `/api/`_API_key_`/sensor/new`
371
- * @return {Deconz.ApiResponse} response - The response.
372
- * @throws {HttpError} In case of HTTP error.
373
- * @throws {Deconz.ApiError} In case of API error.
374
- */
375
- async search () {
376
- return this.put('/config', { permitjoin: 120 })
377
- }
378
-
379
- /** Restart the gateway.
380
- *
381
- * Calls {@link Deconz.ApiClient#post post()} to issue a POST request to
382
- * `/api/`_API_key_`/config/restartapp`, to restart the deCONZ gateway.
383
- *
384
- * @return {Deconz.ApiResponse} response - The response.
385
- * @throws {HttpError} In case of HTTP error.
386
- * @throws {Deconz.ApiError} In case of API error.
387
- */
388
- async restart () {
389
- return this.post('/config/restartapp')
390
- }
391
-
392
- // ===========================================================================
393
-
394
- /** Issue an HTTP request to the deCONZ gateway.
395
- *
396
- * This method does the heavy lifting for {@link Deconz.AplClient#get get()},
397
- * {@link Deconz.ApiClient#put put()}, {@link Deconz.ApiClient#post post()},
398
- * and {@link Deconz.ApiClient#delete delete()}.
399
- * It shouldn't be called directly.
400
- *
401
- * @param {string} method - The method for the request.
402
- * @param {!string} resource - The resource for the request.
403
- * @param {?*} body - The body for the request.
404
- * @return {Deconz.ApiResponse} response - The response.
405
- * @throws {HttpError} In case of HTTP error.
406
- * @throws {Deconz.ApiError} In case of API error.
407
- */
408
- async request (method, resource, body = null, retry = 0) {
409
- try {
410
- const httpResponse = await super.request(method, resource, body)
411
- const response = new Deconz.ApiResponse(httpResponse)
412
- for (const error of response.errors) {
413
- /** Emitted for each API error returned by the or deCONZ gateway.
414
- *
415
- * @event Deconz.ApiClient#error
416
- * @param {HttpClient.HttpError|Deconz.ApiError} error - The error.
417
- */
418
- this.emit('error', error)
419
- if (!error.nonCritical) {
420
- throw error
421
- }
422
- }
423
- return response
424
- } catch (error) {
425
- if (
426
- error.code === 'ECONNRESET' ||
427
- error.statusCode === 503 ||
428
- error.type === 901
429
- ) {
430
- if (error.request != null && this._options.waitTimeResend > 0 && retry < 5) {
431
- error.message += ' - retry in ' + this._options.waitTimeResend + 'ms'
432
- this.emit('error', error)
433
- await timeout(this._options.waitTimeResend)
434
- return this.request(method, resource, body, retry + 1)
435
- }
436
- }
437
- throw error
438
- }
439
- }
440
- }
441
-
442
- module.exports = ApiClient
@@ -1,49 +0,0 @@
1
- // homebridge-deconz/lib/Deconz/ApiError.js
2
- //
3
- // Homebridge plug-in for deCONZ.
4
- // Copyright © 2018-2023 Erik Baauw. All rights reserved.
5
-
6
- 'use strict'
7
-
8
- const { HttpClient } = require('homebridge-lib')
9
-
10
- // API errors that could still cause (part of) the PUT command to be executed.
11
- const nonCriticalApiErrorTypes = [
12
- 6, // parameter not available
13
- 7, // invalid value for parameter
14
- 8, // paramater not modifiable
15
- 201 // paramater not modifiable, device is set to off
16
- ]
17
-
18
- /** Deconz API error.
19
- * @hideconstructor
20
- * @extends HttpClient.HttpError
21
- * @memberof Deconz
22
- */
23
- class ApiError extends HttpClient.HttpError {
24
- constructor (e, response) {
25
- super(
26
- `${e.address}: api error ${e.type}: ${e.description}`,
27
- response.request, response.statusCode, response.statusMessage
28
- )
29
-
30
- /** @member {integer} - The API error type.
31
- */
32
- this.type = e.type
33
-
34
- /** @member {string} - The address causing the error.
35
- */
36
- this.address = e.address
37
-
38
- /** @member {string} - The API error description.
39
- */
40
- this.description = e.description
41
-
42
- /** @member {boolean} - Indication that the request might still succeed
43
- * for other attributes.
44
- */
45
- this.nonCritical = nonCriticalApiErrorTypes.includes(e.type)
46
- }
47
- }
48
-
49
- module.exports = ApiError
@@ -1,57 +0,0 @@
1
- // homebridge-deconz/lib/Deconz/ApiResponse.js
2
- //
3
- // Homebridge plug-in for deCONZ.
4
- // Copyright © 2018-2023 Erik Baauw. All rights reserved.
5
-
6
- 'use strict'
7
-
8
- const { HttpClient } = require('homebridge-lib')
9
-
10
- const Deconz = require('../Deconz')
11
-
12
- /** Deconz API response.
13
- * @hideconstructor
14
- * @extends HttpClient.HttpResponse
15
- * @memberof Deconz
16
- */
17
- class ApiResponse extends HttpClient.HttpResponse {
18
- constructor (response) {
19
- super(
20
- response.request, response.statusCode, response.statusMessage,
21
- response.headers, response.body, response.parsedBody
22
- )
23
-
24
- /** @member {object} - An object with the `"success"` API responses.
25
- */
26
- this.success = {}
27
-
28
- /** @member {Deconz.ApiError[]} - A list of `"error"` API responses.
29
- */
30
- this.errors = []
31
-
32
- if (Array.isArray(response.body)) {
33
- for (const id in response.body) {
34
- const e = response.body[id].error
35
- if (e != null && typeof e === 'object') {
36
- this.errors.push(new Deconz.ApiError(e, response))
37
- }
38
- const s = response.body[id].success
39
- if (s != null && typeof s === 'object') {
40
- for (const path of Object.keys(s)) {
41
- const keys = path.split('/')
42
- let obj = this.success
43
- for (let i = 1; i < keys.length - 1; i++) {
44
- if (obj[keys[i]] == null) {
45
- obj[keys[i]] = {}
46
- }
47
- obj = obj[keys[i]]
48
- }
49
- obj[keys[keys.length - 1]] = s[path]
50
- }
51
- }
52
- }
53
- }
54
- }
55
- }
56
-
57
- module.exports = ApiResponse