@homebridge-plugins/homebridge-meross
Version:
Homebridge plugin to integrate Meross devices into HomeKit.
175 lines (155 loc) • 5.68 kB
JavaScript
import { createHash } from 'node:crypto'
import { connect as mqttConnect } from 'mqtt'
import pTimeout from 'p-timeout'
import { generateRandomString } from '../utils/functions.js'
import platformLang from '../utils/lang-en.js'
export default class {
constructor(platform, accessory) {
this.accessory = accessory
this.clientResponseTopic = null
this.key = platform.accountDetails.key
this.platform = platform
this.queuedCommands = []
this.status = 'init'
this.userId = platform.accountDetails.userId
this.uuid = accessory.context.serialNumber
this.waitingMessageIds = {}
}
connect() {
const randomUUID = this.accessory.UUID.substring(0, this.accessory.UUID.length - 6) + generateRandomString(6)
const appId = createHash('md5')
.update(`API${randomUUID}`)
.digest('hex')
this.client = mqttConnect({
protocol: 'mqtts',
host: this.accessory.context.domain || 'eu-iotx.meross.com',
port: 2001,
clientId: `app:${appId}`,
username: this.userId,
password: createHash('md5')
.update(this.userId + this.key)
.digest('hex'),
rejectUnauthorized: true,
keepalive: 30,
reconnectPeriod: 5000,
})
this.client.on('connect', () => {
this.client.subscribe(`/app/${this.userId}/subscribe`, (err) => {
if (err) {
this.accessory.logWarn(`mqtt subscribe error - ${err}`)
}
})
this.clientResponseTopic = `/app/${this.userId}-${appId}/subscribe`
this.client.subscribe(this.clientResponseTopic, (err) => {
if (err) {
this.accessory.logWarn(`mqtt user-response subscribe error - ${err}`)
}
this.accessory.logDebug('mqtt subscribe complete')
})
this.status = 'online'
while (this.queuedCommands.length > 0) {
const resolveFn = this.queuedCommands.pop()
if (typeof resolveFn === 'function') {
resolveFn()
}
}
})
this.client.on('message', (topic, msg) => {
if (!msg) {
return
}
const msgStr = msg.toString()
let decMsg
try {
decMsg = JSON.parse(msgStr)
} catch (e) {
this.accessory.logWarn(`mqtt message error - [${e}] [${msgStr}]]`)
return
}
if (!decMsg.header?.from.includes(this.uuid)) {
return
}
// If message is the RESP for a previous action,
// process return the control to the 'stopped' method.
const resolveForThisMessage = this.waitingMessageIds[decMsg.header.messageId]
if (typeof resolveForThisMessage === 'function') {
resolveForThisMessage({ data: decMsg })
delete this.waitingMessageIds[decMsg.header.messageId]
} else if (decMsg.header.method === 'PUSH') {
// Otherwise, process it accordingly
if (this.accessory.control?.receiveUpdate && decMsg.payload) {
this.accessory.control.receiveUpdate(decMsg)
}
}
})
this.client.on('error', (error) => {
this.accessory.logWarn(`mqtt connection error${error ? ` [${error.toString()}]` : ''}`)
})
this.client.on('close', (error) => {
this.accessory.logWarn(`mqtt connection closed${error ? ` [${error.toString()}]` : ''}`)
this.status = 'offline'
})
this.client.on('reconnect', () => {
this.accessory.logWarn('mqtt connection reconnecting')
this.status = 'offline'
})
}
disconnect() {
this.client.end(true)
}
async sendUpdate(accessory, toSend) {
// Timeout shorter for get updates than set updates
const timeout = toSend.method === 'GET' ? 4000 : 9000
// Helper to queue commands before the device is connected
if (this.status !== 'online') {
let connectResolve
// We create a idle promise - connectPromise
const connectPromise = new Promise((resolve) => {
connectResolve = resolve
})
// connectPromise will get resolved when the device connects
this.queuedCommands.push(connectResolve)
// when the device is connected, the futureCommand will be executed
// that is exactly the same command issued now, but in the future
const futureCommand = () => this.sendUpdate(toSend)
// we return immediately an 'idle' promise, that when it gets resolved
// it will then execute the futureCommand
// IF the above takes too much time, the command will fail with a TimeoutError
return pTimeout(connectPromise.then(futureCommand), {
milliseconds: timeout,
})
}
let commandResolve
// create an awaiting promise, it will get (maybe) resolved if the device responds in time
const commandPromise = new Promise((resolve) => {
commandResolve = resolve
})
const messageId = createHash('md5')
.update(generateRandomString(16))
.digest('hex')
const timestamp = Math.round(new Date().getTime() / 1000)
const data = {
header: {
from: this.clientResponseTopic,
messageId,
method: toSend.method,
namespace: toSend.namespace,
payloadVersion: 1,
sign: createHash('md5')
.update(messageId + this.key + timestamp)
.digest('hex'),
timestamp,
},
payload: toSend.payload || {},
}
// Log to send
accessory.logDebug(`${platformLang.sendMQTT}: ${JSON.stringify(data)}`)
// Send the message
this.client.publish(`/appliance/${this.uuid}/subscribe`, JSON.stringify(data))
this.waitingMessageIds[messageId] = commandResolve
// the command returns with a timeout
return pTimeout(commandPromise, {
milliseconds: timeout,
})
}
}