homebridge-lib 5.1.17 → 5.1.19

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 CHANGED
@@ -28,11 +28,12 @@ See [Homebridge WS](https://github.com/ebaauw/homebridge-ws) for an example plug
28
28
  ### Command-Line Tools
29
29
  The Homebridge Lib library comes with a number of command-line tools for troubleshooting Homebridge installations.
30
30
 
31
- Tool | Description
32
- ------- | -----------
33
- `hap` | Logger for HomeKit accessory announcements.
34
- `json` | JSON formatter.
35
- `upnp` | UPnP tool.
31
+ Tool | Description
32
+ --------- | -----------
33
+ `hap` | Logger for HomeKit accessory announcements.
34
+ `json` | JSON formatter.
35
+ `sysinfo` | Print hardware and operating system information.
36
+ `upnp` | UPnP tool.
36
37
 
37
38
  Each command-line tool takes a `-h` or `--help` argument to provide a brief overview of its functionality and command-line arguments.
38
39
 
package/cli/sysinfo.js ADDED
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env node
2
+
3
+ // homebridge-lib/cli/sysinfo.js
4
+ //
5
+ // Library for Homebridge plugins.
6
+ // Copyright © 2021 Erik Baauw. All rights reserved.
7
+ //
8
+ // Show system info.
9
+
10
+ 'use strict'
11
+
12
+ const homebridgeLib = require('../index')
13
+
14
+ const { b } = homebridgeLib.CommandLineTool
15
+
16
+ const usage = `${b('sysinfo')} [${b('-hVDj')}]`
17
+ const help = `System information tool.
18
+
19
+ Print Hardware and Operating System information.
20
+
21
+ Usage: ${usage}
22
+
23
+ Parameters:
24
+ ${b('-h')}, ${b('--help')}
25
+ Print this help and exit.
26
+
27
+ ${b('-V')}, ${b('--version')}
28
+ Print version and exit.
29
+
30
+ ${b('-D')}, ${b('--debug')}
31
+ Print debug messages.
32
+
33
+ ${b('-j')}, ${b('--json')}
34
+ Print full info in json.`
35
+
36
+ class Main extends homebridgeLib.CommandLineTool {
37
+ constructor () {
38
+ super()
39
+ this.usage = usage
40
+ this.options = {
41
+ noWhiteSpace: false
42
+ }
43
+ this.upnp = {}
44
+ }
45
+
46
+ parseArguments () {
47
+ const parser = new homebridgeLib.CommandLineParser()
48
+ parser
49
+ .help('h', 'help', help)
50
+ .version('V', 'version')
51
+ .flag('D', 'debug', () => { this.setOptions({ debug: true }) })
52
+ .flag('j', 'json', () => { this.json = true })
53
+ .parse()
54
+ }
55
+
56
+ async main () {
57
+ try {
58
+ this.parseArguments()
59
+ this.systemInfo = new homebridgeLib.SystemInfo()
60
+ this.systemInfo
61
+ .on('error', (error) => { this.error(error) })
62
+ .on('exec', (command) => { this.debug('exec: %s', command) })
63
+ .on('readFile', (fileName) => { this.debug('read file: %s', fileName) })
64
+ await this.systemInfo.init()
65
+ if (this.json) {
66
+ const jsonFormatter = new homebridgeLib.JsonFormatter(this.options)
67
+ this.print(jsonFormatter.stringify({
68
+ hardware: this.systemInfo.hwInfo,
69
+ os: this.systemInfo.osInfo
70
+ }))
71
+ } else {
72
+ this.print(this.systemInfo.hwInfo.prettyName)
73
+ this.print(this.systemInfo.osInfo.prettyName)
74
+ }
75
+ } catch (error) {
76
+ await this.fatal(error)
77
+ }
78
+ }
79
+ }
80
+
81
+ new Main().main()
package/index.js CHANGED
@@ -30,6 +30,8 @@ function isNodejsError (e) {
30
30
  return typeof e.code === 'string' && e.code.startsWith('ERR_')
31
31
  }
32
32
 
33
+ const zeroes = '00000000000000000000000000000000'
34
+
33
35
  /** Library for Homebridge plugins.
34
36
  * see the {@tutorial homebridgeLib} tutorial.
35
37
  *
@@ -180,6 +182,13 @@ class homebridgeLib {
180
182
  */
181
183
  static get ServiceDelegate () { return require('./lib/ServiceDelegate') }
182
184
 
185
+ /** System information.
186
+ * <br>See {@link SystemInfo}.
187
+ * @type {Class}
188
+ * @memberof module:homebridgeLib
189
+ */
190
+ static get SystemInfo () { return require('./lib/SystemInfo') }
191
+
183
192
  /** Universal Plug and Play client.
184
193
  * <br>See {@link UpnpClient}.
185
194
  * @type {Class}
@@ -258,6 +267,16 @@ class homebridgeLib {
258
267
  }
259
268
  return e.message
260
269
  }
270
+
271
+ /** Convert integer to hex string.
272
+ * @param {integer} i - The integer.
273
+ * @param {integer} [length=4] - The number of digits in the hex string.
274
+ * @returns {string} - The hex string.
275
+ * @memberof module:homebridgeLib
276
+ */
277
+ static toHexString (i, length = 4) {
278
+ return (zeroes + i.toString(16)).slice(-length).toUpperCase()
279
+ }
261
280
  }
262
281
 
263
282
  module.exports = homebridgeLib
@@ -53,10 +53,15 @@ class CommandLineTool {
53
53
  .on('SIGINT', this._onSignal.bind(this))
54
54
  .on('SIGTERM', this._onSignal.bind(this))
55
55
  .on('SIGABRT', this._onSignal.bind(this))
56
- .on('uncaughtException', (error) => { this.fatal(error) })
57
- .on('unhandledRejection', (error) => { this.fatal(error) })
56
+ .on('uncaughtException', async (error) => {
57
+ await this.fatal('uncaught exception: %s', error.stack)
58
+ })
59
+ .removeAllListeners('unhandledRejection')
60
+ .on('unhandledRejection', async (error) => {
61
+ await this.fatal('unhandled rejection: %s', error.stack)
62
+ })
58
63
  } catch (error) {
59
- this.fatal(error)
64
+ this._log({ label: 'fatal', chalk: chalk.bold.red }, error)
60
65
  }
61
66
  }
62
67
 
@@ -120,6 +120,8 @@ class MyHomeKitTypes extends homebridgeLib.CustomHomeKitTypes {
120
120
  * <br>Used by: homebridge-otgw.
121
121
  * @property {Class} PumpStarts - Number of pump starts.
122
122
  * <br>Used by: homebridge-otgw.
123
+ * @property {Class} Recall - Recall a scene.
124
+ * <br>Used by: homebridge-hue.
123
125
  * @property {Class} Repeat - Repeat tracks (off, 1, all).
124
126
  * <br>Used by: homebridge-zp.
125
127
  * @property {Class} Resource - REST API resource corresponding for
@@ -133,6 +135,8 @@ class MyHomeKitTypes extends homebridgeLib.CustomHomeKitTypes {
133
135
  * <br>Used by: homebridge-otgw.
134
136
  * @property {Class} RingToOpen - Ring to Open active.
135
137
  * <br> Used by: homebridge-nb.
138
+ * @property {Class} Search - Search for new devices.
139
+ * <br>Used by: homebridge-hue.
136
140
  * @property {Class} Sensitivity - Motion sensor sensitivity.
137
141
  * <br>See also: {@link EveHomeKitTypes#Characteristics eve.Sensitivity}.
138
142
  * <br>Previously used by: homebridge-hue in MotionSensor service.
@@ -664,6 +668,17 @@ class MyHomeKitTypes extends homebridgeLib.CustomHomeKitTypes {
664
668
  perms: [this.Perms.READ, this.Perms.NOTIFY, this.Perms.WRITE]
665
669
  }, 'Motor Speed')
666
670
 
671
+ this.createCharacteristicClass('Search', uuid('073'), {
672
+ format: this.Formats.BOOL,
673
+ perms: [this.Perms.READ, this.Perms.NOTIFY, this.Perms.WRITE],
674
+ adminOnlyAccess: [this.Access.WRITE]
675
+ })
676
+
677
+ this.createCharacteristicClass('Recall', uuid('074'), {
678
+ format: this.Formats.BOOL,
679
+ perms: [this.Perms.READ, this.Perms.NOTIFY, this.Perms.WRITE]
680
+ })
681
+
667
682
  // Characteristic for Unique ID, used by homebridge-hue.
668
683
  // Source: as exposed by the Philips Hue bridge. This characteristic is
669
684
  // used by the Hue app to select the accessories when syncing Hue bridge
package/lib/Platform.js CHANGED
@@ -210,6 +210,18 @@ class Platform extends homebridgeLib.Delegate {
210
210
  // this.error('unhandled rejection: %s', error.stack)
211
211
  // })
212
212
  global[globalKey].Platform.heartbeat = this
213
+ /** System information.
214
+ * @type {SystemInfo}
215
+ * @readonly
216
+ */
217
+ this.systemInfo = new homebridgeLib.SystemInfo()
218
+ this.systemInfo
219
+ .on('error', (error) => { this.warn(error) })
220
+ .on('exec', (command) => { this.debug('exec: %s', command) })
221
+ .on('readFile', (filename) => { this.debug('read file: %s', filename) })
222
+ await this.systemInfo.init()
223
+ this.log('hardware: %s', this.systemInfo.hwInfo.prettyName)
224
+ this.log('os: %s', this.systemInfo.osInfo.prettyName)
213
225
  this.heartbeatClients = Object.keys(global[globalKey]).filter((key) => {
214
226
  return global[globalKey][key].platform != null
215
227
  })
@@ -221,6 +233,8 @@ class Platform extends homebridgeLib.Delegate {
221
233
  this._homebridge.updatePlatformAccessories()
222
234
  this.log('goodbye')
223
235
  })
236
+ } else {
237
+ this.systemInfo = global[globalKey].Platform.heartbeat.systemInfo
224
238
  }
225
239
  if (this._isHomebridgeLib) {
226
240
  const jobs = []
@@ -0,0 +1,419 @@
1
+ // homebridge-lib/lib/SystemInfo.js
2
+ //
3
+ // Library for Homebridge plugins.
4
+ // Copyright © 2019-2021 Erik Baauw. All rights reserved.
5
+
6
+ 'use strict'
7
+
8
+ const events = require('events')
9
+ const { exec, execFile } = require('child_process')
10
+ const fs = require('fs').promises
11
+ const os = require('os')
12
+ const semver = require('semver')
13
+
14
+ const homebridgeLib = require('../index')
15
+
16
+ const rpiInfo = {
17
+ manufacturers: {
18
+ 0: 'Sony UK',
19
+ 1: 'Egoman',
20
+ 2: 'Embest',
21
+ 3: 'Sony Japan',
22
+ 4: 'Embest',
23
+ 5: 'Stadium'
24
+ },
25
+ memorySizes: {
26
+ 0: '256MB',
27
+ 1: '512MB',
28
+ 2: '1GB',
29
+ 3: '2GB',
30
+ 4: '4GB',
31
+ 5: '8GB'
32
+ },
33
+ models: {
34
+ 0: 'A',
35
+ 1: 'B',
36
+ 2: 'A+',
37
+ 3: 'B+',
38
+ 4: '2B',
39
+ 5: 'Alpha', // early prototype
40
+ 6: 'CM1',
41
+ 8: '3B',
42
+ 9: 'Zero',
43
+ 10: 'CM3',
44
+ 12: 'Zero W',
45
+ 13: '3B+',
46
+ 14: '3A+',
47
+ // 15: '', // Internal use only
48
+ 16: 'CM3+',
49
+ 17: '4B',
50
+ 18: 'Zero 2 W',
51
+ 19: '400',
52
+ 20: 'CM4'
53
+ },
54
+ processors: {
55
+ 0: 'BCM2835',
56
+ 1: 'BCM2836',
57
+ 2: 'BCM2837',
58
+ 3: 'BCM2711'
59
+ },
60
+ oldRevisions: {
61
+ 2: { model: 'B', revision: '1.0', memory: '256MB', manufacturer: 'Egoman' },
62
+ 3: { model: 'B', revision: '1.0', memory: '256MB', manufacturer: 'Egoman' },
63
+ 4: { model: 'B', revision: '2.0', memory: '256MB', manufacturer: 'Sony UK' },
64
+ 5: { model: 'B', revision: '2.0', memory: '256MB', manufacturer: 'Qisda' },
65
+ 6: { model: 'B', revision: '2.0', memory: '256MB', manufacturer: 'Egoman' },
66
+ 7: { model: 'A', revision: '2.0', memory: '256MB', manufacturer: 'Egoman' },
67
+ 8: { model: 'A', revision: '2.0', memory: '256MB', manufacturer: 'Sony UK' },
68
+ 9: { model: 'A', revision: '2.0', memory: '256MB', manufacturer: 'Qisda' },
69
+ 13: { model: 'B', revision: '2.0', memory: '512MB', manufacturer: 'Egoman' },
70
+ 14: { model: 'B', revision: '2.0', memory: '512MB', manufacturer: 'Sony UK' },
71
+ 15: { model: 'B', revision: '2.0', memory: '512MB', manufacturer: 'Egoman' },
72
+ 16: { model: 'B+', revision: '1.2', memory: '512MB', manufacturer: 'Sony UK' },
73
+ 17: { model: 'CM1', revision: '1.0', memory: '512MB', manufacturer: 'Sony UK' },
74
+ 18: { model: 'A+', revision: '1.1', memory: '256MB', manufacturer: 'Sony UK' },
75
+ 19: { model: 'B+', revision: '1.2', memory: '512MB', manufacturer: 'Embest' },
76
+ 20: { model: 'CM1', revision: '1.0', memory: '512MB', manufacturer: 'Embest' },
77
+ 21: { model: 'A+', revision: '1.1', memory: '256MB/512MB', manufacturer: 'Embest' }
78
+ }
79
+ }
80
+
81
+ // See: https://en.wikipedia.org/wiki/MacOS_version_history
82
+ const macOsInfo = {
83
+ versionNames: {
84
+ '10.0': 'Cheetah',
85
+ 10.1: 'Puma',
86
+ 10.2: 'Jaguar',
87
+ 10.3: 'Panther',
88
+ 10.4: 'Tiger',
89
+ 10.5: 'Leopard',
90
+ 10.6: 'Snow Leopard',
91
+ 10.7: 'Lion',
92
+ 10.8: 'Mountain Lion',
93
+ 10.9: 'Mavericks',
94
+ '10.10': 'Yosemite',
95
+ 10.11: 'El Capitan',
96
+ 10.12: 'Sierra',
97
+ 10.13: 'High Sierra',
98
+ 10.14: 'Mojave',
99
+ 10.15: 'Catalina',
100
+ 11: 'Big Sur',
101
+ 12: 'Monterey'
102
+ }
103
+ }
104
+
105
+ /** System information.
106
+ * @extends EventEmitter
107
+ */
108
+ class SystemInfo extends events.EventEmitter {
109
+ /** Extract Raspberry Pi serial number and hardware revision info from the
110
+ * contents of `/proc/cpuinfo`.
111
+ * @param {string} cpuInfo - The contents of `/proc/cpuinfo`.
112
+ * @return {object} - The extracted info.
113
+ */
114
+ static parseRpiCpuInfo (cpuInfo) {
115
+ const id = /Serial\s*: ([0-9a-f]{16})/.exec(cpuInfo)[1].toUpperCase()
116
+ const revision = parseInt(
117
+ /Revision\s*: ([0-9a-f]{4,})/.exec(cpuInfo)[1], 16
118
+ ) & 0x00FFFFFF
119
+ let gpioMask, manufacturer, memory, model, modelRevision, processor
120
+ if ((revision & 0x00800000) !== 0) { // New revision scheme.
121
+ manufacturer = rpiInfo.manufacturers[(revision & 0x000F0000) >> 16]
122
+ memory = rpiInfo.memorySizes[(revision & 0x00700000) >> 20]
123
+ model = rpiInfo.models[(revision & 0x00000FF0) >> 4]
124
+ modelRevision = '1.' + ((revision & 0x0000000F) >> 0).toString()
125
+ processor = rpiInfo.processors[(revision & 0x0000F000) >> 12]
126
+ } else if (rpiInfo.oldRevisions[revision] != null) { // Old incremental revisions.
127
+ manufacturer = rpiInfo.oldRevisions[revision].manufacturer
128
+ memory = rpiInfo.oldRevisions[revision].memory
129
+ model = rpiInfo.oldRevisions[revision].model
130
+ modelRevision = rpiInfo.oldRevisions[revision].revision
131
+ processor = 'BCM2835'
132
+ }
133
+ if (model != null && model.startsWith('CM')) {
134
+ // Compute module
135
+ gpioMask = 0xFFFFFFFF // 0-31
136
+ } else if (revision >= 16) {
137
+ // Type 3
138
+ gpioMask = 0x0FFFFFFC // 2-27
139
+ } else if (revision >= 4) {
140
+ // Type 2
141
+ gpioMask = 0xFBC6CF9C // 2-4, 7-11, 14-15, 17-18, 22-25, 27-31
142
+ } else {
143
+ // Type 1
144
+ gpioMask = 0x03E6CF93 // 0-1, 4, 7-11, 14-15, 17-18, 21-25
145
+ }
146
+ return {
147
+ gpioMask: gpioMask,
148
+ gpioMaskSerial: (1 << 15) | (1 << 14),
149
+ id: id,
150
+ isRpi: true,
151
+ manufacturer: manufacturer,
152
+ memory: memory,
153
+ model: model,
154
+ modelRevision: modelRevision,
155
+ prettyName: [
156
+ 'Raspberry Pi', model, modelRevision, '(' + memory + ')'
157
+ ].join(' '),
158
+ powerLed: !(['A', 'B', 'Zero', 'Zero W', 'Zero 2 W'].includes(model)),
159
+ processor: processor,
160
+ revision: homebridgeLib.toHexString(revision, 6),
161
+ usbPower: ['B+', '2B', '3B', '3B+'].includes(model)
162
+ }
163
+ }
164
+
165
+ /** Initialise SystemInfo instance.
166
+ */
167
+ async init () {
168
+ switch (process.platform) {
169
+ case 'linux':
170
+ try {
171
+ this.osInfo = await this.getPiOsInfo()
172
+ } catch (error) { this.emit('error', error) }
173
+ if (['arm', 'arm64'].includes(process.arch)) {
174
+ try {
175
+ this.hwInfo = await this.getRpiInfo()
176
+ } catch (error) { this.emit('error', error) }
177
+ }
178
+ break
179
+ case 'darwin':
180
+ try {
181
+ this.osInfo = await this.getMacOsInfo()
182
+ } catch (error) { this.emit('error', error) }
183
+ try {
184
+ this.hwInfo = await this.getMacInfo()
185
+ } catch (error) { this.emit('error', error) }
186
+ break
187
+ default:
188
+ break
189
+ }
190
+ if (this.osInfo == null) {
191
+ this.osInfo = {
192
+ name: process.platform,
193
+ platform: process.platform,
194
+ prettyName: process.platform
195
+ }
196
+ }
197
+ if (this.hwInfo == null) {
198
+ this.hwInfo = {
199
+ nCores: os.cpus().length,
200
+ prettyName: process.arch,
201
+ processor: process.arch
202
+ }
203
+ }
204
+ this.platform = this.osInfo.platform
205
+ }
206
+
207
+ /** Extract serial number and hardware revision info from `/proc/cpuinfo`.
208
+ * @return {object} - The extracted info.
209
+ */
210
+ async getRpiInfo () {
211
+ const cpuInfo = await this.readTextFile('/proc/cpuinfo')
212
+ return SystemInfo.parseRpiCpuInfo(cpuInfo)
213
+ }
214
+
215
+ /** Extract OS info from /etc/os-release.
216
+ * @return {object} - The extracted info.
217
+ */
218
+ async getPiOsInfo () {
219
+ let name, platform, prettyName, version, versionName
220
+ const text = await this.readTextFile('/etc/os-release')
221
+ const lines = text.replace(/"/g, '').split('\n')
222
+ for (const line of lines) {
223
+ const fields = line.split('=')
224
+ if (fields.length === 2) {
225
+ switch (fields[0]) {
226
+ case 'ID':
227
+ platform = fields[1] // e.g. 'raspbian'
228
+ break
229
+ case 'NAME':
230
+ name = fields[1] // e.g. 'Raspbian GNU/Linux'
231
+ break
232
+ case 'PRETTY_NAME':
233
+ prettyName = fields[1] // e.g. 'Raspbian GNU/Linux 11 (bullseye)'
234
+ break
235
+ case 'VERSION_CODENAME':
236
+ versionName = fields[1] // e.g. 'bullseye'
237
+ break
238
+ case 'VERSION_ID':
239
+ version = fields[1] // e.g. '11'
240
+ break
241
+ default:
242
+ break
243
+ }
244
+ }
245
+ }
246
+ return {
247
+ name: name,
248
+ platform: platform,
249
+ prettyName: prettyName,
250
+ version: version,
251
+ versionName: versionName
252
+ }
253
+ }
254
+
255
+ /** Extract Apple Mac hardware info from `system_profiler` command.
256
+ * @return {object} - The extracted info.
257
+ */
258
+ async getMacInfo () {
259
+ let id, memory, model, nCores, prettyName, processor, revision
260
+ let text = await this.exec('system_profiler', 'SPHardwareDataType')
261
+ const lines = text.split('\n')
262
+ for (const line of lines) {
263
+ const fields = line.split(': ')
264
+ if (fields.length === 2) {
265
+ switch (fields[0].trim()) {
266
+ case 'Memory':
267
+ memory = fields[1].replace(/ /g, '')
268
+ break
269
+ case 'Model Identifier':
270
+ revision = fields[1]
271
+ break
272
+ case 'Model Name':
273
+ model = fields[1]
274
+ break
275
+ case 'Chip':
276
+ case 'Processor Name':
277
+ processor = fields[1]
278
+ break
279
+ case 'Serial Number (system)':
280
+ id = fields[1]
281
+ break
282
+ case 'Total Number of Cores':
283
+ nCores = fields[1].split(' ')[0]
284
+ break
285
+ default:
286
+ break
287
+ }
288
+ }
289
+ }
290
+ text = await this.exec(
291
+ 'plutil', '-convert', 'json', '-o', '-',
292
+ process.env.HOME + '/Library/Preferences/com.apple.SystemProfiler.plist'
293
+ )
294
+ const s = JSON.parse(text)['CPU Names']
295
+ if (s != null) {
296
+ // This is empty on my MacBook Pro (14-inch, 2021).
297
+ for (const key of Object.keys(s)) {
298
+ if (key.startsWith(id.slice(-4))) {
299
+ prettyName = s[key]
300
+ }
301
+ }
302
+ } else {
303
+ // text = await this.exec(
304
+ // 'ioreg', '-c', 'IOPlatformDevice', '-d', '4', '-n', 'product'
305
+ // )
306
+ text = await this.execShell('ioreg -l | grep product-description')
307
+ // This is empty on my Intel Macs - I think the key is registered for
308
+ // running iPad apps on Apple chips.
309
+ const a = /"product-description" = <"([^"]*)">/.exec(text)
310
+ if (a != null) {
311
+ prettyName = a[1]
312
+ }
313
+ }
314
+ return {
315
+ id: id,
316
+ isMac: true,
317
+ manufacturer: 'Apple Inc.',
318
+ memory: memory,
319
+ model: model,
320
+ nCores: nCores,
321
+ prettyName: prettyName || model,
322
+ processor: processor,
323
+ revision: revision
324
+ }
325
+ }
326
+
327
+ /** Extract macOS info from `sw_vers` command.
328
+ * @return {object} - The extracted info.
329
+ */
330
+ async getMacOsInfo () {
331
+ let name, version, build
332
+ const text = await this.exec('sw_vers')
333
+ const lines = text.split('\n')
334
+ for (const line of lines) {
335
+ const fields = line.split(':\t')
336
+ if (fields.length === 2) {
337
+ switch (fields[0]) {
338
+ case 'ProductName': // e.g. 'macOS' or 'Mac OS X'
339
+ name = fields[1]
340
+ break
341
+ case 'ProductVersion': // e.g. '12.0.1'
342
+ version = fields[1]
343
+ break
344
+ case 'BuildVersion': // e.g. '21A559'
345
+ build = fields[1]
346
+ break
347
+ default:
348
+ break
349
+ }
350
+ }
351
+ }
352
+ let v = semver.major(version)
353
+ if (v === 10) {
354
+ v += '.' + semver.minor(version)
355
+ }
356
+ const versionName = macOsInfo.versionNames[v] // e.g. 'Monterey'
357
+ return {
358
+ build: build,
359
+ catalina: semver.gte(version, '10.15.0'),
360
+ name: name,
361
+ platform: process.platform,
362
+ prettyName: [name, versionName, version, '(' + build + ')'].join(' '),
363
+ version: version,
364
+ versionName: versionName
365
+ }
366
+ }
367
+
368
+ /** Execute a command on the local machine.
369
+ * @param {string} command - The command.
370
+ * @param {...string} ...args - The command parameters.
371
+ * @return {string} - The output of the command.
372
+ */
373
+ async exec (command, ...args) {
374
+ return new Promise((resolve, reject) => {
375
+ /** Emitted when a command is executed.
376
+ * @event SystemInfo#exec
377
+ * @param {string} command - The command.
378
+ */
379
+ this.emit('exec', command + ' ' + args.join(' '))
380
+ execFile(command, args, null, (error, stdout, stderr) => {
381
+ if (error != null) {
382
+ reject(error)
383
+ }
384
+ resolve(stdout)
385
+ })
386
+ })
387
+ }
388
+
389
+ /** Execute a shell command on the local machine.
390
+ * @param {string} command - The command.
391
+ * @return {string} - The output of the command.
392
+ */
393
+ async execShell (command) {
394
+ return new Promise((resolve, reject) => {
395
+ this.emit('exec', command)
396
+ exec(command, (error, stdout, stderr) => {
397
+ if (error != null) {
398
+ reject(error)
399
+ }
400
+ resolve(stdout)
401
+ })
402
+ })
403
+ }
404
+
405
+ /** Read a text file.
406
+ * @param {string} fileName - The file name.
407
+ * @return {string} - The contents of the file.
408
+ */
409
+ async readTextFile (fileName) {
410
+ /** Emitted when a file is read.
411
+ * @event SystemInfo#readFile
412
+ * @param {string} fileName - The file name.
413
+ */
414
+ this.emit('readFile', fileName)
415
+ return fs.readFile(fileName, 'utf8')
416
+ }
417
+ }
418
+
419
+ module.exports = SystemInfo
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.1.17",
6
+ "version": "5.1.19",
7
7
  "keywords": [
8
8
  "homekit",
9
9
  "homebridge"
@@ -17,10 +17,11 @@
17
17
  "bin": {
18
18
  "hap": "cli/hap.js",
19
19
  "json": "cli/json.js",
20
+ "sysinfo": "cli/sysinfo.js",
20
21
  "upnp": "cli/upnp.js"
21
22
  },
22
23
  "engines": {
23
- "homebridge": "^1.3.6",
24
+ "homebridge": "^1.3.8",
24
25
  "node": "^16.13.0"
25
26
  },
26
27
  "dependencies": {