homebridge-lib 5.2.3-0 → 5.3.1

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/index.js CHANGED
@@ -196,6 +196,13 @@ class homebridgeLib {
196
196
  */
197
197
  static get SystemInfo () { return require('./lib/SystemInfo') }
198
198
 
199
+ /** Server for dynamic configuration settings through Homebridge UI.
200
+ * <br>See {@link UiServer}.
201
+ * @type {Class}
202
+ * @memberof module:homebridgeLib
203
+ */
204
+ static get UiServer () { return require('./lib/UiServer') }
205
+
199
206
  /** Universal Plug and Play client.
200
207
  * <br>See {@link UpnpClient}.
201
208
  * @type {Class}
@@ -150,8 +150,9 @@ class CommandLineTool {
150
150
  return !!this._options.debug
151
151
  }
152
152
 
153
- /* Usage string.
154
- */
153
+ /** Usage string.
154
+ * @type {string}
155
+ */
155
156
  get usage () { return this._usage }
156
157
  set usage (usage) {
157
158
  this._usage = usage
@@ -167,6 +168,9 @@ class CommandLineTool {
167
168
  // ===== Logging =============================================================
168
169
 
169
170
  /** Print debug message to stderr.
171
+ * @param {string|Error} format - The printf-style message or an instance of
172
+ * [Error](https://nodejs.org/dist/latest-v14.x/docs/api/errors.html#errors_class_error).
173
+ * @param {...string} args - Arguments to the printf-style message.
170
174
  */
171
175
  debug (format, ...args) {
172
176
  if (this._options.debug) {
@@ -175,12 +179,18 @@ class CommandLineTool {
175
179
  }
176
180
 
177
181
  /** Print error message to stderr.
182
+ * @param {string|Error} format - The printf-style message or an instance of
183
+ * [Error](https://nodejs.org/dist/latest-v14.x/docs/api/errors.html#errors_class_error).
184
+ * @param {...string} args - Arguments to the printf-style message.
178
185
  */
179
186
  error (format, ...args) {
180
187
  this._log({ label: 'error', chalk: chalk.bold.red }, format, ...args)
181
188
  }
182
189
 
183
190
  /** Print error message to stderr and abort program.
191
+ * @param {string|Error} format - The printf-style message or an instance of
192
+ * [Error](https://nodejs.org/dist/latest-v14.x/docs/api/errors.html#errors_class_error).
193
+ * @param {...string} args - Arguments to the printf-style message.
184
194
  */
185
195
  async fatal (format, ...args) {
186
196
  this._log({ label: 'fatal', chalk: chalk.bold.red }, format, ...args)
@@ -195,24 +205,36 @@ class CommandLineTool {
195
205
  }
196
206
 
197
207
  /** Print log message to stderr.
208
+ * @param {string|Error} format - The printf-style message or an instance of
209
+ * [Error](https://nodejs.org/dist/latest-v14.x/docs/api/errors.html#errors_class_error).
210
+ * @param {...string} args - Arguments to the printf-style message.
198
211
  */
199
212
  log (format, ...args) {
200
213
  this._log({}, format, ...args)
201
214
  }
202
215
 
203
216
  /** Print log message continuation to stderr.
217
+ * @param {string|Error} format - The printf-style message or an instance of
218
+ * [Error](https://nodejs.org/dist/latest-v14.x/docs/api/errors.html#errors_class_error).
219
+ * @param {...string} args - Arguments to the printf-style message.
204
220
  */
205
221
  logc (format, ...args) {
206
222
  this._log({ noLabel: true }, format, ...args)
207
223
  }
208
224
 
209
225
  /** Print message to stdout.
226
+ * @param {string|Error} format - The printf-style message or an instance of
227
+ * [Error](https://nodejs.org/dist/latest-v14.x/docs/api/errors.html#errors_class_error).
228
+ * @param {...string} args - Arguments to the printf-style message.
210
229
  */
211
230
  print (format, ...args) {
212
231
  this._log({ noLabel: true, stdout: true }, format, ...args)
213
232
  }
214
233
 
215
234
  /** Print verbose debug message to stderr.
235
+ * @param {string|Error} format - The printf-style message or an instance of
236
+ * [Error](https://nodejs.org/dist/latest-v14.x/docs/api/errors.html#errors_class_error).
237
+ * @param {...string} args - Arguments to the printf-style message.
216
238
  */
217
239
  vdebug (format, ...args) {
218
240
  if (this._options.vdebug) {
@@ -221,6 +243,9 @@ class CommandLineTool {
221
243
  }
222
244
 
223
245
  /** Print very verbose debug message to stderr.
246
+ * @param {string|Error} format - The printf-style message or an instance of
247
+ * [Error](https://nodejs.org/dist/latest-v14.x/docs/api/errors.html#errors_class_error).
248
+ * @param {...string} args - Arguments to the printf-style message.
224
249
  */
225
250
  vvdebug (format, ...args) {
226
251
  if (this._options.vvdebug) {
@@ -229,6 +254,9 @@ class CommandLineTool {
229
254
  }
230
255
 
231
256
  /** Print warning message to stderr.
257
+ * @param {string|Error} format - The printf-style message or an instance of
258
+ * [Error](https://nodejs.org/dist/latest-v14.x/docs/api/errors.html#errors_class_error).
259
+ * @param {...string} args - Arguments to the printf-style message.
232
260
  */
233
261
  warn (format, ...args) {
234
262
  this._log({ label: 'warning', chalk: chalk.yellow }, format, ...args)
package/lib/Platform.js CHANGED
@@ -9,8 +9,10 @@ const homebridgeLib = require('../index')
9
9
 
10
10
  const events = require('events')
11
11
  const fs = require('fs')
12
+ const http = require('http')
12
13
  const semver = require('semver')
13
14
  const util = require('util')
15
+
14
16
  const libPackageJson = require('../package.json')
15
17
 
16
18
  const uuid = /^[0-9A-F]{8}-[0-9A-F]{4}-[1-5][0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/
@@ -37,7 +39,8 @@ const context = {
37
39
  * the plugin through Homebridge;
38
40
  * - Persist HomeKit accessories across Homebridge restarts;
39
41
  * - Support for device polling by providing a heartbeat;
40
- * - Support for UPnP device discovery.
42
+ * - Support for UPnP device discovery;
43
+ * - Support dynamic configuration through the Homebridge UI.
41
44
  * @abstract
42
45
  * @extends Delegate
43
46
  */
@@ -172,11 +175,7 @@ class Platform extends homebridgeLib.Delegate {
172
175
  this.log('os: %s', this.systemInfo.osInfo.prettyName)
173
176
  this._heartbeatStart = new Date()
174
177
  setTimeout(() => { this._beat(-1) }, 1000)
175
- this.on('exit', () => {
176
- this.debug('flush cachedAccessories')
177
- this._homebridge.updatePlatformAccessories()
178
- this.log('goodbye')
179
- })
178
+ this.on('exit', () => { this._flushCachedAccessories() })
180
179
 
181
180
  const n = Object.keys(this._accessories).length
182
181
  if (n > 0) {
@@ -188,7 +187,13 @@ class Platform extends homebridgeLib.Delegate {
188
187
  if (this.listenerCount('upnpDeviceFound') > 0) {
189
188
  this._upnpMonitor.search()
190
189
  }
190
+ if (typeof this.onUiRequest === 'function') {
191
+ try {
192
+ await this._createUiServer()
193
+ } catch (error) { this.error(error) }
194
+ }
191
195
  await events.once(this, 'initialised')
196
+ this._flushCachedAccessories()
192
197
  for (const id in this._accessories) {
193
198
  if (this._accessoryDelegates[id] == null) {
194
199
  const accessory = this._accessories[id]
@@ -205,6 +210,12 @@ class Platform extends homebridgeLib.Delegate {
205
210
  }
206
211
  }
207
212
 
213
+ // Write `cachedAccessories` to disk.
214
+ _flushCachedAccessories () {
215
+ this.debug('flush cachedAccessories')
216
+ this._homebridge.updatePlatformAccessories()
217
+ }
218
+
208
219
  // Called every second.
209
220
  _beat (beat) {
210
221
  beat += 1
@@ -221,9 +232,7 @@ class Platform extends homebridgeLib.Delegate {
221
232
  }, 1000 - drift)
222
233
 
223
234
  if (beat % context.saveInterval === 30) {
224
- // Persist dynamic platform accessories to cachedAccessories
225
- this.debug('flush cachedAccessories')
226
- this._homebridge.updatePlatformAccessories()
235
+ this._flushCachedAccessories()
227
236
  }
228
237
 
229
238
  if (beat % context.checkInterval === 0) {
@@ -255,6 +264,9 @@ class Platform extends homebridgeLib.Delegate {
255
264
  this._shuttingDown = true
256
265
  this.removeAllListeners('upnpDeviceAlive')
257
266
  this.removeAllListeners('upnpDeviceFound')
267
+ if (this._ui != null && this._ui.abortController != null) {
268
+ this._ui.abortController.abort()
269
+ }
258
270
  for (const id in this._accessoryDelegates) {
259
271
  /** Emitted when Homebridge is shutting down.
260
272
  *
@@ -545,6 +557,97 @@ class Platform extends homebridgeLib.Delegate {
545
557
  })
546
558
  }
547
559
 
560
+ // ===== Dynamic Configuration through Homebridge UI =========================
561
+
562
+ /** Handler for requests from the Homebridge Plugin UI Server.
563
+ * @function Platform#onUiRequest
564
+ * @async
565
+ * @abstract
566
+ * @param {string} method - The request method.
567
+ * @param {string} resource - The request resource.
568
+ * @param {*} body - The request body.
569
+ * @returns {*} - The response body.
570
+ */
571
+
572
+ // Create HTTP server for Homebridge Plugin UI Settings.
573
+ async _createUiServer () {
574
+ this._ui = {}
575
+ this._ui.server = new http.Server()
576
+ this._ui.server
577
+ .on('listening', () => {
578
+ this._ui.port = this._ui.server.address().port
579
+ this.log('ui server: listening on http://127.0.0.1:%d/', this._ui.port)
580
+ for (const id in this._accessoryDelegates) {
581
+ this._accessoryDelegates[id]._context.uiPort = this._ui.port
582
+ }
583
+ })
584
+ .on('error', (error) => { this.error(error) })
585
+ .on('close', () => {
586
+ this.debug('ui server: closed port %d', this._ui.port)
587
+ })
588
+ .on('request', async (request, response) => {
589
+ let buffer = ''
590
+ request.on('data', (data) => { buffer += data })
591
+ request.on('end', async () => {
592
+ try {
593
+ if (buffer !== '') {
594
+ try {
595
+ request.body = JSON.parse(buffer)
596
+ } catch (error) {
597
+ this.log(
598
+ 'ui request %s: %s %s %s', ++this._ui.requestId,
599
+ request.method, request.url, buffer
600
+ )
601
+ this.warn('ui request %d: %s', this._ui.requestId, error.message)
602
+ response.writeHead(400) // Bad Request
603
+ response.end()
604
+ return
605
+ }
606
+ this.debug(
607
+ 'ui request %s: %s %s %j', ++this._ui.requestId,
608
+ request.method, request.url, request.body
609
+ )
610
+ } else {
611
+ this.debug(
612
+ 'ui request %s: %s %s', ++this._ui.requestId,
613
+ request.method, request.url
614
+ )
615
+ }
616
+ const { status, body } =
617
+ request.method === 'GET' && request.url === '/ping'
618
+ ? { status: 200, body: 'pong' }
619
+ : await this.onUiRequest(
620
+ request.method, request.url, request.body
621
+ )
622
+ this.debug(
623
+ 'ui request %d: %d %s', this._ui.requestId,
624
+ status, http.STATUS_CODES[status]
625
+ )
626
+ if (status === 200) {
627
+ this.vdebug('ui request %d: response: %j', this._ui.requestId, body)
628
+ response.writeHead(status, { 'Content-Type': 'application/json' })
629
+ response.end(JSON.stringify(body))
630
+ } else {
631
+ response.writeHead(status)
632
+ response.end()
633
+ }
634
+ } catch (error) {
635
+ this.warn('ui request %d: %s', this._ui.requestId, error)
636
+ response.writeHead(500) // Internal Server Error
637
+ response.end()
638
+ }
639
+ })
640
+ })
641
+ this._ui.abortController = new AbortController() // eslint-disable-line no-undef
642
+ this._ui.requestId = 0
643
+ this._ui.server.listen({
644
+ port: 0,
645
+ host: '127.0.0.1',
646
+ signal: this._ui.abortController.signal
647
+ })
648
+ await events.once(this._ui.server, 'listening')
649
+ }
650
+
548
651
  // ===== Logging =============================================================
549
652
 
550
653
  // Do the heavy lifting for debug(), error(), fatal(), log(), and warn(),
@@ -0,0 +1,237 @@
1
+ // homebridge-deconz/homebridge-lib/UiServer.js
2
+ //
3
+ // Library for Homebridge plugins.
4
+ // Copyright © 2022 Erik Baauw. All rights reserved.
5
+
6
+ 'use strict'
7
+
8
+ const {
9
+ HomebridgePluginUiServer // , RequestError
10
+ } = require('@homebridge/plugin-ui-utils')
11
+ const { HttpClient, formatError } = require('../index')
12
+ const chalk = require('chalk')
13
+ const fs = require('fs/promises')
14
+ const path = require('path')
15
+ const util = require('util')
16
+
17
+ /** Server for handling Homebridge Plugin UI requests.
18
+ *
19
+ * See {@link https://github.com/homebridge/plugin-ui-utils plugin-ui-utils}.
20
+ *
21
+ * The Homebridge Plugin UI Server runs in a separate process, which is spawned
22
+ * by the Homebridge UI when the plugin _Settings_ are opened.
23
+ * It implements an {@link HttpClient} to connect to the HTTP server provided
24
+ * by {@link Platform} to change plugin settings dynamically.
25
+ *
26
+ * `UiServer` implemensts the following requests, which are documented as events:
27
+ * - {@link UiServer#event:get get} - Send a GET request to the plugin instance.
28
+ * - {@link UiServer#event:put put} - Send a put request to the plugin instance.
29
+ * - {@link UiServer#event:cachedAccessories cachedAccessories} - Get the
30
+ * cachedAccessories for a (child) bridge instance.
31
+ * @extends HomebridgePluginUiServer
32
+ */
33
+ class UiServer extends HomebridgePluginUiServer {
34
+ constructor () {
35
+ super()
36
+ this.clients = {}
37
+
38
+ /** Do a GET request to the plugin instance.
39
+ * @event UiServer#get
40
+ * @type {object}
41
+ * @property {integer} uiPort - The port of the plugin instance UI server.
42
+ * @property {string} resource - The requested resource.
43
+ * @returns {*} - The response body.
44
+ */
45
+ this.onRequest('get', async (params) => {
46
+ try {
47
+ const { uiPort, path } = params
48
+ const client = this._getClient(uiPort)
49
+ const { body } = await client.get(path)
50
+ return body
51
+ } catch (error) {
52
+ if (!(error instanceof HttpClient.HttpError)) {
53
+ this.error(error)
54
+ }
55
+ }
56
+ })
57
+
58
+ /** Do a PUT request to the plugin instance.
59
+ * @event UiServer#put
60
+ * @param {object}
61
+ * @property {integer} uiPort - The port of the plugin instance UI server.
62
+ * @property {string} resource - The requested resource.
63
+ * @property {*} body - The body of the request.
64
+ * @returns {HttpResponse} - The response.
65
+ */
66
+ this.onRequest('put', async (params) => {
67
+ try {
68
+ const { uiPort, path, body } = params
69
+ const client = this._getClient(uiPort)
70
+ const response = await client.put(path, JSON.stringify(body))
71
+ return response
72
+ } catch (error) {
73
+ if (!(error instanceof HttpClient.HttpError)) {
74
+ this.error(error)
75
+ }
76
+ }
77
+ })
78
+
79
+ /** Get the cached accessories for a single (child) bridge instance.
80
+ *
81
+ * This endpoint is needed because `homebridge.getCachedAccessories()` from
82
+ * `plugin-ui-utils` doesn't indicate to which child bridge an accessory
83
+ * belongs.
84
+ * @event UiServer#cachedAccessories
85
+ * @type {object}
86
+ * @property {?string} username - The virtual MAC address of the child
87
+ * bridge. Use `null` for the main bridge.
88
+ * @returns {Object} cachedAccessories - The cached accessories.
89
+ */
90
+ this.onRequest('cachedAccessories', async (params) => {
91
+ try {
92
+ const { username } = params
93
+ let fileName = 'cachedAccessories'
94
+ if (username != null) {
95
+ fileName += '.' + username.replace(/:/g, '').toUpperCase()
96
+ }
97
+ const fullFileName = path.join(
98
+ this.homebridgeStoragePath,
99
+ 'accessories',
100
+ fileName
101
+ )
102
+ const json = await fs.readFile(fullFileName)
103
+ const cachedAccessories = JSON.parse(json)
104
+ return cachedAccessories
105
+ } catch (error) {
106
+ this.error(error)
107
+ }
108
+ return []
109
+ })
110
+ }
111
+
112
+ _getClient (uiPort) {
113
+ if (this.clients[uiPort] == null) {
114
+ this.clients[uiPort] = new HttpClient({
115
+ host: 'localhost:' + uiPort,
116
+ json: true,
117
+ keepAlive: true
118
+ })
119
+ this.clients[uiPort]
120
+ .on('error', (error) => {
121
+ this.warn('request %d: %s', error.request.id, error)
122
+ })
123
+ .on('request', (request) => {
124
+ if (request.body == null) {
125
+ this.debug(
126
+ 'request %d: %s %s', request.id, request.method, request.resource
127
+ )
128
+ } else {
129
+ this.debug(
130
+ 'request %d: %s %s %j', request.id,
131
+ request.method, request.resource, request.body
132
+ )
133
+ }
134
+ })
135
+ .on('response', (response) => {
136
+ this.vdebug(
137
+ 'request %d: response: %j', response.request.id, response.body
138
+ )
139
+ this.debug(
140
+ 'request %d: %s %s', response.request.id,
141
+ response.statusCode, response.statusMessage
142
+ )
143
+ })
144
+ }
145
+ return this.clients[uiPort]
146
+ }
147
+
148
+ // ===== Logging =============================================================
149
+
150
+ /** Print debug message to stdout.
151
+ * @param {string|Error} format - The printf-style message or an instance of
152
+ * [Error](https://nodejs.org/dist/latest-v14.x/docs/api/errors.html#errors_class_error).
153
+ * @param {...string} args - Arguments to the printf-style message.
154
+ */
155
+ debug (format, ...args) {
156
+ this._log({ chalk: chalk.grey }, format, ...args)
157
+ }
158
+
159
+ /** Print error message to stdout.
160
+ * @param {string|Error} format - The printf-style message or an instance of
161
+ * [Error](https://nodejs.org/dist/latest-v14.x/docs/api/errors.html#errors_class_error).
162
+ * @param {...string} args - Arguments to the printf-style message.
163
+ */
164
+ error (format, ...args) {
165
+ this._log({ label: 'error', chalk: chalk.bold.red }, format, ...args)
166
+ }
167
+
168
+ /** Print log message to stdout.
169
+ * @param {string|Error} format - The printf-style message or an instance of
170
+ * [Error](https://nodejs.org/dist/latest-v14.x/docs/api/errors.html#errors_class_error).
171
+ * @param {...string} args - Arguments to the printf-style message.
172
+ */
173
+ log (format, ...args) {
174
+ this._log({}, format, ...args)
175
+ }
176
+
177
+ /** Print verbose debug message to stdout.
178
+ * @param {string|Error} format - The printf-style message or an instance of
179
+ * [Error](https://nodejs.org/dist/latest-v14.x/docs/api/errors.html#errors_class_error).
180
+ * @param {...string} args - Arguments to the printf-style message.
181
+ */
182
+ vdebug (format, ...args) {
183
+ this._log({ chalk: chalk.grey }, format, ...args)
184
+ }
185
+
186
+ /** Print very verbose debug message to stdout.
187
+ * @param {string|Error} format - The printf-style message or an instance of
188
+ * [Error](https://nodejs.org/dist/latest-v14.x/docs/api/errors.html#errors_class_error).
189
+ * @param {...string} args - Arguments to the printf-style message.
190
+ */
191
+ vvdebug (format, ...args) {
192
+ this._log({ chalk: chalk.grey }, format, ...args)
193
+ }
194
+
195
+ /** Print warning message to stdout.
196
+ * @param {string|Error} format - The printf-style message or an instance of
197
+ * [Error](https://nodejs.org/dist/latest-v14.x/docs/api/errors.html#errors_class_error).
198
+ * @param {...string} args - Arguments to the printf-style message.
199
+ */
200
+ warn (format, ...args) {
201
+ this._log({ label: 'warning', chalk: chalk.yellow }, format, ...args)
202
+ }
203
+
204
+ // Do the heavy lifting for debug(), error(), fatal(), log(), and warn(),
205
+ // taking into account the options, and errors vs exceptions.
206
+ _log (params = {}, ...args) {
207
+ const output = process.stdout
208
+ let message = ''
209
+
210
+ // If last argument is Error convert it to string.
211
+ if (args.length > 0) {
212
+ let lastArg = args.pop()
213
+ if (lastArg instanceof Error) {
214
+ lastArg = formatError(lastArg, true)
215
+ }
216
+ args.push(lastArg)
217
+ }
218
+
219
+ // Format message.
220
+ if (args[0] == null) {
221
+ message = ''
222
+ } else if (typeof args[0] === 'string') {
223
+ message = util.format(...args)
224
+ } else {
225
+ message = util.format('%o', ...args)
226
+ }
227
+
228
+ // Handle colours.
229
+ if (params.chalk != null) {
230
+ message = params.chalk(message)
231
+ }
232
+
233
+ output.write(message)
234
+ }
235
+ }
236
+
237
+ module.exports = UiServer
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "Library for homebridge plugins",
4
4
  "author": "Erik Baauw",
5
5
  "license": "Apache-2.0",
6
- "version": "5.2.3-0",
6
+ "version": "5.3.1",
7
7
  "keywords": [
8
8
  "homekit",
9
9
  "homebridge"
@@ -22,9 +22,10 @@
22
22
  },
23
23
  "engines": {
24
24
  "homebridge": "^1.4.0",
25
- "node": "^16.14.0"
25
+ "node": "^16.14.2"
26
26
  },
27
27
  "dependencies": {
28
+ "@homebridge/plugin-ui-utils": "~0.0.19",
28
29
  "bonjour-hap": "^3.6.3",
29
30
  "chalk": "^4.1.2",
30
31
  "semver": "^7.3.5"