@homebridge-plugins/homebridge-meross 10.8.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.
Files changed (54) hide show
  1. package/CHANGELOG.md +1346 -0
  2. package/LICENSE +21 -0
  3. package/README.md +68 -0
  4. package/config.schema.json +2066 -0
  5. package/eslint.config.js +49 -0
  6. package/lib/connection/http.js +345 -0
  7. package/lib/connection/mqtt.js +174 -0
  8. package/lib/device/baby.js +532 -0
  9. package/lib/device/cooler-single.js +447 -0
  10. package/lib/device/diffuser.js +730 -0
  11. package/lib/device/fan.js +530 -0
  12. package/lib/device/garage-main.js +225 -0
  13. package/lib/device/garage-single.js +495 -0
  14. package/lib/device/garage-sub.js +376 -0
  15. package/lib/device/heater-single.js +445 -0
  16. package/lib/device/hub-contact.js +56 -0
  17. package/lib/device/hub-leak.js +86 -0
  18. package/lib/device/hub-main.js +403 -0
  19. package/lib/device/hub-sensor.js +115 -0
  20. package/lib/device/hub-smoke.js +40 -0
  21. package/lib/device/hub-valve.js +377 -0
  22. package/lib/device/humidifier.js +521 -0
  23. package/lib/device/index.js +63 -0
  24. package/lib/device/light-cct.js +474 -0
  25. package/lib/device/light-dimmer.js +312 -0
  26. package/lib/device/light-rgb.js +528 -0
  27. package/lib/device/outlet-multi.js +383 -0
  28. package/lib/device/outlet-single.js +405 -0
  29. package/lib/device/power-strip.js +282 -0
  30. package/lib/device/purifier-single.js +372 -0
  31. package/lib/device/purifier.js +403 -0
  32. package/lib/device/roller-location.js +317 -0
  33. package/lib/device/roller.js +234 -0
  34. package/lib/device/sensor-presence.js +201 -0
  35. package/lib/device/switch-multi.js +403 -0
  36. package/lib/device/switch-single.js +371 -0
  37. package/lib/device/template.js +177 -0
  38. package/lib/device/thermostat.js +493 -0
  39. package/lib/fakegato/LICENSE +21 -0
  40. package/lib/fakegato/fakegato-history.js +814 -0
  41. package/lib/fakegato/fakegato-storage.js +108 -0
  42. package/lib/fakegato/fakegato-timer.js +125 -0
  43. package/lib/fakegato/uuid.js +27 -0
  44. package/lib/homebridge-ui/public/index.html +316 -0
  45. package/lib/homebridge-ui/server.js +10 -0
  46. package/lib/index.js +8 -0
  47. package/lib/platform.js +1256 -0
  48. package/lib/utils/colour.js +581 -0
  49. package/lib/utils/constants.js +377 -0
  50. package/lib/utils/custom-chars.js +165 -0
  51. package/lib/utils/eve-chars.js +130 -0
  52. package/lib/utils/functions.js +39 -0
  53. package/lib/utils/lang-en.js +114 -0
  54. package/package.json +70 -0
@@ -0,0 +1,49 @@
1
+ import { antfu } from '@antfu/eslint-config'
2
+
3
+ /** @type {typeof antfu} */
4
+ export default antfu(
5
+ {
6
+ ignores: [],
7
+ jsx: false,
8
+ rules: {
9
+ 'curly': ['error', 'multi-line'],
10
+ 'new-cap': 'off',
11
+ 'import/extensions': ['error', 'ignorePackages'],
12
+ 'import/order': 0,
13
+ 'jsdoc/check-alignment': 'warn',
14
+ 'jsdoc/check-line-alignment': 'warn',
15
+ 'jsdoc/require-returns-check': 0,
16
+ 'jsdoc/require-returns-description': 0,
17
+ 'no-undef': 'error',
18
+ 'perfectionist/sort-exports': 'error',
19
+ 'perfectionist/sort-imports': [
20
+ 'error',
21
+ {
22
+ groups: [
23
+ 'type',
24
+ 'internal-type',
25
+ 'builtin',
26
+ 'external',
27
+ 'internal',
28
+ ['parent-type', 'sibling-type', 'index-type'],
29
+ ['parent', 'sibling', 'index'],
30
+ 'object',
31
+ 'unknown',
32
+ ],
33
+ order: 'asc',
34
+ type: 'natural',
35
+ },
36
+ ],
37
+ 'perfectionist/sort-named-exports': 'error',
38
+ 'perfectionist/sort-named-imports': 'error',
39
+ 'quotes': ['error', 'single'],
40
+ 'sort-imports': 0,
41
+ 'style/brace-style': ['error', '1tbs', { allowSingleLine: true }],
42
+ 'style/quote-props': ['error', 'consistent-as-needed'],
43
+ 'test/no-only-tests': 'error',
44
+ 'unicorn/no-useless-spread': 'error',
45
+ 'unused-imports/no-unused-vars': ['error', { caughtErrors: 'none' }],
46
+ },
47
+ typescript: false,
48
+ },
49
+ )
@@ -0,0 +1,345 @@
1
+ import { Buffer } from 'node:buffer'
2
+ import { createHash } from 'node:crypto'
3
+
4
+ import axios from 'axios'
5
+
6
+ import platformConsts from '../utils/constants.js'
7
+ import {
8
+ encodeParams,
9
+ generateRandomString,
10
+ hasProperty,
11
+ parseError,
12
+ sleep,
13
+ } from '../utils/functions.js'
14
+ import platformLang from '../utils/lang-en.js'
15
+
16
+ export default class {
17
+ constructor(platform) {
18
+ this.devLoginRetried = false
19
+ this.domain = platform.accountDetails.domain || platform.config.domain
20
+ this.ignoredDevices = platform.ignoredDevices
21
+ this.ignoreHKNative = platform.config.ignoreHKNative
22
+ this.ignoreMatter = platform.config.ignoreMatter
23
+ this.key = platform.accountDetails.key
24
+ this.log = platform.log
25
+ this.mfaCode = platform.config.mfaCode
26
+ this.password = platform.config.password
27
+ this.showUserKey = platform.config.showUserKey
28
+ this.storageData = platform.storageData
29
+ this.token = platform.accountDetails.token
30
+ this.userId = platform.accountDetails.userId
31
+ this.username = platform.config.username
32
+ this.userkey = platform.config.userkey
33
+
34
+ this.requestHeaders = {
35
+ 'AppLanguage': 'en',
36
+ 'AppType': 'iOS',
37
+ 'AppVersion': '3.22.4',
38
+ 'Vendor': 'meross',
39
+ 'User-Agent': 'intellect_socket/3.22.4 (iPhone; iOS 17.2; Scale/2.00)',
40
+ }
41
+
42
+ // Common error codes
43
+ // https://github.com/Apollon77/meross-cloud/blob/master/lib/errorcodes.js
44
+ // 500: 'The selected timezone is not supported',
45
+ // 1001: 'Wrong or missing password',
46
+ // 1002: 'Account does not exist',
47
+ // 1003: 'This account has been disabled or deleted',
48
+ // 1004: 'Wrong email or password',
49
+ // 1005: 'Invalid email address',
50
+ // 1006: 'Bad password format',
51
+ // 1008: 'This email is not registered',
52
+ // 1019: 'Token expired',
53
+ // 1022: some issue with token
54
+ // 1030: wrong region
55
+ // 1032: missing mfa
56
+ // 1033: invalid mfa
57
+ // 1200: 'Token has expired',
58
+ // 1255: 'The number of remote control boards exceeded the limit',
59
+ // 1301: 'Too many tokens have been issued',
60
+ // 5000: 'Unknown or generic error',
61
+ // 5001: 'Unknown or generic error',
62
+ // 5002: 'Unknown or generic error',
63
+ // 5003: 'Unknown or generic error',
64
+ // 5004: 'Unknown or generic error',
65
+ // 5020: 'Infrared Remote device is busy',
66
+ // 5021: 'Infrared record timeout',
67
+ // 5022: 'Infrared record invalid'
68
+ }
69
+
70
+ async login() {
71
+ try {
72
+ const nonce = generateRandomString(16)
73
+ const timestampMillis = Date.now()
74
+ const loginParams = encodeParams({
75
+ email: this.username,
76
+ password: createHash('md5')
77
+ .update(this.password)
78
+ .digest('hex'),
79
+ encryption: 1,
80
+ accountCountryCode: '--',
81
+ mobileInfo: {
82
+ resolution: '--',
83
+ carrier: '--',
84
+ deviceModel: '--',
85
+ mobileOs: '--',
86
+ mobileOSVersion: '--',
87
+ uuid: '--',
88
+ },
89
+ agree: 1,
90
+ mfaCode: this.mfaCode || undefined,
91
+ })
92
+
93
+ // Generate the md5-hash (called signature)
94
+ const dataToSign = `23x17ahWarFH6w29${timestampMillis}${nonce}${loginParams}`
95
+ const md5hash = createHash('md5')
96
+ .update(dataToSign)
97
+ .digest('hex')
98
+
99
+ const res = await axios({
100
+ url: `https://${this.domain}/v1/Auth/signIn`,
101
+ method: 'post',
102
+ headers: {
103
+ Authorization: 'Basic ',
104
+ ...this.requestHeaders,
105
+ },
106
+ data: {
107
+ params: loginParams,
108
+ sign: md5hash,
109
+ timestamp: timestampMillis,
110
+ nonce,
111
+ },
112
+ })
113
+
114
+ // Check to see we got a response
115
+ if (!res.data || !res.data.data) {
116
+ throw new Error(platformLang.noResponse)
117
+ }
118
+
119
+ if (Object.keys(res.data.data).length === 0) {
120
+ // Sometimes returns 'Wrong password', sometimes 'Incorrect password'
121
+ if (res.data.info?.includes('password') || res.data.apiStatus === 1004) {
122
+ if (!this.base64Tried) {
123
+ this.base64Tried = true
124
+ this.password = Buffer.from(this.password, 'base64')
125
+ .toString('utf8')
126
+ .replace(/\r\n|\n|\r/g, '')
127
+ .trim()
128
+ return await this.login()
129
+ }
130
+ }
131
+
132
+ if ([1032, 1033].includes(res.data.apiStatus)) {
133
+ throw new Error(platformLang.mfaFail)
134
+ }
135
+
136
+ throw new Error(`${platformLang.loginFail} - ${JSON.stringify(res.data)}`)
137
+ }
138
+
139
+ // The iot-x.meross.com domain seems to work for most users, but not all
140
+ // If at this point the apiStatus is 1030 then the data object will include the correct region
141
+ // We can use this to update the domain and try again
142
+ if (res.data.apiStatus === 1030) {
143
+ this.domain = res.data.data.domain.replace(/^https?:\/\//, '')
144
+ this.log.warn('[HTTP] %s [%s].', platformLang.regionUpdate, this.domain)
145
+ return await this.login()
146
+ }
147
+
148
+ if (!res.data.data.token) {
149
+ // Now something unknown is happening
150
+ throw new Error(`${platformLang.loginFail} - ${JSON.stringify(res.data)}`)
151
+ }
152
+
153
+ this.key = res.data.data.key
154
+ this.token = res.data.data.token
155
+ this.userId = res.data.data.userid
156
+ if (this.showUserKey && !this.userkey) {
157
+ this.log.warn('%s: %s', platformLang.merossKey, this.key)
158
+ }
159
+ try {
160
+ await this.storageData.setItem(
161
+ 'Meross_All_Devices_temp',
162
+ `${this.username}:::${this.key}:::${this.token}:::${this.userId}:::${this.domain}`,
163
+ )
164
+ } catch (e) {
165
+ this.log.warn('[HTTP] %s %s.', platformLang.accTokenStoreErr, parseError(e))
166
+ }
167
+
168
+ return {
169
+ key: this.key,
170
+ token: this.token,
171
+ userId: this.userId,
172
+ }
173
+ } catch (err) {
174
+ if (err.code && platformConsts.httpRetryCodes.includes(err.code)) {
175
+ // Retry if another attempt could be successful
176
+ this.log.warn('[HTTP] %s [login() - %s].', platformLang.httpRetry, err.code)
177
+ await sleep(30000)
178
+ return this.login()
179
+ }
180
+ throw err
181
+ }
182
+ }
183
+
184
+ async getDevices() {
185
+ try {
186
+ if (!this.token) {
187
+ throw new Error(platformLang.notAuth)
188
+ }
189
+
190
+ const nonce = generateRandomString(16)
191
+ const timestampMillis = Date.now()
192
+ const loginParams = encodeParams({})
193
+
194
+ // Generate the md5-hash (called signature)
195
+ const dataToSign = `23x17ahWarFH6w29${timestampMillis}${nonce}${loginParams}`
196
+ const md5hash = createHash('md5')
197
+ .update(dataToSign)
198
+ .digest('hex')
199
+
200
+ const res = await axios({
201
+ url: `https://${this.domain}/v1/Device/devList`,
202
+ method: 'post',
203
+ headers: {
204
+ Authorization: `Basic ${this.token}`,
205
+ ...this.requestHeaders,
206
+ },
207
+ data: {
208
+ params: loginParams,
209
+ sign: md5hash,
210
+ timestamp: timestampMillis,
211
+ nonce,
212
+ },
213
+ })
214
+
215
+ // Check to see we got a response
216
+ if (!res.data) {
217
+ throw new Error(platformLang.noResponse)
218
+ }
219
+
220
+ if (!hasProperty(res.data, 'data') || !Array.isArray(res.data.data)) {
221
+ // apiStatus 1022 denotes that the token is invalid or has expired
222
+ // we could try logging in again, just once, and only if the mfaCode is not set
223
+ if (res.data.apiStatus === 1022) {
224
+ if (!this.mfaCode && !this.devLoginRetried) {
225
+ this.devLoginRetried = true
226
+ this.log.warn('[HTTP] %s.', platformLang.loginRetry)
227
+ await this.login()
228
+ return this.getDevices()
229
+ }
230
+
231
+ await this.storageData.removeItem('Meross_All_Devices_temp')
232
+ throw new Error(platformLang.accTokenInvalid)
233
+ }
234
+ throw new Error(`${platformLang.invalidDevices} - ${JSON.stringify(res.data)}`)
235
+ }
236
+
237
+ // Don't return ignored devices or those that have been configured for local control
238
+ const toReturn = []
239
+ res.data.data.forEach((device) => {
240
+ // Don't initialise the device if ignored
241
+ if (this.ignoredDevices.includes(device.uuid)) {
242
+ this.log('[%s] %s.', device.devName, platformLang.noInitIgnore)
243
+ return
244
+ }
245
+
246
+ const model = device.deviceType.toUpperCase()
247
+
248
+ // Don't initialise the device if the 'ignore homekit native option' is enabled and hardware matches
249
+ if (
250
+ this.ignoreHKNative
251
+ && device.hdwareVersion
252
+ && Array.isArray(platformConsts.hkNativeHardware[model])
253
+ && platformConsts.hkNativeHardware[model].includes(device.hdwareVersion.charAt(0))
254
+ ) {
255
+ this.log('[%s] %s.', device.devName, platformLang.noInitHKIgnore)
256
+ return
257
+ }
258
+
259
+ // Don't initialise the device if the 'ignore matter option' is enabled and hardware matches
260
+ if (
261
+ this.ignoreMatter
262
+ && device.hdwareVersion
263
+ && Array.isArray(platformConsts.matterHardware[model])
264
+ && platformConsts.matterHardware[model].includes(device.hdwareVersion.charAt(0))
265
+ ) {
266
+ this.log('[%s] %s.', device.devName, platformLang.noInitMatterIgnore)
267
+ return
268
+ }
269
+
270
+ // Add the device to the return array for the plugin to initialise as a cloud device
271
+ toReturn.push(device)
272
+ })
273
+
274
+ // Return the amended device list
275
+ return toReturn
276
+ } catch (err) {
277
+ if (err.code && platformConsts.httpRetryCodes.includes(err.code)) {
278
+ // Retry if another attempt could be successful
279
+ this.log.warn('[HTTP] %s [getDevices() - %s].', platformLang.httpRetry, err.code)
280
+ await sleep(30000)
281
+ return this.getDevices()
282
+ }
283
+ throw err
284
+ }
285
+ }
286
+
287
+ async getSubDevices(device) {
288
+ try {
289
+ if (!this.token) {
290
+ throw new Error(platformLang.notAuth)
291
+ }
292
+
293
+ const nonce = generateRandomString(16)
294
+ const timestampMillis = Date.now()
295
+ const loginParams = encodeParams({
296
+ uuid: device.uuid,
297
+ })
298
+
299
+ // Generate the md5-hash (called signature)
300
+ const dataToSign = `23x17ahWarFH6w29${timestampMillis}${nonce}${loginParams}`
301
+ const md5hash = createHash('md5')
302
+ .update(dataToSign)
303
+ .digest('hex')
304
+
305
+ const res = await axios({
306
+ url: `https://${this.domain}/v1/Hub/getSubDevices`,
307
+ method: 'post',
308
+ headers: {
309
+ Authorization: `Basic ${this.token}`,
310
+ ...this.requestHeaders,
311
+ },
312
+ data: {
313
+ params: loginParams,
314
+ sign: md5hash,
315
+ timestamp: timestampMillis,
316
+ nonce,
317
+ },
318
+ })
319
+
320
+ // Check to see we got a response
321
+ if (!res.data) {
322
+ throw new Error(platformLang.noResponse)
323
+ }
324
+
325
+ if (
326
+ res.data.info !== 'Success'
327
+ || !hasProperty(res.data, 'data')
328
+ || !Array.isArray(res.data.data)
329
+ ) {
330
+ throw new Error(`${platformLang.invalidSubdevices} - ${JSON.stringify(res.data)}`)
331
+ }
332
+
333
+ // Return the subdevice list to the platform
334
+ return res.data.data
335
+ } catch (err) {
336
+ if (err.code && platformConsts.httpRetryCodes.includes(err.code)) {
337
+ // Retry if another attempt could be successful
338
+ this.log.warn('[HTTP] %s [getDevices() - %s].', platformLang.httpRetry, err.code)
339
+ await sleep(30000)
340
+ return this.getSubDevices()
341
+ }
342
+ throw err
343
+ }
344
+ }
345
+ }
@@ -0,0 +1,174 @@
1
+ import { createHash } from 'node:crypto'
2
+
3
+ import { connect as mqttConnect } from 'mqtt'
4
+ import pTimeout from 'p-timeout'
5
+
6
+ import { generateRandomString } from '../utils/functions.js'
7
+ import platformLang from '../utils/lang-en.js'
8
+
9
+ export default class {
10
+ constructor(platform, accessory) {
11
+ this.accessory = accessory
12
+ this.clientResponseTopic = null
13
+ this.key = platform.accountDetails.key
14
+ this.platform = platform
15
+ this.queuedCommands = []
16
+ this.status = 'init'
17
+ this.userId = platform.accountDetails.userId
18
+ this.uuid = accessory.context.serialNumber
19
+ this.waitingMessageIds = {}
20
+ }
21
+
22
+ connect() {
23
+ const randomUUID = this.accessory.UUID.substring(0, this.accessory.UUID.length - 6) + generateRandomString(6)
24
+ const appId = createHash('md5')
25
+ .update(`API${randomUUID}`)
26
+ .digest('hex')
27
+ this.client = mqttConnect({
28
+ protocol: 'mqtts',
29
+ host: this.accessory.context.domain || 'eu-iotx.meross.com',
30
+ port: 2001,
31
+ clientId: `app:${appId}`,
32
+ username: this.userId,
33
+ password: createHash('md5')
34
+ .update(this.userId + this.key)
35
+ .digest('hex'),
36
+ rejectUnauthorized: true,
37
+ keepalive: 30,
38
+ reconnectPeriod: 5000,
39
+ })
40
+
41
+ this.client.on('connect', () => {
42
+ this.client.subscribe(`/app/${this.userId}/subscribe`, (err) => {
43
+ if (err) {
44
+ this.accessory.logWarn(`mqtt subscribe error - ${err}`)
45
+ }
46
+ })
47
+
48
+ this.clientResponseTopic = `/app/${this.userId}-${appId}/subscribe`
49
+ this.client.subscribe(this.clientResponseTopic, (err) => {
50
+ if (err) {
51
+ this.accessory.logWarn(`mqtt user-response subscribe error - ${err}`)
52
+ }
53
+ this.accessory.logDebug('mqtt subscribe complete')
54
+ })
55
+ this.status = 'online'
56
+ while (this.queuedCommands.length > 0) {
57
+ const resolveFn = this.queuedCommands.pop()
58
+ if (typeof resolveFn === 'function') {
59
+ resolveFn()
60
+ }
61
+ }
62
+ })
63
+
64
+ this.client.on('message', (topic, msg) => {
65
+ if (!msg) {
66
+ return
67
+ }
68
+
69
+ const msgStr = msg.toString()
70
+
71
+ let decMsg
72
+ try {
73
+ decMsg = JSON.parse(msgStr)
74
+ } catch (e) {
75
+ this.accessory.logWarn(`mqtt message error - [${e}] [${msgStr}]]`)
76
+ return
77
+ }
78
+ if (!decMsg.header?.from.includes(this.uuid)) {
79
+ return
80
+ }
81
+
82
+ // If message is the RESP for a previous action,
83
+ // process return the control to the 'stopped' method.
84
+ const resolveForThisMessage = this.waitingMessageIds[decMsg.header.messageId]
85
+ if (typeof resolveForThisMessage === 'function') {
86
+ resolveForThisMessage({ data: decMsg })
87
+ delete this.waitingMessageIds[decMsg.header.messageId]
88
+ } else if (decMsg.header.method === 'PUSH') {
89
+ // Otherwise, process it accordingly
90
+ if (this.accessory.control?.receiveUpdate && decMsg.payload) {
91
+ this.accessory.control.receiveUpdate(decMsg)
92
+ }
93
+ }
94
+ })
95
+ this.client.on('error', (error) => {
96
+ this.accessory.logWarn(`mqtt connection error${error ? ` [${error.toString()}]` : ''}`)
97
+ })
98
+ this.client.on('close', (error) => {
99
+ this.accessory.logWarn(`mqtt connection closed${error ? ` [${error.toString()}]` : ''}`)
100
+ this.status = 'offline'
101
+ })
102
+ this.client.on('reconnect', () => {
103
+ this.accessory.logWarn('mqtt connection reconnecting')
104
+ this.status = 'offline'
105
+ })
106
+ }
107
+
108
+ disconnect() {
109
+ this.client.end(true)
110
+ }
111
+
112
+ async sendUpdate(accessory, toSend) {
113
+ // Timeout shorter for get updates than set updates
114
+ const timeout = toSend.method === 'GET' ? 4000 : 9000
115
+ // Helper to queue commands before the device is connected
116
+ if (this.status !== 'online') {
117
+ let connectResolve
118
+
119
+ // We create a idle promise - connectPromise
120
+ const connectPromise = new Promise((resolve) => {
121
+ connectResolve = resolve
122
+ })
123
+
124
+ // connectPromise will get resolved when the device connects
125
+ this.queuedCommands.push(connectResolve)
126
+ // when the device is connected, the futureCommand will be executed
127
+ // that is exactly the same command issued now, but in the future
128
+ const futureCommand = () => this.sendUpdate(toSend)
129
+ // we return immediately an 'idle' promise, that when it gets resolved
130
+ // it will then execute the futureCommand
131
+ // IF the above takes too much time, the command will fail with a TimeoutError
132
+ return pTimeout(connectPromise.then(futureCommand), {
133
+ milliseconds: timeout,
134
+ })
135
+ }
136
+
137
+ let commandResolve
138
+ // create an awaiting promise, it will get (maybe) resolved if the device responds in time
139
+ const commandPromise = new Promise((resolve) => {
140
+ commandResolve = resolve
141
+ })
142
+
143
+ const messageId = createHash('md5')
144
+ .update(generateRandomString(16))
145
+ .digest('hex')
146
+ const timestamp = Math.round(new Date().getTime() / 1000)
147
+
148
+ const data = {
149
+ header: {
150
+ from: this.clientResponseTopic,
151
+ messageId,
152
+ method: toSend.method,
153
+ namespace: toSend.namespace,
154
+ payloadVersion: 1,
155
+ sign: createHash('md5')
156
+ .update(messageId + this.key + timestamp)
157
+ .digest('hex'),
158
+ timestamp,
159
+ },
160
+ payload: toSend.payload || {},
161
+ }
162
+
163
+ // Log to send
164
+ accessory.logDebug(`${platformLang.sendMQTT}: ${JSON.stringify(data)}`)
165
+
166
+ // Send the message
167
+ this.client.publish(`/appliance/${this.uuid}/subscribe`, JSON.stringify(data))
168
+ this.waitingMessageIds[messageId] = commandResolve
169
+ // the command returns with a timeout
170
+ return pTimeout(commandPromise, {
171
+ milliseconds: timeout,
172
+ })
173
+ }
174
+ }