homebridge-meross
Version:
Homebridge plugin to control Meross devices.
310 lines (266 loc) • 9.94 kB
JavaScript
/* jshint node: true,esversion: 9, -W014, -W033 */
/* eslint-disable new-cap */
'use strict'
const crypto = require('crypto')
const mqtt = require('mqtt')
const pTimeout = require('promise-timeout')
module.exports = class connectionMQTT {
constructor (platform, accessory) {
this.accessory = accessory
this.clientResponseTopic = null
this.funcs = platform.funcs
this.key = platform.accountDetails.key
this.lang = platform.lang
this.log = platform.log
this.name = accessory.displayName
this.platform = platform
this.queuedCommands = []
this.status = 'init'
this.token = platform.accountDetails.token
this.userid = platform.accountDetails.userid
this.uuid = accessory.context.serialNumber
this.waitingMessageIds = {}
}
connect () {
const appId = crypto
.createHash('md5')
.update('API' + this.accessory.UUID)
.digest('hex')
this.client = mqtt.connect({
protocol: 'mqtts',
host: this.accessory.context.domain || 'eu-iot.meross.com',
port: 2001,
clientId: 'app:' + appId,
username: this.userid,
password: crypto
.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.log.warn('[%s] mqtt subscribe error - %s.', this.name, err)
}
})
this.clientResponseTopic = '/app/' + this.userid + '-' + appId + '/subscribe'
this.client.subscribe(this.clientResponseTopic, err => {
if (err) {
this.log.warn('[%s] mqtt user-response subscribe error - %s.', this.name, err)
}
if (this.accessory.context.enableDebugLogging) {
this.log('[%s] mqtt subscribe complete.', this.name)
}
})
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 decMsg = JSON.parse(msg.toString())
if (decMsg.header.from && !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 && this.accessory.control.receiveUpdate && decMsg.payload) {
this.accessory.control.receiveUpdate(decMsg)
}
}
})
this.client.on('error', error => {
this.log.warn(
'[%s] mqtt connection error%s.',
this.name,
error ? ' [' + error.toString() + ']' : ''
)
})
this.client.on('close', error => {
this.log.warn(
'[%s] mqtt connection closed%s.',
this.name,
error ? ' [' + error.toString() + ']' : ''
)
this.status = 'offline'
})
this.client.on('reconnect', () => {
this.log.warn('[%s] mqtt connection reconnecting.', this.name)
this.status = 'offline'
})
}
disconnect () {
this.client.end(true)
}
async sendUpdate (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.timeout(connectPromise.then(futureCommand), timeout)
}
let commandResolve
// create of an waiting Promise, it will get (maybe) resolved if the device
// responds in time
const commandPromise = new Promise(resolve => {
commandResolve = resolve
})
// if not subscribed und so ...
const messageId = crypto
.createHash('md5')
.update(this.funcs.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: crypto
.createHash('md5')
.update(messageId + this.key + timestamp)
.digest('hex'),
timestamp
},
payload: toSend.payload || {}
}
// Log the send if in debug mode
if (this.accessory.context.enableDebugLogging) {
this.log('[%s] %s: %s.', this.name, this.lang.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.timeout(commandPromise, timeout)
}
async getSystemAllData () {
return this.sendUpdate('GET', 'Appliance.System.All', {})
}
async getSystemDebug () {
return this.sendUpdate('GET', 'Appliance.System.Debug', {})
}
async getSystemAbilities () {
return this.sendUpdate('GET', 'Appliance.System.Ability', {})
}
async getSystemReport () {
return this.sendUpdate('GET', 'Appliance.System.Report', {})
}
async getSystemRuntime () {
return this.sendUpdate('GET', 'Appliance.System.Runtime', {})
}
async getSystemDNDMode () {
return this.sendUpdate('GET', 'Appliance.System.DNDMode', {})
}
async setSystemDNDMode (onoff) {
const payload = { DNDMode: { mode: onoff ? 1 : 0 } }
return this.sendUpdate('SET', 'Appliance.System.DNDMode', payload)
}
async getOnlineStatus () {
return this.sendUpdate('GET', 'Appliance.System.Online', {})
}
async getConfigWifiList () {
return this.sendUpdate('GET', 'Appliance.Config.WifiList', {})
}
async getConfigTrace () {
return this.sendUpdate('GET', 'Appliance.Config.Trace', {})
}
async getControlPowerConsumption () {
return this.sendUpdate('GET', 'Appliance.Control.Consumption', {})
}
async getControlPowerConsumptionX () {
return this.sendUpdate('GET', 'Appliance.Control.ConsumptionX', {})
}
async getControlElectricity () {
return this.sendUpdate('GET', 'Appliance.Control.Electricity', {})
}
async getGarageDoorState (channel) {
const payload = { state: { channel: channel } }
return this.sendUpdate('GET', 'Appliance.GarageDoor.State', payload)
}
async controlToggle (onoff) {
const payload = { toggle: { onoff: onoff ? 1 : 0 } }
return this.sendUpdate('SET', 'Appliance.Control.Toggle', payload)
}
async controlToggleX (channel, onoff) {
const payload = { togglex: { channel: channel, onoff: onoff ? 1 : 0 } }
return this.sendUpdate('SET', 'Appliance.Control.ToggleX', payload)
}
async controlSpray (channel, mode) {
const payload = { spray: { channel: channel, mode: mode || 0 } }
return this.sendUpdate('SET', 'Appliance.Control.Spray', payload)
}
async controlGarageDoor (channel, open) {
const payload = { state: { channel: channel, open: open ? 1 : 0, uuid: this.uuid } }
return this.sendUpdate('SET', 'Appliance.GarageDoor.State', payload)
}
async controlRollerPosition (channel, position) {
const payload = { position: { channel: channel, position } }
return this.sendUpdate('SET', 'Appliance.RollerShutter.Position', payload)
}
async controlLight (light) {
const payload = { light: light }
return this.sendUpdate('SET', 'Appliance.Control.Light', payload)
}
async controlDiffusorSpray (type, channel, mode) {
const payload = { spray: [{ channel: channel, mode: mode || 0, uuid: this.uuid }] }
return this.sendUpdate('SET', 'Appliance.Control.Diffuser.Spray', payload)
}
async controlDiffusorLight (type, light) {
light.uuid = this.uuid
const payload = { light: [light] }
return this.sendUpdate('SET', 'Appliance.Control.Diffuser.Light', payload)
}
async getHubBattery () {
const payload = { battery: [] }
return this.sendUpdate('GET', 'Appliance.Hub.Battery', payload)
}
async getMts100All (ids) {
const payload = { all: [] }
ids.forEach(id => payload.all.push({ id: id }))
return this.sendUpdate('GET', 'Appliance.Hub.Mts100.All', payload)
}
async controlHubToggleX (subId, onoff) {
const payload = { togglex: [{ id: subId, onoff: onoff ? 1 : 0 }] }
return this.sendUpdate('SET', 'Appliance.Hub.ToggleX', payload)
}
async controlHubMts100Mode (subId, mode) {
const payload = { mode: [{ id: subId, state: mode }] }
return this.sendUpdate('SET', 'Appliance.Hub.Mts100.Mode', payload)
}
async controlHubMts100Temperature (subId, temp) {
temp.id = subId
const payload = { temperature: [temp] }
return this.sendUpdate('SET', 'Appliance.Hub.Mts100.Temperature', payload)
}
}