@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.
- package/CHANGELOG.md +1346 -0
- package/LICENSE +21 -0
- package/README.md +68 -0
- package/config.schema.json +2066 -0
- package/eslint.config.js +49 -0
- package/lib/connection/http.js +345 -0
- package/lib/connection/mqtt.js +174 -0
- package/lib/device/baby.js +532 -0
- package/lib/device/cooler-single.js +447 -0
- package/lib/device/diffuser.js +730 -0
- package/lib/device/fan.js +530 -0
- package/lib/device/garage-main.js +225 -0
- package/lib/device/garage-single.js +495 -0
- package/lib/device/garage-sub.js +376 -0
- package/lib/device/heater-single.js +445 -0
- package/lib/device/hub-contact.js +56 -0
- package/lib/device/hub-leak.js +86 -0
- package/lib/device/hub-main.js +403 -0
- package/lib/device/hub-sensor.js +115 -0
- package/lib/device/hub-smoke.js +40 -0
- package/lib/device/hub-valve.js +377 -0
- package/lib/device/humidifier.js +521 -0
- package/lib/device/index.js +63 -0
- package/lib/device/light-cct.js +474 -0
- package/lib/device/light-dimmer.js +312 -0
- package/lib/device/light-rgb.js +528 -0
- package/lib/device/outlet-multi.js +383 -0
- package/lib/device/outlet-single.js +405 -0
- package/lib/device/power-strip.js +282 -0
- package/lib/device/purifier-single.js +372 -0
- package/lib/device/purifier.js +403 -0
- package/lib/device/roller-location.js +317 -0
- package/lib/device/roller.js +234 -0
- package/lib/device/sensor-presence.js +201 -0
- package/lib/device/switch-multi.js +403 -0
- package/lib/device/switch-single.js +371 -0
- package/lib/device/template.js +177 -0
- package/lib/device/thermostat.js +493 -0
- package/lib/fakegato/LICENSE +21 -0
- package/lib/fakegato/fakegato-history.js +814 -0
- package/lib/fakegato/fakegato-storage.js +108 -0
- package/lib/fakegato/fakegato-timer.js +125 -0
- package/lib/fakegato/uuid.js +27 -0
- package/lib/homebridge-ui/public/index.html +316 -0
- package/lib/homebridge-ui/server.js +10 -0
- package/lib/index.js +8 -0
- package/lib/platform.js +1256 -0
- package/lib/utils/colour.js +581 -0
- package/lib/utils/constants.js +377 -0
- package/lib/utils/custom-chars.js +165 -0
- package/lib/utils/eve-chars.js +130 -0
- package/lib/utils/functions.js +39 -0
- package/lib/utils/lang-en.js +114 -0
- package/package.json +70 -0
package/eslint.config.js
ADDED
|
@@ -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
|
+
}
|