UNPKG

@palekseii/homebridge-tuya-platform

Version:

Fork version of official Tuya Homebridge plugin. Brings a bunch of bug fix and new device support.

182 lines 7.38 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const mqtt_1 = __importDefault(require("mqtt")); const uuid_1 = require("uuid"); const crypto_1 = __importDefault(require("crypto")); const crypto_js_1 = __importDefault(require("crypto-js")); const Logger_1 = require("../util/Logger"); const GCM_TAG_LENGTH = 16; class TuyaOpenMQ { constructor(api, log = console, debug = false) { this.api = api; this.log = log; this.debug = debug; this.version = '1.0'; this.messageListeners = new Set(); this.linkId = (0, uuid_1.v4)(); // eslint-disable-next-line @typescript-eslint/no-explicit-any this.consumedQueue = []; this.log = new Logger_1.PrefixLogger(log, TuyaOpenMQ.name, debug); } start() { this._connect(); } stop() { if (this.timer) { clearTimeout(this.timer); } if (this.client) { this.client.removeAllListeners(); this.client.end(); } } async _connect() { this.stop(); const res = await this._getMQConfig('mqtt'); if (res.success === false) { this.log.warn('Get MQTT config failed. code = %s, msg = %s', res.code, res.msg); return; } const { url, client_id, username, password, expire_time, source_topic } = res.result; this.log.debug('Connecting to:', url); const client = mqtt_1.default.connect(url, { clientId: client_id, username: username, password: password, }); client.on('connect', this._onConnect.bind(this)); client.on('error', this._onError.bind(this)); client.on('end', this._onEnd.bind(this)); client.on('message', this._onMessage.bind(this)); client.subscribe(source_topic.device); this.client = client; this.config = res.result; // reconnect every 2 hours required this.timer = setTimeout(this._connect.bind(this), (expire_time - 60) * 1000); } async _getMQConfig(linkType) { const res = await this.api.post('/v1.0/iot-03/open-hub/access-config', { 'uid': this.api.tokenInfo.uid, 'link_id': this.linkId, 'link_type': linkType, 'topics': 'device', 'msg_encrypted_version': this.version, }); return res; } _onConnect() { this.log.debug('Connected'); } _onError(error) { this.log.error('Error:', error); } _onEnd() { this.log.debug('End'); } async _onMessage(topic, payload) { const { protocol, data, t } = JSON.parse(payload.toString()); const messageData = this._decodeMQMessage(data, this.config.password, t); if (!messageData) { this.log.warn('Message decode failed:', payload.toString()); return; } const message = JSON.parse(messageData); this.log.debug('onMessage:\ntopic = %s\nprotocol = %s\nmessage = %s\nt = %s', topic, protocol, JSON.stringify(message, null, 2), t); this._fixWrongOrderMessage(protocol, message, t); for (const listener of this.messageListeners) { listener(topic, protocol, message); } } _fixWrongOrderMessage(protocol, message, t) { if (protocol !== 4) { return; } const currentPayload = { protocol, message, t }; // eslint-disable-next-line @typescript-eslint/no-explicit-any const lastPayload = this.consumedQueue[this.consumedQueue.length - 1]; if (lastPayload && currentPayload.t < lastPayload.t) { this.log.debug('Message received with wrong order.'); this.log.debug('LastMessage: dataId = %s, t = %s', lastPayload.message.dataId, lastPayload.t); this.log.debug('CurrentMessage: dataId = %s, t = %s', message.dataId, t); this.log.debug('This may cause outdated device status update.'); // Use newer status to override current status. for (const _status of message.status) { for (const payload of this.consumedQueue.reverse()) { if (message.devId !== payload.message.devId) { continue; } const latestStatus = payload.message.status.find(item => item.code === _status.code); if (latestStatus) { if (latestStatus.value !== _status.value) { this.log.debug('Override status %o => %o', latestStatus, _status); _status.value = latestStatus.value; _status.t = latestStatus.t; } break; } } } return; } this.consumedQueue.push(currentPayload); while (this.consumedQueue.length > 0) { let t = this.consumedQueue[0].t; if (t > Math.pow(10, 12)) { // timestamp format always changing, seconds or milliseconds is not certain :( t = t / 1000; } // Remove message older than 30 seconds if (Date.now() / 1000 > t + 30) { this.consumedQueue.shift(); } else { break; } } } _decodeMQMessage_1_0(b64msg, password) { password = password.substring(8, 24); const msg = crypto_js_1.default.AES.decrypt(b64msg, crypto_js_1.default.enc.Utf8.parse(password), { mode: crypto_js_1.default.mode.ECB, padding: crypto_js_1.default.pad.Pkcs7, }).toString(crypto_js_1.default.enc.Utf8); return msg; } _decodeMQMessage_2_0(data, password, t) { // Base64 decoding generates Buffers const tmpbuffer = Buffer.from(data, 'base64'); const key = password.substring(8, 24).toString(); //get iv_length & iv_buffer const iv_length = tmpbuffer.readUIntBE(0, 4); const iv_buffer = tmpbuffer.slice(4, iv_length + 4); //Removes the IV bits of the head and 16 bits of the tail tags const data_buffer = tmpbuffer.slice(iv_length + 4, tmpbuffer.length - GCM_TAG_LENGTH); const cipher = crypto_1.default.createDecipheriv('aes-128-gcm', key, iv_buffer); //setAuthTag buffer cipher.setAuthTag(tmpbuffer.slice(tmpbuffer.length - GCM_TAG_LENGTH, tmpbuffer.length)); //setAAD buffer const buf = Buffer.allocUnsafe(6); buf.writeUIntBE(t, 0, 6); cipher.setAAD(buf); const msg = cipher.update(data_buffer); return msg.toString('utf8'); } _decodeMQMessage(data, password, t) { if (this.version === '2.0') { return this._decodeMQMessage_2_0(data, password, t); } else { return this._decodeMQMessage_1_0(data, password); } } addMessageListener(listener) { this.messageListeners.add(listener); } removeMessageListener(listener) { this.messageListeners.delete(listener); } } exports.default = TuyaOpenMQ; //# sourceMappingURL=TuyaOpenMQ.js.map